Compare commits

..

No commits in common. "b4867f88d5654f4f149d9ae15d360738c581fbee" and "6ed2ecd2e6a2920dcefe2f07cce78031223b1a70" have entirely different histories.

6 changed files with 59 additions and 35 deletions

View file

@ -146,11 +146,24 @@ 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,7 +295,6 @@ 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,7 +10,6 @@ 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
@ -76,7 +75,6 @@ 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,6 +34,7 @@ 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);
@ -100,6 +101,7 @@ 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);
@ -219,7 +221,17 @@ class DecryptResponseTest {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
byte[] encryptedResponse = getJsonResponse("9.9", Constants.HYBRID_ALGORITHM);
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);
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get);
@ -235,7 +247,17 @@ class DecryptResponseTest {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, "wrong-algorithm");
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);
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get);
@ -244,10 +266,15 @@ class DecryptResponseTest {
"Error message should mention unsupported algorithm");
}
private static byte[] getJsonResponse(String protocolVersion, String value) {
@Test
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("decryptResponse should throw SecurityError when private key not initialized")
void decryptResponse_noPrivateKey_shouldThrowSecurityError() {
SecureCompletionClient client = new SecureCompletionClient();
JsonObject packageJson = new JsonObject();
packageJson.addProperty("version", protocolVersion);
packageJson.addProperty("algorithm", value);
packageJson.addProperty("version", Constants.PROTOCOL_VERSION);
packageJson.addProperty("algorithm", Constants.HYBRID_ALGORITHM);
packageJson.addProperty("encrypted_aes_key", "dGVzdA==");
JsonObject encryptedPayload = new JsonObject();
encryptedPayload.addProperty("ciphertext", "dGVzdA==");
@ -255,16 +282,7 @@ class DecryptResponseTest {
encryptedPayload.addProperty("tag", "dGVzdA==");
packageJson.add("encrypted_payload", encryptedPayload);
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);
byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8);
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();
if (!dir1.mkdirs()) return;
if (!dir2.mkdirs()) return;
dir1.mkdirs();
dir2.mkdirs();
// Client 1
SecureCompletionClient client1 = new SecureCompletionClient();

View file

@ -104,12 +104,11 @@ 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};
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data)) {
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
@ -128,31 +127,28 @@ class SecureMemoryTest {
@DisplayName("SecureBuffer close should be idempotent")
void secureBuffer_close_idempotent() {
byte[] data = new byte[]{1, 2, 3};
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data)) {
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};
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true)) {
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};
try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true)) {
SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true);
assertFalse(buffer.unlock(), "Unlock should return false");
}
assertFalse(buffer.unlock(), "Unlock should return false");
}
@Test