Start with encryption

This commit is contained in:
Oracle 2026-04-23 19:22:01 +02:00
parent 9df61e0cd3
commit b6af1c9792
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
8 changed files with 413 additions and 79 deletions

View file

@ -3,6 +3,8 @@ package ai.nomyo;
import ai.nomyo.errors.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import java.io.File;
import java.nio.file.Files;
@ -11,7 +13,8 @@ import java.security.*;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Execution(ExecutionMode.CONCURRENT)
class SecureCompletionClientE2ETest {
private static final String TEST_PASSWORD = "e2e-test-password-456";
@ -31,7 +34,7 @@ class SecureCompletionClientE2ETest {
// Full Lifecycle E2E Tests
@Test
@Order(1)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("E2E: Generate keys, save to disk, load in new client, validate")
void e2e_fullLifecycle_generateSaveLoadValidate() {
// Step 1: Generate keys and save to disk
@ -72,7 +75,7 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(2)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("E2E: Generate plaintext keys, load, and validate")
void e2e_plaintextKeys_generateLoadValidate() {
// Generate plaintext keys (no password)
@ -96,7 +99,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(3)
@DisplayName("E2E: Key validation with various key sizes")
void e2e_keyValidation_variousSizes() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
@ -126,7 +128,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(4)
@DisplayName("E2E: HTTP status mapping covers all documented cases")
void e2e_httpStatusMapping_allCases() {
SecureCompletionClient client = new SecureCompletionClient();
@ -176,7 +177,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(5)
@DisplayName("E2E: Retryable status codes match Constants.RETRYABLE_STATUS_CODES")
void e2e_retryableStatusCodes_matchConstants() {
SecureCompletionClient client = new SecureCompletionClient();
@ -189,7 +189,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(6)
@DisplayName("E2E: URL encoding of public key PEM")
void e2e_urlEncoding_publicKey() {
SecureCompletionClient client = new SecureCompletionClient();
@ -214,7 +213,7 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(7)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("E2E: Multiple clients can independently generate and load keys")
void e2e_multipleClients_independentOperations() throws Exception {
File dir1 = tempDir.resolve("dir1").toFile();
@ -258,7 +257,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(8)
@DisplayName("E2E: Client constructor parameters are correctly set")
void e2e_clientConstructor_parametersSetCorrectly() {
SecureCompletionClient client = new SecureCompletionClient(
@ -275,7 +273,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(9)
@DisplayName("E2E: Client strips trailing slashes from routerUrl")
void e2e_clientConstructor_stripsTrailingSlashes() {
SecureCompletionClient client = new SecureCompletionClient(
@ -287,7 +284,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(10)
@DisplayName("E2E: Client uses default values when constructed with no args")
void e2e_clientConstructor_defaultValues() {
SecureCompletionClient client = new SecureCompletionClient();
@ -299,7 +295,7 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(11)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("E2E: Encrypted key file is unreadable without password")
void e2e_encryptedKey_unreadableWithoutPassword() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
@ -321,7 +317,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(12)
@DisplayName("E2E: Generate keys without saving produces in-memory keys")
void e2e_generateKeys_noSave_producesInMemoryKeys() {
SecureCompletionClient client = new SecureCompletionClient();
@ -336,7 +331,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(13)
@DisplayName("E2E: SecurityError is thrown for null key validation")
void e2e_nullKeyValidation_throwsSecurityError() {
SecureCompletionClient client = new SecureCompletionClient();
@ -349,7 +343,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(14)
@DisplayName("E2E: mapHttpStatus returns null for 200 status")
void e2e_mapHttpStatus_200_returnsNull() {
SecureCompletionClient client = new SecureCompletionClient();
@ -359,7 +352,6 @@ class SecureCompletionClientE2ETest {
}
@Test
@Order(15)
@DisplayName("E2E: mapHttpStatus includes response body in error message")
void e2e_mapHttpStatus_includesResponseBody() {
SecureCompletionClient client = new SecureCompletionClient();

View file

@ -4,6 +4,8 @@ import ai.nomyo.errors.SecurityError;
import ai.nomyo.util.Pass2Key;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import java.io.File;
import java.math.BigInteger;
@ -16,29 +18,18 @@ import java.security.spec.RSAPrivateCrtKeySpec;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Execution(ExecutionMode.CONCURRENT)
class SecureCompletionClientTest {
private static final String TEST_PASSWORD = "test-password-123";
private static final String PLAINTEXT_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TBb\n-----END PRIVATE KEY-----";
private SecureCompletionClient client;
@BeforeEach
void setUp() {
client = new SecureCompletionClient();
}
@AfterEach
void tearDown() {
client = null;
}
// Key Generation Tests
@Test
@Order(1)
@DisplayName("generateKeys should create 4096-bit RSA key pair")
void generateKeys_shouldCreateValidKeyPair() {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
@ -51,9 +42,9 @@ class SecureCompletionClientTest {
}
@Test
@Order(2)
@DisplayName("generateKeys should produce keys with correct bit size")
void generateKeys_shouldProduceCorrectKeySize() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
@ -66,9 +57,9 @@ class SecureCompletionClientTest {
}
@Test
@Order(3)
@DisplayName("generateKeys should create unique keys on each call")
void generateKeys_shouldProduceUniqueKeys() {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey firstKey = client.getPrivateKey();
@ -83,11 +74,12 @@ class SecureCompletionClientTest {
// Key Generation with File Save Tests
@Test
@Order(4)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("generateKeys with saveToFile=true should create key files")
void generateKeys_withSaveToFile_shouldCreateKeyFiles(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
@ -100,11 +92,12 @@ class SecureCompletionClientTest {
}
@Test
@Order(5)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("generateKeys with password should encrypt private key file")
void generateKeys_withPassword_shouldEncryptPrivateKey(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
@ -117,11 +110,12 @@ class SecureCompletionClientTest {
}
@Test
@Order(6)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("generateKeys should not overwrite existing key files")
void generateKeys_shouldNotOverwriteExistingKeys(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
long firstSize = privateKeyFile.length();
@ -136,10 +130,11 @@ class SecureCompletionClientTest {
// Key Loading Tests
@Test
@Order(7)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("loadKeys should load plaintext private key from file")
void loadKeys_plaintext_shouldLoadPrivateKey(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
PrivateKey originalKey = client.getPrivateKey();
@ -157,10 +152,11 @@ class SecureCompletionClientTest {
}
@Test
@Order(8)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("loadKeys should load encrypted private key with correct password")
void loadKeys_encrypted_correctPassword_shouldLoadPrivateKey(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
PrivateKey originalKey = client.getPrivateKey();
@ -179,10 +175,11 @@ class SecureCompletionClientTest {
}
@Test
@Order(9)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("loadKeys should handle wrong password gracefully")
void loadKeys_encrypted_wrongPassword_shouldHandleGracefully(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
SecureCompletionClient loadClient = new SecureCompletionClient();
@ -198,7 +195,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(10)
@DisplayName("loadKeys should throw exception for non-existent file")
void loadKeys_nonExistentFile_shouldThrowException() {
SecureCompletionClient loadClient = new SecureCompletionClient();
@ -213,9 +209,9 @@ class SecureCompletionClientTest {
// Key Validation Tests
@Test
@Order(11)
@DisplayName("validateRsaKey should accept valid 4096-bit key")
void validateRsaKey_validKey_shouldPass() {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey key = client.getPrivateKey();
@ -224,9 +220,9 @@ class SecureCompletionClientTest {
}
@Test
@Order(12)
@DisplayName("validateRsaKey should reject null key")
void validateRsaKey_nullKey_shouldThrowSecurityError() {
SecureCompletionClient client = new SecureCompletionClient();
SecurityError error = assertThrows(SecurityError.class, () ->
client.validateRsaKey(null));
@ -235,7 +231,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(13)
@DisplayName("validateRsaKey should reject keys below minimum size")
void validateRsaKey_tooSmallKey_shouldThrowSecurityError() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
@ -243,6 +238,7 @@ class SecureCompletionClientTest {
KeyPair pair = generator.generateKeyPair();
PrivateKey smallKey = pair.getPrivate();
SecureCompletionClient client = new SecureCompletionClient();
SecurityError error = assertThrows(SecurityError.class, () ->
client.validateRsaKey(smallKey));
@ -251,7 +247,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(14)
@DisplayName("validateRsaKey should accept minimum size key (2048 bits)")
void validateRsaKey_minimumSizeKey_shouldPass() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
@ -259,6 +254,7 @@ class SecureCompletionClientTest {
KeyPair pair = generator.generateKeyPair();
PrivateKey minKey = pair.getPrivate();
SecureCompletionClient client = new SecureCompletionClient();
assertDoesNotThrow(() -> client.validateRsaKey(minKey),
"2048-bit key should pass validation (minimum)");
}
@ -266,12 +262,13 @@ class SecureCompletionClientTest {
// Key Roundtrip Tests
@Test
@Order(15)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("Full roundtrip: generate, save, load should produce same key")
void roundtrip_generateSaveLoad_shouldProduceSameKey(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
// Generate and save
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
PrivateKey originalKey = client.getPrivateKey();
@ -291,7 +288,7 @@ class SecureCompletionClientTest {
}
@Test
@Order(16)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("Multiple generate/load cycles should work correctly")
void multipleCycles_shouldWorkCorrectly(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
@ -321,9 +318,9 @@ class SecureCompletionClientTest {
// Utility Method Tests
@Test
@Order(17)
@DisplayName("urlEncodePublicKey should properly encode PEM keys")
void urlEncodePublicKey_shouldEncodeCorrectly() {
SecureCompletionClient client = new SecureCompletionClient();
String pemKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n-----END PUBLIC KEY-----";
String encoded = client.urlEncodePublicKey(pemKey);
@ -334,9 +331,9 @@ class SecureCompletionClientTest {
}
@Test
@Order(18)
@DisplayName("urlEncodePublicKey should handle empty string")
void urlEncodePublicKey_emptyString_shouldReturnEmpty() {
SecureCompletionClient client = new SecureCompletionClient();
String encoded = client.urlEncodePublicKey("");
assertEquals("", encoded, "Empty string should encode to empty string");
}
@ -344,7 +341,6 @@ class SecureCompletionClientTest {
// Pass2Key Encryption/Decryption Tests
@Test
@Order(19)
@DisplayName("Pass2Key encrypt/decrypt should preserve plaintext")
void pass2Key_encryptDecrypt_shouldPreservePlaintext() throws Exception {
String plaintext = "Test plaintext content for encryption";
@ -361,7 +357,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(20)
@DisplayName("Pass2Key should produce different ciphertext for same plaintext")
void pass2Key_shouldProduceDifferentCiphertext() throws Exception {
String plaintext = "Same plaintext";
@ -375,7 +370,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(21)
@DisplayName("Pass2Key decrypt should fail with wrong password")
void pass2Key_wrongPassword_shouldFail() throws Exception {
String plaintext = "Secret content";
@ -391,7 +385,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(22)
@DisplayName("Pass2Key convertStringToPrivateKey should parse PEM correctly")
void convertStringToPrivateKey_shouldParsePEM() throws Exception {
SecureCompletionClient tempClient = new SecureCompletionClient();
@ -408,7 +401,6 @@ class SecureCompletionClientTest {
}
@Test
@Order(23)
@DisplayName("Pass2Key convertStringToPrivateKey should handle PEM with whitespace")
void convertStringToPrivateKey_shouldHandleWhitespace() throws Exception {
SecureCompletionClient tempClient = new SecureCompletionClient();

View file

@ -0,0 +1,166 @@
package ai.nomyo.util;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
@Execution(ExecutionMode.CONCURRENT)
class PEMConverterTest {
private static String realPublicKeyPem;
private static String realPrivateKeyPem;
@BeforeAll
static void loadRealKeys() throws Exception {
Path keyDir = Path.of("client_keys");
realPublicKeyPem = Files.readString(keyDir.resolve("public_key.pem"));
realPrivateKeyPem = Files.readString(keyDir.resolve("private_key.pem"));
}
// validatePEM Tests
@Test
@DisplayName("validatePEM should accept real public key file content")
void validatePEM_realPublicKeyFile_shouldPass() {
assertTrue(PEMConverter.validatePEM(realPublicKeyPem),
"Real public key file content should be valid PEM");
}
@Test
@DisplayName("validatePEM should accept PEM generated by toPEM")
void validatePEM_generatedByToPEM_shouldPass() {
String b64Content = realPublicKeyPem
.replace("-----BEGIN PUBLIC KEY-----\n", "")
.replace("\n-----END PUBLIC KEY-----", "")
.replace("\n", "");
byte[] decoded = java.util.Base64.getDecoder().decode(b64Content);
String generatedPem = PEMConverter.toPEM(decoded, false);
assertTrue(PEMConverter.validatePEM(generatedPem),
"PEM generated by toPEM should be valid");
}
@Test
@DisplayName("validatePEM should reject null input")
void validatePEM_nullInput_shouldReturnFalse() {
assertFalse(PEMConverter.validatePEM(null));
}
@Test
@DisplayName("validatePEM should reject blank input")
void validatePEM_blankInput_shouldReturnFalse() {
assertFalse(PEMConverter.validatePEM(" "));
assertFalse(PEMConverter.validatePEM(""));
}
@Test
@DisplayName("validatePEM should reject private key PEM")
void validatePEM_privateKey_shouldReturnFalse() {
assertFalse(PEMConverter.validatePEM(realPrivateKeyPem));
}
@Test
@DisplayName("validatePEM should reject truncated PEM")
void validatePEM_truncated_shouldReturnFalse() {
assertFalse(PEMConverter.validatePEM("-----BEGIN PUBLIC KEY-----\nMIIBIjAN"));
}
@Test
@DisplayName("validatePEM should reject PEM with wrong end marker")
void validatePEM_wrongEndMarker_shouldReturnFalse() {
String pem = "-----BEGIN PUBLIC KEY-----\nMIIBIjAN\n-----BEGIN PRIVATE KEY-----";
assertFalse(PEMConverter.validatePEM(pem));
}
@Test
@DisplayName("validatePEM should accept PEM with trailing newline")
void validatePEM_trailingNewline_shouldPass() {
String pemWithNewline = realPublicKeyPem + "\n";
assertTrue(PEMConverter.validatePEM(pemWithNewline),
"PEM with trailing newline should be valid");
}
// toPEM Tests
@Test
@DisplayName("toPEM should produce correct header and footer")
void toPEM_publicKey_shouldHaveCorrectMarkers() {
String b64Content = realPublicKeyPem
.replace("-----BEGIN PUBLIC KEY-----\n", "")
.replace("\n-----END PUBLIC KEY-----", "")
.replace("\n", "");
byte[] decoded = java.util.Base64.getDecoder().decode(b64Content);
String pem = PEMConverter.toPEM(decoded, false);
assertTrue(pem.startsWith("-----BEGIN PUBLIC KEY-----\n"));
assertTrue(pem.contains("-----END PUBLIC KEY-----"));
}
@Test
@DisplayName("toPEM should produce correct header and footer for private key")
void toPEM_privateKey_shouldHaveCorrectMarkers() {
String b64Content = realPrivateKeyPem
.replace("-----BEGIN PRIVATE KEY-----\n", "")
.replace("\n-----END PRIVATE KEY-----", "")
.replace("\n", "");
byte[] decoded = java.util.Base64.getDecoder().decode(b64Content);
String pem = PEMConverter.toPEM(decoded, true);
assertTrue(pem.startsWith("-----BEGIN PRIVATE KEY-----\n"));
assertTrue(pem.contains("-----END PRIVATE KEY-----"));
}
@Test
@DisplayName("toPEM should wrap base64 content at 64 characters")
void toPEM_shouldWrapAt64Chars() {
byte[] keyBytes = new byte[100];
for (int i = 0; i < 100; i++) {
keyBytes[i] = (byte) i;
}
String pem = PEMConverter.toPEM(keyBytes, false);
String[] lines = pem.split("\n");
for (int i = 1; i < lines.length - 1; i++) {
assertTrue(lines[i].length() <= 64,
"Line " + i + " should be at most 64 chars, got " + lines[i].length());
}
}
// Roundtrip Tests
@Test
@DisplayName("toPEM + validatePEM roundtrip should always pass")
void roundtrip_toPEM_validatePEM_shouldPass() {
String b64Content = realPublicKeyPem
.replace("-----BEGIN PUBLIC KEY-----\n", "")
.replace("\n-----END PUBLIC KEY-----", "")
.replace("\n", "");
byte[] decoded = java.util.Base64.getDecoder().decode(b64Content);
String pem = PEMConverter.toPEM(decoded, false);
assertTrue(PEMConverter.validatePEM(pem),
"PEM generated by toPEM should pass validatePEM");
}
@Test
@DisplayName("toPEM + validatePEM roundtrip should work for private keys")
void roundtrip_toPEM_validatePEM_privateKey_shouldPass() {
String b64Content = realPrivateKeyPem
.replace("-----BEGIN PRIVATE KEY-----\n", "")
.replace("\n-----END PRIVATE KEY-----", "")
.replace("\n", "");
byte[] decoded = java.util.Base64.getDecoder().decode(b64Content);
String pem = PEMConverter.toPEM(decoded, true);
assertFalse(PEMConverter.validatePEM(pem),
"Private key PEM should not pass validatePEM (public key check)");
}
}