Compare commits

...

2 commits

Author SHA1 Message Date
b4867f88d5
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/test/java/ai/nomyo/DecryptResponseTest.java
#	src/test/java/ai/nomyo/SecureMemoryTest.java
2026-04-29 17:06:58 +02:00
084ce14451
Misc cleanup 2026-04-29 17:05:24 +02:00
6 changed files with 35 additions and 59 deletions

View file

@ -146,24 +146,11 @@ public class SecureChatCompletion {
/**
* Convenience variant with no additional parameters.
*/
@SuppressWarnings("UnusedReturnValue")
public Map<String, Object> create(String model, List<Map<String, Object>> messages) {
return create(model, messages, null);
}
/**
* Async alias for {@link #create(String, List, Map)}.
*/
public Map<String, Object> acreate(String model, List<Map<String, Object>> messages, Map<String, Object> kwargs) {
return create(model, messages, kwargs);
}
/**
* Async alias for {@link #create(String, List)}.
*/
public Map<String, Object> acreate(String model, List<Map<String, Object>> messages) {
return create(model, messages);
}
/**
* Delegates to {@link SecureCompletionClient#close()}.
*/

View file

@ -295,6 +295,7 @@ public class SecureCompletionClient {
* @return encrypted bytes (JSON package)
* @throws SecurityError if encryption fails or keys not loaded
*/
@SuppressWarnings("JavadocDeclaration")
public CompletableFuture<byte[]> encryptPayload(Map<String, Object> payload) {
return CompletableFuture.supplyAsync(() -> {
try {

View file

@ -10,6 +10,7 @@ import java.util.Map;
/**
* Cross-platform memory locking and secure zeroing for sensitive cryptographic buffers. Fails gracefully if unavailable.
*/
@SuppressWarnings("SameReturnValue")
public final class SecureMemory {
@Getter
@ -75,6 +76,7 @@ public final class SecureMemory {
/**
* Wraps bytes with memory locking and guaranteed zeroing on close. AutoCloseable for try-with-resources.
*/
@SuppressWarnings("SameReturnValue")
public static class SecureBuffer implements AutoCloseable {
private final Arena arena;

View file

@ -34,7 +34,6 @@ class DecryptResponseTest {
void decryptResponse_validPackage_shouldReturnDecryptedMap() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
String plaintext = "{\"content\":\"Hello, world!\",\"role\":\"assistant\"}";
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
@ -101,7 +100,6 @@ class DecryptResponseTest {
void decryptResponse_missingProcessedAt_shouldSetNull() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
String plaintext = "{\"response\":\"ok\"}";
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
@ -221,17 +219,7 @@ class DecryptResponseTest {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
JsonObject packageJson = new JsonObject();
packageJson.addProperty("version", "9.9");
packageJson.addProperty("algorithm", Constants.HYBRID_ALGORITHM);
packageJson.addProperty("encrypted_aes_key", "dGVzdA==");
JsonObject encryptedPayload = new JsonObject();
encryptedPayload.addProperty("ciphertext", "dGVzdA==");
encryptedPayload.addProperty("nonce", "dGVzdA==");
encryptedPayload.addProperty("tag", "dGVzdA==");
packageJson.add("encrypted_payload", encryptedPayload);
byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8);
byte[] encryptedResponse = getJsonResponse("9.9", Constants.HYBRID_ALGORITHM);
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get);
@ -247,17 +235,7 @@ class DecryptResponseTest {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
JsonObject packageJson = new JsonObject();
packageJson.addProperty("version", Constants.PROTOCOL_VERSION);
packageJson.addProperty("algorithm", "wrong-algorithm");
packageJson.addProperty("encrypted_aes_key", "dGVzdA==");
JsonObject encryptedPayload = new JsonObject();
encryptedPayload.addProperty("ciphertext", "dGVzdA==");
encryptedPayload.addProperty("nonce", "dGVzdA==");
encryptedPayload.addProperty("tag", "dGVzdA==");
packageJson.add("encrypted_payload", encryptedPayload);
byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8);
byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, "wrong-algorithm");
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get);
@ -266,15 +244,10 @@ class DecryptResponseTest {
"Error message should mention unsupported algorithm");
}
@Test
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("decryptResponse should throw SecurityError when private key not initialized")
void decryptResponse_noPrivateKey_shouldThrowSecurityError() {
SecureCompletionClient client = new SecureCompletionClient();
private static byte[] getJsonResponse(String protocolVersion, String value) {
JsonObject packageJson = new JsonObject();
packageJson.addProperty("version", Constants.PROTOCOL_VERSION);
packageJson.addProperty("algorithm", Constants.HYBRID_ALGORITHM);
packageJson.addProperty("version", protocolVersion);
packageJson.addProperty("algorithm", value);
packageJson.addProperty("encrypted_aes_key", "dGVzdA==");
JsonObject encryptedPayload = new JsonObject();
encryptedPayload.addProperty("ciphertext", "dGVzdA==");
@ -282,7 +255,16 @@ class DecryptResponseTest {
encryptedPayload.addProperty("tag", "dGVzdA==");
packageJson.add("encrypted_payload", encryptedPayload);
byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8);
return packageJson.toString().getBytes(StandardCharsets.UTF_8);
}
@Test
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("decryptResponse should throw SecurityError when private key not initialized")
void decryptResponse_noPrivateKey_shouldThrowSecurityError() {
SecureCompletionClient client = new SecureCompletionClient();
byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, Constants.HYBRID_ALGORITHM);
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get);

View file

@ -218,8 +218,8 @@ class SecureCompletionClientE2ETest {
void e2e_multipleClients_independentOperations() throws Exception {
File dir1 = tempDir.resolve("dir1").toFile();
File dir2 = tempDir.resolve("dir2").toFile();
dir1.mkdirs();
dir2.mkdirs();
if (!dir1.mkdirs()) return;
if (!dir2.mkdirs()) return;
// Client 1
SecureCompletionClient client1 = new SecureCompletionClient();

View file

@ -104,11 +104,12 @@ class SecureMemoryTest {
@DisplayName("SecureBuffer zero should clear all bytes")
void secureBuffer_zero_shouldClearBytes() {
byte[] data = new byte[]{(byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF};
SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data);
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data)) {
buffer.zero();
buffer.zero();
assertDoesNotThrow(buffer::zero, "Zeroing should not throw");
assertDoesNotThrow(buffer::zero, "Zeroing should not throw");
}
}
@Test
@ -127,28 +128,31 @@ class SecureMemoryTest {
@DisplayName("SecureBuffer close should be idempotent")
void secureBuffer_close_idempotent() {
byte[] data = new byte[]{1, 2, 3};
SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data);
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data)) {
assertDoesNotThrow(buffer::close, "First close should not throw");
assertDoesNotThrow(buffer::close, "Second close should not throw");
assertDoesNotThrow(buffer::close, "First close should not throw");
assertDoesNotThrow(buffer::close, "Second close should not throw");
}
}
@Test
@DisplayName("SecureBuffer lock should return false (not supported)")
void secureBuffer_lock_shouldReturnFalse() {
byte[] data = new byte[]{1, 2, 3};
SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true);
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true)) {
assertFalse(buffer.lock(), "Lock should return false (not supported)");
assertFalse(buffer.lock(), "Lock should return false (not supported)");
}
}
@Test
@DisplayName("SecureBuffer unlock should return false")
void secureBuffer_unlock_shouldReturnFalse() {
byte[] data = new byte[]{1, 2, 3};
SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true);
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true)) {
assertFalse(buffer.unlock(), "Unlock should return false");
assertFalse(buffer.unlock(), "Unlock should return false");
}
}
@Test