Compare commits
No commits in common. "b4867f88d5654f4f149d9ae15d360738c581fbee" and "6ed2ecd2e6a2920dcefe2f07cce78031223b1a70" have entirely different histories.
b4867f88d5
...
6ed2ecd2e6
6 changed files with 59 additions and 35 deletions
|
|
@ -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()}.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue