Files
ZeroEcho/lib/src/test/java/zeroecho/sdk/guard/SessionRecipientOpenerContractTest.java

140 lines
6.2 KiB
Java

/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.guard;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import zeroecho.core.io.Util;
import zeroecho.sdk.Pbkdf2Limits;
import zeroecho.sdk.ZeroEchoSession;
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
class SessionRecipientOpenerContractTest {
@Test
void publicApiExposesOnlyReusableSessionOpeners() {
System.out.println("publicApiExposesOnlyReusableSessionOpeners");
assertSessionOnlyConstructor(KemCtxOpener.class);
assertSessionOnlyConstructor(EncCtxOpener.class);
List<Method> addOpenerMethods = Arrays.stream(MultiRecipientDataSourceBuilder.class.getMethods())
.filter(method -> method.getName().equals("addOpener")).toList();
assertEquals(1, addOpenerMethods.size());
assertEquals(RecipientOpener.class, addOpenerMethods.get(0).getParameterTypes()[0]);
System.out.println("...addOpenerOverloads=1");
System.out.println("publicApiExposesOnlyReusableSessionOpeners...ok");
}
@Test
void sessionOpenersIgnoreUnrelatedEntryFamilies() throws Exception {
System.out.println("sessionOpenersIgnoreUnrelatedEntryFamilies");
ZeroEchoSession session = new ZeroEchoSession();
UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' });
try {
assertNull(new KemCtxOpener(session).tryOpen("CTX-ENC:RSA", new byte[0], material));
assertNull(new EncCtxOpener(session).tryOpen("KEM:ML-KEM:GCM-WRAP", new byte[0], material));
} finally {
material.destroy();
}
System.out.println("...ignoredFamilies=2");
System.out.println("sessionOpenersIgnoreUnrelatedEntryFamilies...ok");
}
@Test
void customReusableOpenerScansEveryEntryAndClosesOnce() throws Exception {
System.out.println("customReusableOpenerScansEveryEntryAndClosesOnce");
ZeroEchoSession session = new ZeroEchoSession().withPbkdf2Limits(new Pbkdf2Limits(20_000, 30_000));
TrackingOpener opener = new TrackingOpener();
UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' });
try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session)
.withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()).unlockWith(material)
.addOpener(opener); MultiRecipientContent content = builder.build(false)) {
content.setInput(() -> new ByteArrayInputStream(twoEntryHeader()));
assertThrows(IOException.class, content::getStream);
} finally {
material.destroy();
}
assertEquals(2, opener.attempts);
assertEquals(1, opener.closeCount);
System.out.println("...attempts=" + opener.attempts);
System.out.println("customReusableOpenerScansEveryEntryAndClosesOnce...ok");
}
private static void assertSessionOnlyConstructor(Class<?> openerClass) {
Constructor<?>[] constructors = openerClass.getConstructors();
assertEquals(1, constructors.length);
assertEquals(ZeroEchoSession.class, constructors[0].getParameterTypes()[0]);
}
private static byte[] twoEntryHeader() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
Util.writePack7I(output, 2);
Util.writeUTF8(output, "first");
Util.write(output, new byte[] { 1 });
Util.writeUTF8(output, "second");
Util.write(output, new byte[] { 2 });
return output.toByteArray();
}
private static final class TrackingOpener implements RecipientOpener {
private int attempts;
private int closeCount;
@Override
public byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) {
attempts++;
return null;
}
@Override
public void close() {
closeCount++;
}
}
}