From 9df61e0cd3baec6b0a03c6310c67e5243729a620 Mon Sep 17 00:00:00 2001
From: Oracle
Date: Thu, 23 Apr 2026 13:36:46 +0200
Subject: [PATCH] AGENTS.md + code cleanup
---
AGENTS.md | 51 +++
src/main/java/ai/nomyo/Constants.java | 79 ++--
src/main/java/ai/nomyo/Main.java | 5 +-
.../java/ai/nomyo/SecureChatCompletion.java | 148 ++-----
.../java/ai/nomyo/SecureCompletionClient.java | 394 ++++++------------
src/main/java/ai/nomyo/SecureMemory.java | 187 +++------
.../ai/nomyo/errors/APIConnectionError.java | 26 +-
src/main/java/ai/nomyo/errors/APIError.java | 44 +-
.../ai/nomyo/errors/AuthenticationError.java | 24 +-
.../java/ai/nomyo/errors/ForbiddenError.java | 24 +-
.../ai/nomyo/errors/InvalidRequestError.java | 24 +-
.../java/ai/nomyo/errors/RateLimitError.java | 24 +-
.../java/ai/nomyo/errors/SecurityError.java | 23 +-
.../java/ai/nomyo/errors/ServerError.java | 24 +-
.../nomyo/errors/ServiceUnavailableError.java | 24 +-
src/main/java/ai/nomyo/util/PEMConverter.java | 6 +-
src/main/java/ai/nomyo/util/Pass2Key.java | 65 +--
src/main/java/ai/nomyo/util/Splitter.java | 30 +-
.../nomyo/SecureCompletionClientE2ETest.java | 46 +-
.../ai/nomyo/SecureCompletionClientTest.java | 27 +-
20 files changed, 365 insertions(+), 910 deletions(-)
create mode 100644 AGENTS.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9f87804
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,51 @@
+# nomyo4J — Agent Instructions
+
+Java port of the NOMYO Python client. Hybrid encryption (RSA-4096 + AES-256-GCM) for secure API communication.
+
+## Build & Run
+
+```
+mvn compile # Java 25, Lombok annotation processor
+mvn test # JUnit Jupiter 5.12.1, @Order enforced
+mvn test -Dtest=ClassName # single test class
+```
+
+## Architecture
+
+- **`SecureCompletionClient`** — low-level client: key mgmt, HTTP, encryption, decryption
+- **`SecureChatCompletion`** — high-level OpenAI-compatible surface (`create()`, `acreate()`)
+- **`Constants`** — all protocol/crypto constants (version, algorithms, timeouts)
+- **`SecureMemory`** — Java 25 FFM `SecureBuffer` for locked/zeroed memory
+- **`errors/`** — exception hierarchy, all `extends Exception` (checked)
+- **`util/`** — `Pass2Key` (PBKDF2 + AES-GCM), `PEMConverter`, `Splitter`
+
+## Critical: This is a partial/in-progress port
+
+Many methods are stubbed with `UnsupportedOperationException`. Before implementing, check `TRANSLATION_REFERENCE.md` for the Python reference. Stubbed methods:
+
+- `SecureCompletionClient.fetchServerPublicKey()` — GET `/pki/public_key`
+- `SecureCompletionClient.encryptPayload()` / `doEncrypt()` — hybrid encryption
+- `SecureCompletionClient.decryptResponse()` — response decryption
+- `SecureCompletionClient.sendSecureRequest()` (3 overloads) — full request lifecycle
+- `SecureCompletionClient.ensureKeys()` — key init (partial DCL implemented)
+- `SecureCompletionClient.close()` — resource cleanup
+- `SecureChatCompletion.create()` / `acreate()` — return `null`, stubbed
+- `SecureMemory` lock/unlock — always returns `false`
+
+**No JSON library** (Jackson/Gson) in `pom.xml` — needed for wire format serialization.
+
+## Key files
+
+- `TRANSLATION_REFERENCE.md` — **primary documentation**. Cross-language spec derived from Python reference. Read before implementing any method.
+- `client_keys/` — contains real RSA keys. **Gitignored.** Do not commit.
+- `Main.java` — entry point is `static void main()` — **not `public static void main(String[])`**. Cannot run standalone.
+
+## Conventions
+
+- Package: `ai.nomyo`
+- Lombok: `@Getter` on fields, `@Setter` on static flags
+- Tests: `@TestMethodOrder(OrderAnnotation.class)`, `@DisplayName` on every test
+- Error classes: checked exceptions with `status_code` and `error_details`
+- Key files: `PosixFilePermissions.OWNER_READ` only (mode 400)
+- RSA: 4096-bit, exponent 65537, OAEP-SHA256 padding
+- Protocol constants in `Constants.java` — marked "never change"
diff --git a/src/main/java/ai/nomyo/Constants.java b/src/main/java/ai/nomyo/Constants.java
index 25e249d..aacd1b4 100644
--- a/src/main/java/ai/nomyo/Constants.java
+++ b/src/main/java/ai/nomyo/Constants.java
@@ -4,63 +4,51 @@ package ai.nomyo;
import java.util.Set;
/**
- * Constants used throughout the NOMYO Java client library.
- *
- * These values correspond to the documented constants in the Python
- * reference. Protocol version and algorithm strings are immutable and
- * must never be changed — they are used for downgrade detection.
+ * Protocol, crypto, and configuration constants. Immutable — used for downgrade detection.
*/
public final class Constants {
// ── Protocol Constants ──────────────────────────────────────────
/**
- * Protocol version string. Never change — used for downgrade detection.
+ * Protocol version — never change (downgrade detection).
*/
public static final String PROTOCOL_VERSION = "1.0";
-
/**
- * Hybrid encryption algorithm identifier. Never change — used for downgrade detection.
+ * Hybrid encryption algorithm identifier — never change (downgrade detection).
*/
public static final String HYBRID_ALGORITHM = "hybrid-aes256-rsa4096";
-
/**
- * RSA-OAEP-SHA256 key wrapping algorithm identifier.
+ * RSA-OAEP-SHA256 key wrapping algorithm.
*/
public static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-SHA256";
-
/**
- * AES-256-GCM payload encryption algorithm identifier.
+ * AES-256-GCM payload encryption algorithm.
*/
public static final String PAYLOAD_ALGORITHM = "AES-256-GCM";
// ── Cryptographic Constants ─────────────────────────────────────
/**
- * RSA key size in bits. Fixed at 4096.
+ * RSA key size in bits.
*/
public static final int RSA_KEY_SIZE = 4096;
-
/**
- * RSA public exponent. Fixed at 65537.
+ * RSA public exponent.
*/
public static final int RSA_PUBLIC_EXPONENT = 65537;
-
/**
- * AES key size in bytes (256-bit). Per-request ephemeral.
+ * AES key size in bytes (256-bit).
*/
public static final int AES_KEY_SIZE = 32;
-
/**
- * GCM nonce size in bytes (96-bit). Per-request.
+ * GCM nonce size in bytes (96-bit).
*/
public static final int GCM_NONCE_SIZE = 12;
-
/**
* GCM authentication tag size in bytes.
*/
public static final int GCM_TAG_SIZE = 16;
-
/**
* Minimum RSA key size for validation (bits).
*/
@@ -69,7 +57,7 @@ public final class Constants {
// ── Payload Limits ──────────────────────────────────────────────
/**
- * Maximum payload size in bytes (10 MB). Used for DoS protection.
+ * Maximum payload size in bytes (10 MB).
*/
public static final long MAX_PAYLOAD_SIZE = 10L * 1024 * 1024;
@@ -79,61 +67,53 @@ public final class Constants {
* Default HTTP request timeout in seconds.
*/
public static final int DEFAULT_TIMEOUT_SECONDS = 60;
-
/**
- * Default number of retries on retryable errors.
- * Exponential backoff: 1s, 2s, 4s…
+ * Default retries on retryable errors (exponential backoff: 1s, 2s, 4s…).
*/
public static final int DEFAULT_MAX_RETRIES = 2;
-
/**
- * Set of HTTP status codes that are eligible for retry.
+ * Retryable HTTP status codes.
*/
public static final Set RETRYABLE_STATUS_CODES = Set.of(429, 500, 502, 503, 504);
// ── File Permission Constants ───────────────────────────────────
/**
- * File permission for private key files (owner read/write only).
+ * Private key file permission (owner rw only).
*/
public static final String PRIVATE_KEY_FILE_MODE = "rw-------";
-
/**
- * File permission for public key files (owner rw, group/others r).
+ * Public key file permission (owner rw, group/others r).
*/
public static final String PUBLIC_KEY_FILE_MODE = "rw-r--r--";
// ── Security Tier Constants ─────────────────────────────────────
/**
- * Valid security tier values. Case-sensitive.
+ * Valid security tier values (case-sensitive).
*/
public static final Set VALID_SECURITY_TIERS = Set.of("standard", "high", "maximum");
-
/**
- * Standard security tier — GPU general secure inference.
+ * GPU general secure inference.
*/
public static final String SECURITY_TIER_STANDARD = "standard";
-
/**
- * High security tier — CPU/GPU for sensitive business data.
+ * CPU/GPU for sensitive business data.
*/
public static final String SECURITY_TIER_HIGH = "high";
-
/**
- * Maximum security tier — CPU only for PHI/classified data.
+ * CPU only for PHI/classified data.
*/
public static final String SECURITY_TIER_MAXIMUM = "maximum";
// ── Endpoint Paths ──────────────────────────────────────────────
/**
- * PKI public key endpoint path.
+ * PKI public key endpoint.
*/
public static final String PKI_PUBLIC_KEY_PATH = "/pki/public_key";
-
/**
- * Secure chat completion endpoint path.
+ * Secure chat completion endpoint.
*/
public static final String SECURE_COMPLETION_PATH = "/v1/chat/secure_completion";
@@ -143,24 +123,20 @@ public final class Constants {
* Content-Type for encrypted payloads.
*/
public static final String CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
-
/**
- * HTTP header name for payload ID.
+ * Header name for payload ID.
*/
public static final String HEADER_PAYLOAD_ID = "X-Payload-ID";
-
/**
- * HTTP header name for client public key.
+ * Header name for client public key.
*/
public static final String HEADER_PUBLIC_KEY = "X-Public-Key";
-
/**
- * HTTP header name for security tier.
+ * Header name for security tier.
*/
public static final String HEADER_SECURITY_TIER = "X-Security-Tier";
-
/**
- * HTTP header prefix for Bearer token authorization.
+ * Bearer token prefix.
*/
public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer ";
@@ -170,17 +146,14 @@ public final class Constants {
* Default NOMYO router base URL.
*/
public static final String DEFAULT_BASE_URL = "https://api.nomyo.ai";
-
/**
- * Default key directory name for persisted keys.
+ * Default key directory name.
*/
public static final String DEFAULT_KEY_DIR = "client_keys";
-
/**
* Default private key file name.
*/
public static final String DEFAULT_PRIVATE_KEY_FILE = "private_key.pem";
-
/**
* Default public key file name.
*/
@@ -189,7 +162,7 @@ public final class Constants {
// ── Memory Protection Constants ─────────────────────────────────
/**
- * Page size used for memory locking calculations (typically 4096 bytes).
+ * Page size for memory locking calculations.
*/
public static final int PAGE_SIZE = 4096;
}
diff --git a/src/main/java/ai/nomyo/Main.java b/src/main/java/ai/nomyo/Main.java
index 26a9c4d..786f2bb 100644
--- a/src/main/java/ai/nomyo/Main.java
+++ b/src/main/java/ai/nomyo/Main.java
@@ -3,8 +3,7 @@ package ai.nomyo;
import ai.nomyo.errors.SecurityError;
/**
- * @author NieGestorben
- * Copyright© (c) 2026, All Rights Reserved.
+ * Entry point — loads RSA keys and validates key length.
*/
public class Main {
@@ -16,7 +15,7 @@ public class Main {
try {
secureCompletionClient.validateRsaKey(secureCompletionClient.getPrivateKey());
} catch (SecurityError e) {
- System.out.println("RSA Key is to short!");
+ System.out.println("RSA Key is too short!");
return;
}
diff --git a/src/main/java/ai/nomyo/SecureChatCompletion.java b/src/main/java/ai/nomyo/SecureChatCompletion.java
index 4823961..879c121 100644
--- a/src/main/java/ai/nomyo/SecureChatCompletion.java
+++ b/src/main/java/ai/nomyo/SecureChatCompletion.java
@@ -7,45 +7,7 @@ import java.util.List;
import java.util.Map;
/**
- * High-level OpenAI-compatible entrypoint for the NOMYO secure API.
- *
- * This class provides a familiar API surface matching {@code openai.ChatCompletion.create()}.
- * All requests are automatically encrypted using hybrid AES-256-GCM + RSA-4096 encryption
- * before being sent to the NOMYO router.
- *
- * Usage
- * {@code
- * SecureChatCompletion client = new SecureChatCompletion(
- * "https://api.nomyo.ai",
- * false,
- * "your-api-key",
- * true,
- * "/path/to/keys",
- * 2
- * );
- *
- * Map response = client.create(
- * "Qwen/Qwen3-0.6B",
- * List.of(Map.of("role", "user", "content", "Hello, world!"))
- * );
- * }
- *
- * Streaming
- * Streaming is not supported. The server rejects streaming requests with HTTP 400.
- * Always use {@code stream=false} (the default).
- *
- * Security Tiers
- * The {@code security_tier} parameter controls the hardware isolation level:
- *
- * - {@code "standard"} — GPU inference (general secure inference)
- * - {@code "high"} — CPU/GPU (sensitive business data)
- * - {@code "maximum"} — CPU only (PHI, classified data)
- *
- *
- * Key Persistence
- * Set {@code keyDir} to a directory path to persist RSA keys to disk.
- * Keys are generated on first use and reused across all calls.
- * Set {@code keyDir} to {@code null} for ephemeral keys (in-memory only, lost on restart).
+ * High-level OpenAI-compatible entrypoint with automatic hybrid encryption (AES-256-GCM + RSA-4096).
*/
@Getter
public class SecureChatCompletion {
@@ -55,83 +17,45 @@ public class SecureChatCompletion {
private final String keyDir;
/**
- * Constructs a {@code SecureChatCompletion} with default settings.
- *
- * Uses the default NOMYO router URL ({@code https://api.nomyo.ai}),
- * HTTPS-only, secure memory enabled, ephemeral keys, and 2 retries.
+ * Default settings: {@code https://api.nomyo.ai}, HTTPS-only, secure memory, ephemeral keys, 2 retries.
*/
public SecureChatCompletion() {
this(Constants.DEFAULT_BASE_URL, false, null, true, null, Constants.DEFAULT_MAX_RETRIES);
}
/**
- * Constructs a {@code SecureChatCompletion} with the specified settings.
- *
- * @param baseUrl NOMYO Router base URL (HTTPS enforced unless {@code allowHttp} is {@code true})
- * @param allowHttp permit {@code http://} URLs (development only)
- * @param apiKey Bearer token for authentication (can also be passed per-call via {@link #create})
- * @param secureMemory enable memory locking/zeroing (warns if unavailable)
- * @param keyDir directory to persist RSA keys; {@code null} = ephemeral (in-memory only)
- * @param maxRetries retries on 429/500/502/503/504 + network errors (exponential backoff: 1s, 2s, 4s…)
+ * @param baseUrl NOMYO Router base URL (HTTPS enforced unless {@code allowHttp})
+ * @param allowHttp permit {@code http://} URLs (development only)
+ * @param apiKey Bearer token (can also be passed per-call via {@link #create})
+ * @param secureMemory enable memory locking/zeroing
+ * @param keyDir RSA key directory; {@code null} = ephemeral
+ * @param maxRetries retries on 429/500/502/503/504 + network errors (exponential backoff)
*/
- public SecureChatCompletion(
- String baseUrl,
- boolean allowHttp,
- String apiKey,
- boolean secureMemory,
- String keyDir,
- int maxRetries
- ) {
+ public SecureChatCompletion(String baseUrl, boolean allowHttp, String apiKey, boolean secureMemory, String keyDir, int maxRetries) {
this.client = new SecureCompletionClient(baseUrl, allowHttp, secureMemory, maxRetries);
this.apiKey = apiKey;
this.keyDir = keyDir;
}
/**
- * Creates a chat completion with the specified parameters.
+ * Main entrypoint — same signature as {@code openai.ChatCompletion.create()}.
+ * All kwargs are passed through to the OpenAI-compatible API.
+ * Streaming is not supported (server rejects with HTTP 400).
+ * Security tiers: "standard", "high", "maximum".
*
- *
This is the main entrypoint, with the same signature as
- * {@code openai.ChatCompletion.create()}. Returns a map (not an object)
- * containing the OpenAI-compatible response.
- *
- * Parameters
- *
- * | Param | Type | Required | Description |
- * | {@code model} | {@code String} | yes | Model identifier, e.g. "Qwen/Qwen3-0.6B" |
- * | {@code messages} | {@code List | yes | OpenAI-format messages |
- * | {@code temperature} | {@code Double} | no | 0–2 |
- * | {@code maxTokens} | {@code Integer} | no | Maximum tokens in response |
- * | {@code topP} | {@code Double} | no | Top-p sampling parameter |
- * | {@code stop} | {@code String | List} | no | Stop sequences |
- * | {@code presencePenalty} | {@code Double} | no | -2.0 to 2.0 |
- * | {@code frequencyPenalty} | {@code Double} | no | -2.0 to 2.0 |
- * | {@code n} | {@code Integer} | no | Number of completions |
- * | {@code bestOf} | {@code Integer} | no | |
- * | {@code seed} | {@code Integer} | no | Reproducibility seed |
- * | {@code logitBias} | {@code Map} | no | Token bias map |
- * | {@code user} | {@code String} | no | End-user identifier |
- * | {@code tools} | {@code List | no | Tool definitions passed through to llama.cpp |
- * | {@code toolChoice} | {@code String} | no | "auto", "none", or specific tool name |
- * | {@code responseFormat} | {@code Map} | no | {"type": "json_object"} or {"type": "json_schema", ...} |
- * | {@code stream} | {@code Boolean} | no | NOT supported. Server rejects with HTTP 400. Always use {@code false}. |
- * | {@code baseUrl} | {@code String} | no | Per-call override (creates temp client internally) |
- * | {@code securityTier} | {@code String} | no | "standard", "high", or "maximum". Invalid values raise {@code ValueError}. |
- * | {@code apiKey} | {@code String} | no | Per-call override of instance {@code apiKey}. |
- *
- *
- * @param model model identifier (required)
- * @param messages OpenAI-format message list (required)
- * @param kwargs additional OpenAI-compatible parameters
- * @return OpenAI-compatible response map (see §6.2 of reference docs)
- * @throws SecurityError if encryption/decryption fails
- * @throws APIConnectionError if a network error occurs
- * @throws InvalidRequestError if the API returns 400
- * @throws AuthenticationError if the API returns 401
- * @throws ForbiddenError if the API returns 403
- * @throws RateLimitError if the API returns 429
- * @throws ServerError if the API returns 500
- * @throws ServiceUnavailableError if the API returns 503
- * @throws APIError for other errors
+ * @param model model identifier (required)
+ * @param messages OpenAI-format message list (required)
+ * @param kwargs additional OpenAI-compatible params (temperature, maxTokens, etc.)
+ * @return decrypted response map
+ * @throws SecurityError encryption/decryption failure
+ * @throws APIConnectionError network error
+ * @throws InvalidRequestError HTTP 400
+ * @throws AuthenticationError HTTP 401
+ * @throws ForbiddenError HTTP 403
+ * @throws RateLimitError HTTP 429
+ * @throws ServerError HTTP 500
+ * @throws ServiceUnavailableError HTTP 503
+ * @throws APIError other errors
*/
public Map create(String model, List
- *
- * @param payload the payload to encrypt (OpenAI-compatible chat parameters)
- * @return raw encrypted bytes (JSON package serialized to bytes)
- * @throws SecurityError if encryption fails or keys are not loaded
+ * @param payload OpenAI-compatible chat parameters
+ * @return encrypted bytes (JSON package)
+ * @throws SecurityError if encryption fails or keys not loaded
*/
public CompletableFuture encryptPayload(Map payload) {
throw new UnsupportedOperationException("Not yet implemented");
}
/**
- * Core hybrid encryption routine.
+ * Core hybrid encryption: AES-256-GCM encrypts {@code payloadBytes} with {@code aesKey}.
*/
public CompletableFuture doEncrypt(byte[] payloadBytes, byte[] aesKey) {
throw new UnsupportedOperationException("Not yet implemented");
}
- // ── Decryption ──────────────────────────────────────────────────
-
/**
- * Decrypts a server response.
+ * Decrypts server response.
*/
public CompletableFuture> decryptResponse(byte[] encryptedResponse, String payloadId) {
throw new UnsupportedOperationException("Not yet implemented");
}
- // ── Secure Request Lifecycle ────────────────────────────────────
-
/**
- * Full request lifecycle: encrypt → HTTP POST → retry → decrypt → return.
+ * encrypt → POST {routerUrl}/v1/chat/secure_completion → retry → decrypt → return.
+ * Headers: Content-Type=octet-stream, X-Payload-ID, X-Public-Key, Authorization (Bearer), X-Security-Tier.
+ * Retryable: 429, 500, 502, 503, 504 + network errors. Backoff: 2^(attempt-1)s.
+ *
Status mapping: 200→return, 400→InvalidRequestError, 401→AuthenticationError, 403→ForbiddenError,
+ * 404→APIError, 429→RateLimitError, 500→ServerError, 503→ServiceUnavailableError,
+ * 502/504→APIError(retryable), network→APIConnectionError.
*
- *
Request Headers
- *
- * Content-Type: application/octet-stream
- * X-Payload-ID: {payloadId}
- * X-Public-Key: {urlEncodedPublicPemKey}
- * Authorization: Bearer {apiKey} (if apiKey is provided)
- * X-Security-Tier: {tier} (if securityTier is provided)
- *
- *
- * POST
- * {@code {routerUrl}/v1/chat/secure_completion} with encrypted payload as body.
- *
- * Retry Logic
- *
- * - Retryable status codes: {@code {429, 500, 502, 503, 504}}
- * - Backoff: {@code 2^(attempt-1)} seconds (1s, 2s, 4s…)
- * - Total attempts: {@code maxRetries + 1}
- * - Network errors also retry
- * - Non-retryable exceptions propagate immediately
- *
- *
- * Status → Exception Mapping
- *
- * | Status | Result |
- * | 200 | Return decrypted response map |
- * | 400 | {@code InvalidRequestError} |
- * | 401 | {@code AuthenticationError} |
- * | 403 | {@code ForbiddenError} |
- * | 404 | {@code APIError} |
- * | 429 | {@code RateLimitError} |
- * | 500 | {@code ServerError} |
- * | 503 | {@code ServiceUnavailableError} |
- * | 502/504 | {@code APIError} (retryable) |
- * | other | {@code APIError} (non-retryable) |
- * | network error | {@code APIConnectionError} |
- *
- *
- * @param payload the payload to send (OpenAI-compatible chat parameters)
+ * @param payload OpenAI-compatible chat parameters
* @param payloadId unique payload identifier
- * @param apiKey optional API key for authentication
- * @param securityTier optional security tier ({@code "standard"}, {@code "high"}, or {@code "maximum"})
- * @return the decrypted response map
- * @throws SecurityError if encryption/decryption fails
- * @throws APIConnectionError if a network error occurs
- * @throws InvalidRequestError if the API returns 400
- * @throws AuthenticationError if the API returns 401
- * @throws ForbiddenError if the API returns 403
- * @throws RateLimitError if the API returns 429
- * @throws ServerError if the API returns 500
- * @throws ServiceUnavailableError if the API returns 503
- * @throws APIError for other non-retryable errors
+ * @param apiKey optional API key
+ * @param securityTier optional: "standard", "high", "maximum"
+ * @return decrypted response map
+ * @throws SecurityError encryption/decryption failure
+ * @throws APIConnectionError network error
+ * @throws InvalidRequestError HTTP 400
+ * @throws AuthenticationError HTTP 401
+ * @throws ForbiddenError HTTP 403
+ * @throws RateLimitError HTTP 429
+ * @throws ServerError HTTP 500
+ * @throws ServiceUnavailableError HTTP 503
+ * @throws APIError other errors
*/
- public CompletableFuture> sendSecureRequest(
- Map payload,
- String payloadId,
- String apiKey,
- String securityTier
- ) {
+ public CompletableFuture> sendSecureRequest(Map payload, String payloadId, String apiKey, String securityTier) {
throw new UnsupportedOperationException("Not yet implemented");
}
/**
- * Sends a secure request without a security tier.
- *
- * @param payload the payload to send
- * @param payloadId unique payload identifier
- * @param apiKey optional API key for authentication
- * @return the decrypted response map
+ * Without security tier.
*/
- public CompletableFuture> sendSecureRequest(
- Map payload,
- String payloadId,
- String apiKey
- ) {
+ public CompletableFuture> sendSecureRequest(Map payload, String payloadId, String apiKey) {
return sendSecureRequest(payload, payloadId, apiKey, null);
}
/**
- * Sends a secure request with no API key or security tier.
- *
- * @param payload the payload to send
- * @param payloadId unique payload identifier
- * @return the decrypted response map
+ * No API key or security tier.
*/
- public CompletableFuture> sendSecureRequest(
- Map payload,
- String payloadId
- ) {
+ public CompletableFuture> sendSecureRequest(Map payload, String payloadId) {
return sendSecureRequest(payload, payloadId, null, null);
}
- // ── Key Initialization ──────────────────────────────────────────
-
/**
- * Ensures RSA keys are loaded or generated.
+ * Thread-safe key init via double-checked locking. Loads from disk if {@code keyDir} set, else generates.
*
- * Uses double-checked locking via {@link ReentrantLock} to ensure
- * thread-safe initialization. If {@code keyDir} is set, attempts to
- * load keys from disk first; if that fails, generates new keys.
- *
- * @param keyDir directory to persist keys, or {@code null} for ephemeral
+ * @param keyDir key directory or {@code null} for ephemeral
*/
public void ensureKeys(String keyDir) {
if (keysInitialized) return;
keyInitLock.lock();
try {
if (keysInitialized) return;
- // TODO: implement key loading/generation
+ if (keyDir == null || keyDir.isEmpty()) {
+ generateKeys(false);
+ } else {
+ generateKeys(true);
+ }
keysInitialized = true;
} finally {
keyInitLock.unlock();
}
}
- // ── Key Validation ──────────────────────────────────────────────
-
/**
- * Validates that an RSA key meets the minimum size requirement.
- *
- * @param key the RSA key to validate
- * @throws SecurityError if the key size is less than {@link Constants#MIN_RSA_KEY_SIZE} bits
+ * Validates RSA key size >= {@link Constants#MIN_RSA_KEY_SIZE} bits.
*/
public void validateRsaKey(PrivateKey key) throws SecurityError {
if (key == null) {
@@ -466,77 +332,59 @@ public class SecureCompletionClient {
int keySize = extractKeySize(key);
if (keySize < Constants.MIN_RSA_KEY_SIZE) {
- throw new SecurityError(
- "RSA key size " + keySize + " bits is below minimum " + Constants.MIN_RSA_KEY_SIZE + " bits"
- );
+ throw new SecurityError("RSA key size " + keySize + " bits is below minimum " + Constants.MIN_RSA_KEY_SIZE + " bits");
}
}
private int extractKeySize(PrivateKey key) {
try {
- java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
- java.security.spec.RSAPrivateCrtKeySpec crtSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateCrtKeySpec.class);
- return crtSpec.getModulus().bitLength();
- } catch (Exception ignored) {
- // Try RSAPrivateKeySpec
+ var kf = KeyFactory.getInstance("RSA");
try {
- java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
- java.security.spec.RSAPrivateKeySpec privSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateKeySpec.class);
+ var crtSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateCrtKeySpec.class);
+ return crtSpec.getModulus().bitLength();
+ } catch (Exception ignored) {
+ var privSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateKeySpec.class);
return privSpec.getModulus().bitLength();
- } catch (Exception ignored2) {
- // Fall back to encoded length
- if (key.getEncoded() != null) {
- return key.getEncoded().length * 8;
- }
+ }
+ } catch (Exception ignored) {
+ if (key.getEncoded() != null) {
+ return key.getEncoded().length * 8;
}
}
return 0;
}
- // ── HTTP Status → Exception Mapping ─────────────────────────────
-
/**
- * Maps an HTTP status code to the appropriate exception.
+ * Maps HTTP status code to exception (200→null).
*/
public Exception mapHttpStatus(int statusCode, String responseBody) {
- switch (statusCode) {
- case 200:
- return null;
- case 400:
- return new InvalidRequestError("Invalid request: " + (responseBody != null ? responseBody : "no body"));
- case 401:
- return new AuthenticationError("Authentication failed: " + (responseBody != null ? responseBody : "no body"));
- case 403:
- return new ForbiddenError("Access forbidden: " + (responseBody != null ? responseBody : "no body"));
- case 404:
- return new APIError("Not found: " + (responseBody != null ? responseBody : "no body"));
- case 429:
- return new RateLimitError("Rate limit exceeded: " + (responseBody != null ? responseBody : "no body"));
- case 500:
- return new ServerError("Internal server error: " + (responseBody != null ? responseBody : "no body"));
- case 503:
- return new ServiceUnavailableError("Service unavailable: " + (responseBody != null ? responseBody : "no body"));
- case 502:
- case 504:
- return new APIError("Gateway error: " + (responseBody != null ? responseBody : "no body"));
- default:
- return new APIError("Unexpected status " + statusCode + ": " + (responseBody != null ? responseBody : "no body"));
- }
+ return switch (statusCode) {
+ case 200 -> null;
+ case 400 ->
+ new InvalidRequestError("Invalid request: " + (responseBody != null ? responseBody : "no body"));
+ case 401 ->
+ new AuthenticationError("Authentication failed: " + (responseBody != null ? responseBody : "no body"));
+ case 403 -> new ForbiddenError("Access forbidden: " + (responseBody != null ? responseBody : "no body"));
+ case 404 -> new APIError("Not found: " + (responseBody != null ? responseBody : "no body"));
+ case 429 -> new RateLimitError("Rate limit exceeded: " + (responseBody != null ? responseBody : "no body"));
+ case 500 -> new ServerError("Internal server error: " + (responseBody != null ? responseBody : "no body"));
+ case 503 ->
+ new ServiceUnavailableError("Service unavailable: " + (responseBody != null ? responseBody : "no body"));
+ case 502, 504 -> new APIError("Gateway error: " + (responseBody != null ? responseBody : "no body"));
+ default ->
+ new APIError("Unexpected status " + statusCode + ": " + (responseBody != null ? responseBody : "no body"));
+ };
}
- // ── URL Encoding ────────────────────────────────────────────────
-
/**
- * URL-encodes a public key PEM string for use in the {@code X-Public-Key} header.
+ * URL-encodes PEM key for {@code X-Public-Key} header.
*/
public String urlEncodePublicKey(String pemKey) {
return java.net.URLEncoder.encode(pemKey, StandardCharsets.UTF_8);
}
- // ── Getters ─────────────────────────────────────────────────────
-
/**
- * Closes the client and releases any resources.
+ * Delegates to resource cleanup (stub).
*/
public void close() {
throw new UnsupportedOperationException("Not yet implemented");
diff --git a/src/main/java/ai/nomyo/SecureMemory.java b/src/main/java/ai/nomyo/SecureMemory.java
index e16d7fc..ee94d3e 100644
--- a/src/main/java/ai/nomyo/SecureMemory.java
+++ b/src/main/java/ai/nomyo/SecureMemory.java
@@ -8,43 +8,22 @@ import java.lang.foreign.MemorySegment;
import java.util.Map;
/**
- * Cross-platform memory locking and secure zeroing utilities.
- *
- * This module provides optional memory protection for sensitive
- * cryptographic buffers. It fails gracefully if memory locking is
- * unavailable on the current platform (e.g., Windows on some JVM
- * configurations).
- *
- * Protection Levels
- *
- * - "full" — Memory locking and secure zeroing both available
- * - "zeroing_only" — Only secure zeroing available
- * - "none" — No memory protection available
- *
- *
- * Usage
- * {@code
- * try (SecureBuffer buf = SecureMemory.secureBytearray(sensitiveData)) {
- * // buf is locked and ready to use
- * process(buf.getData());
- * }
- * // buf is automatically zeroed and unlocked on exit
- * }
+ * Cross-platform memory locking and secure zeroing for sensitive cryptographic buffers. Fails gracefully if unavailable.
*/
public final class SecureMemory {
- @Getter
- @Setter
- private static volatile boolean secureMemoryEnabled = true;
-
@Getter
private static final boolean HAS_MEMORY_LOCKING;
@Getter
private static final boolean HAS_SECURE_ZEROING;
+ @Getter
+ @Setter
+ private static volatile boolean secureMemoryEnabled = true;
static {
boolean locking = false;
boolean zeroing = false;
+
try {
locking = initMemoryLocking();
zeroing = true; // Secure zeroing is always available at the JVM level
@@ -58,16 +37,59 @@ public final class SecureMemory {
private static boolean initMemoryLocking() {
// FFM doesn't support memory locking at this point in time
- // TODO: Bypass this with native libraries
return false;
}
/**
- * Wraps a byte array with memory locking and guaranteed zeroing on exit.
- *
- * Implements {@link AutoCloseable} for use with try-with-resources.
- * The buffer is automatically zeroed and unlocked when closed, even
- * if an exception occurs.
+ * Recommended way to handle sensitive data — use within try-with-resources for secure zeroing.
+ */
+ public static SecureBuffer secureByteArray(byte[] data, boolean lock) {
+ return new SecureBuffer(data, lock);
+ }
+
+ /**
+ * Always attempts locking.
+ */
+ public static SecureBuffer secureByteArray(byte[] data) {
+ return secureByteArray(data, true);
+ }
+
+ /**
+ * @deprecated Use {@link #secureByteArray(byte[])} instead.
+ */
+ @Deprecated
+ public static SecureBuffer secureBytes(byte[] data, boolean lock) {
+ return new SecureBuffer(data, lock);
+ }
+
+ /**
+ * @deprecated Use {@link #secureByteArray(byte[])} instead.
+ */
+ @Deprecated
+ public static SecureBuffer secureBytes(byte[] data) {
+ return secureBytes(data, true);
+ }
+
+ /**
+ * Returns protection capabilities: enabled, protection_level, has_memory_locking, has_secure_zeroing, supports_full_protection, page_size.
+ */
+ public static Map getMemoryProtectionInfo() {
+ String protectionLevel;
+ if (HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING) {
+ protectionLevel = "full";
+ } else if (HAS_SECURE_ZEROING) {
+ protectionLevel = "zeroing_only";
+ } else {
+ protectionLevel = "none";
+ }
+
+ boolean supportsFull = HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING && secureMemoryEnabled;
+
+ return Map.of("enabled", secureMemoryEnabled, "protection_level", protectionLevel, "has_memory_locking", HAS_MEMORY_LOCKING, "has_secure_zeroing", HAS_SECURE_ZEROING, "supports_full_protection", supportsFull, "page_size", Constants.PAGE_SIZE);
+ }
+
+ /**
+ * Wraps bytes with memory locking and guaranteed zeroing on close. AutoCloseable for try-with-resources.
*/
public static class SecureBuffer implements AutoCloseable {
@@ -85,10 +107,8 @@ public final class SecureMemory {
private boolean closed;
/**
- * Creates a new SecureBuffer wrapping the given data.
- *
- * @param data the byte array to wrap
- * @param lock whether to attempt memory locking
+ * @param data byte array to wrap
+ * @param lock whether to attempt memory locking
*/
public SecureBuffer(byte[] data, boolean lock) {
this.arena = Arena.ofConfined();
@@ -110,19 +130,14 @@ public final class SecureMemory {
}
/**
- * Attempts to lock the buffer in memory, preventing swapping to disk.
- *
- * @return {@code true} if locking succeeded, {@code false} otherwise
+ * Locks buffer in memory (prevents disk swapping). Returns false if unavailable.
*/
public boolean lock() {
- //data = data.asReadOnly();
return false;
}
/**
- * Unlocks the buffer, allowing it to be swapped to disk.
- *
- * @return {@code true} if unlocking succeeded, {@code false} otherwise
+ * Unlocks buffer (allows disk swapping).
*/
public boolean unlock() {
if (!locked) return false;
@@ -131,7 +146,7 @@ public final class SecureMemory {
}
/**
- * Securely zeros the buffer contents.
+ * Securely zeros buffer contents.
*/
public void zero() {
if (data != null) {
@@ -150,88 +165,4 @@ public final class SecureMemory {
closed = true;
}
}
-
- /**
- * Creates a SecureBuffer for the given data with memory locking.
- *
- * This is the recommended way to handle sensitive data. The returned
- * buffer should be used within a try-with-resources block to ensure
- * secure zeroing on exit.
- *
- * @param data the sensitive data bytes
- * @param lock whether to attempt memory locking
- * @return a new SecureBuffer
- */
- public static SecureBuffer secureByteArray(byte[] data, boolean lock) {
- return new SecureBuffer(data, lock);
- }
-
- /**
- * Creates a SecureBuffer for the given data with memory locking.
- * Convenience variant that always attempts locking.
- *
- * @param data the sensitive data bytes
- * @return a new SecureBuffer
- */
- public static SecureBuffer secureByteArray(byte[] data) {
- return secureByteArray(data, true);
- }
-
- /**
- * Creates a SecureBuffer for the given data with memory locking.
- *
- * Deprecated: Use {@link #secureByteArray(byte[])} instead.
- * This method exists for compatibility with the Python reference.
- *
- * @param data the sensitive data bytes
- * @param lock whether to attempt memory locking
- * @return a new SecureBuffer
- * @deprecated Use {@link #secureByteArray(byte[])} instead
- */
- @Deprecated
- public static SecureBuffer secureBytes(byte[] data, boolean lock) {
- return new SecureBuffer(data, lock);
- }
-
- /**
- * Creates a SecureBuffer for the given data with memory locking.
- *
- * Deprecated: Use {@link #secureByteArray(byte[])} instead.
- * This method exists for compatibility with the Python reference.
- *
- * @param data the sensitive data bytes
- * @return a new SecureBuffer
- * @deprecated Use {@link #secureByteArray(byte[])} instead
- */
- @Deprecated
- public static SecureBuffer secureBytes(byte[] data) {
- return secureBytes(data, true);
- }
-
- /**
- * Returns information about the current memory protection capabilities.
- *
- * @return a map of protection capabilities
- */
- public static Map getMemoryProtectionInfo() {
- String protectionLevel;
- if (HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING) {
- protectionLevel = "full";
- } else if (HAS_SECURE_ZEROING) {
- protectionLevel = "zeroing_only";
- } else {
- protectionLevel = "none";
- }
-
- boolean supportsFull = HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING && secureMemoryEnabled;
-
- return Map.of(
- "enabled", secureMemoryEnabled,
- "protection_level", protectionLevel,
- "has_memory_locking", HAS_MEMORY_LOCKING,
- "has_secure_zeroing", HAS_SECURE_ZEROING,
- "supports_full_protection", supportsFull,
- "page_size", Constants.PAGE_SIZE
- );
- }
}
diff --git a/src/main/java/ai/nomyo/errors/APIConnectionError.java b/src/main/java/ai/nomyo/errors/APIConnectionError.java
index f1a6d02..bf91da2 100644
--- a/src/main/java/ai/nomyo/errors/APIConnectionError.java
+++ b/src/main/java/ai/nomyo/errors/APIConnectionError.java
@@ -1,40 +1,16 @@
package ai.nomyo.errors;
-/**
- * Exception thrown when a network failure occurs during communication
- * with the NOMYO API server.
- *
- * This includes connection timeouts, DNS resolution failures,
- * TLS handshake failures, and other network-level errors. Unlike
- * {@link APIError}, this exception does not carry an HTTP status code.
- */
+/** Network-level errors: timeouts, DNS failures, TLS handshake failures. No HTTP status code. */
public class APIConnectionError extends Exception {
- /**
- * Constructs an {@code APIConnectionError} with the specified detail message.
- *
- * @param message the detail message
- */
public APIConnectionError(String message) {
super(message);
}
- /**
- * Constructs an {@code APIConnectionError} with the specified detail message
- * and cause.
- *
- * @param message the detail message
- * @param cause the cause of this exception, or {@code null}
- */
public APIConnectionError(String message, Throwable cause) {
super(message, cause);
}
- /**
- * Constructs an {@code APIConnectionError} with the specified cause.
- *
- * @param cause the cause of this exception
- */
public APIConnectionError(Throwable cause) {
super(cause);
}
diff --git a/src/main/java/ai/nomyo/errors/APIError.java b/src/main/java/ai/nomyo/errors/APIError.java
index df4dc36..801bd9d 100644
--- a/src/main/java/ai/nomyo/errors/APIError.java
+++ b/src/main/java/ai/nomyo/errors/APIError.java
@@ -1,26 +1,23 @@
package ai.nomyo.errors;
+import lombok.Getter;
+
import java.util.Collections;
import java.util.Map;
-/**
- * Base exception for all NOMYO API errors.
- *
- * All API error subclasses carry a {@code status_code} and optional
- * {@code error_details} from the server response.
- */
+/** Base exception for all NOMYO API errors. Carries HTTP status code and optional error details. */
+@Getter
public class APIError extends Exception {
+ /** HTTP status code ({@code null} if not applicable). */
private final Integer statusCode;
+ /** Unmodifiable map of error details from server (never {@code null}). */
private final Map errorDetails;
/**
- * Constructs an {@code APIError} with the specified detail message,
- * status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code, or {@code null} if not applicable
- * @param errorDetails additional error details from the server, or {@code null}
+ * @param message detail message
+ * @param statusCode HTTP status code ({@code null} if not applicable)
+ * @param errorDetails error details from server ({@code null} for empty)
*/
public APIError(String message, Integer statusCode, Map errorDetails) {
super(message);
@@ -30,30 +27,9 @@ public class APIError extends Exception {
: Collections.emptyMap();
}
- /**
- * Constructs an {@code APIError} with the specified detail message.
- *
- * @param message the detail message
- */
+ /** Convenience: no status code or details. */
public APIError(String message) {
this(message, null, null);
}
- /**
- * Returns the HTTP status code associated with this error, or {@code null}.
- *
- * @return the status code, or {@code null}
- */
- public Integer getStatusCode() {
- return statusCode;
- }
-
- /**
- * Returns an unmodifiable map of additional error details from the server.
- *
- * @return the error details map (never {@code null})
- */
- public Map getErrorDetails() {
- return errorDetails;
- }
}
diff --git a/src/main/java/ai/nomyo/errors/AuthenticationError.java b/src/main/java/ai/nomyo/errors/AuthenticationError.java
index a219789..7ae3f16 100644
--- a/src/main/java/ai/nomyo/errors/AuthenticationError.java
+++ b/src/main/java/ai/nomyo/errors/AuthenticationError.java
@@ -2,39 +2,17 @@ package ai.nomyo.errors;
import java.util.Map;
-/**
- * Exception thrown when the API returns a 401 (Unauthorized) status.
- */
+/** HTTP 401 (Unauthorized). */
public class AuthenticationError extends APIError {
- /**
- * Constructs an {@code AuthenticationError} with the specified detail message,
- * status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code (must be 401)
- * @param errorDetails additional error details from the server, or {@code null}
- */
public AuthenticationError(String message, Integer statusCode, Map errorDetails) {
super(message, statusCode, errorDetails);
}
- /**
- * Constructs an {@code AuthenticationError} with the specified detail message.
- *
- * @param message the detail message
- */
public AuthenticationError(String message) {
super(message, 401, null);
}
- /**
- * Constructs an {@code AuthenticationError} with the specified detail message
- * and error details.
- *
- * @param message the detail message
- * @param errorDetails additional error details from the server, or {@code null}
- */
public AuthenticationError(String message, Map errorDetails) {
super(message, 401, errorDetails);
}
diff --git a/src/main/java/ai/nomyo/errors/ForbiddenError.java b/src/main/java/ai/nomyo/errors/ForbiddenError.java
index 7d9125b..f677533 100644
--- a/src/main/java/ai/nomyo/errors/ForbiddenError.java
+++ b/src/main/java/ai/nomyo/errors/ForbiddenError.java
@@ -2,39 +2,17 @@ package ai.nomyo.errors;
import java.util.Map;
-/**
- * Exception thrown when the API returns a 403 (Forbidden) status.
- */
+/** HTTP 403 (Forbidden). */
public class ForbiddenError extends APIError {
- /**
- * Constructs a {@code ForbiddenError} with the specified detail message,
- * status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code (must be 403)
- * @param errorDetails additional error details from the server, or {@code null}
- */
public ForbiddenError(String message, Integer statusCode, Map errorDetails) {
super(message, statusCode, errorDetails);
}
- /**
- * Constructs a {@code ForbiddenError} with the specified detail message.
- *
- * @param message the detail message
- */
public ForbiddenError(String message) {
super(message, 403, null);
}
- /**
- * Constructs a {@code ForbiddenError} with the specified detail message
- * and error details.
- *
- * @param message the detail message
- * @param errorDetails additional error details from the server, or {@code null}
- */
public ForbiddenError(String message, Map errorDetails) {
super(message, 403, errorDetails);
}
diff --git a/src/main/java/ai/nomyo/errors/InvalidRequestError.java b/src/main/java/ai/nomyo/errors/InvalidRequestError.java
index 357b2b8..36a9be9 100644
--- a/src/main/java/ai/nomyo/errors/InvalidRequestError.java
+++ b/src/main/java/ai/nomyo/errors/InvalidRequestError.java
@@ -2,39 +2,17 @@ package ai.nomyo.errors;
import java.util.Map;
-/**
- * Exception thrown when the API returns a 400 (Bad Request) status.
- */
+/** HTTP 400 (Bad Request). */
public class InvalidRequestError extends APIError {
- /**
- * Constructs an {@code InvalidRequestError} with the specified detail message,
- * status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code (must be 400)
- * @param errorDetails additional error details from the server, or {@code null}
- */
public InvalidRequestError(String message, Integer statusCode, Map errorDetails) {
super(message, statusCode, errorDetails);
}
- /**
- * Constructs an {@code InvalidRequestError} with the specified detail message.
- *
- * @param message the detail message
- */
public InvalidRequestError(String message) {
super(message, 400, null);
}
- /**
- * Constructs an {@code InvalidRequestError} with the specified detail message
- * and error details.
- *
- * @param message the detail message
- * @param errorDetails additional error details from the server, or {@code null}
- */
public InvalidRequestError(String message, Map errorDetails) {
super(message, 400, errorDetails);
}
diff --git a/src/main/java/ai/nomyo/errors/RateLimitError.java b/src/main/java/ai/nomyo/errors/RateLimitError.java
index b56da8d..d108729 100644
--- a/src/main/java/ai/nomyo/errors/RateLimitError.java
+++ b/src/main/java/ai/nomyo/errors/RateLimitError.java
@@ -2,39 +2,17 @@ package ai.nomyo.errors;
import java.util.Map;
-/**
- * Exception thrown when the API returns a 429 (Too Many Requests) status.
- */
+/** HTTP 429 (Too Many Requests). */
public class RateLimitError extends APIError {
- /**
- * Constructs a {@code RateLimitError} with the specified detail message,
- * status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code (must be 429)
- * @param errorDetails additional error details from the server, or {@code null}
- */
public RateLimitError(String message, Integer statusCode, Map errorDetails) {
super(message, statusCode, errorDetails);
}
- /**
- * Constructs a {@code RateLimitError} with the specified detail message.
- *
- * @param message the detail message
- */
public RateLimitError(String message) {
super(message, 429, null);
}
- /**
- * Constructs a {@code RateLimitError} with the specified detail message
- * and error details.
- *
- * @param message the detail message
- * @param errorDetails additional error details from the server, or {@code null}
- */
public RateLimitError(String message, Map errorDetails) {
super(message, 429, errorDetails);
}
diff --git a/src/main/java/ai/nomyo/errors/SecurityError.java b/src/main/java/ai/nomyo/errors/SecurityError.java
index 8dd550e..64494a7 100644
--- a/src/main/java/ai/nomyo/errors/SecurityError.java
+++ b/src/main/java/ai/nomyo/errors/SecurityError.java
@@ -1,33 +1,12 @@
package ai.nomyo.errors;
-/**
- * Exception thrown for cryptographic and key-related failures.
- *
- * Unlike {@link APIError}, this exception carries no HTTP status code.
- * It is used for issues such as invalid key sizes, encryption/decryption
- * failures, and missing keys.
- *
- * Any decryption failure (except JSON parse errors) raises
- * {@code SecurityError("Decryption failed: integrity check or authentication failed")}.
- */
+/** Cryptographic and key-related failures. No HTTP status code. */
public class SecurityError extends Exception {
- /**
- * Constructs a {@code SecurityError} with the specified detail message.
- *
- * @param message the detail message
- */
public SecurityError(String message) {
super(message);
}
- /**
- * Constructs a {@code SecurityError} with the specified detail message
- * and cause.
- *
- * @param message the detail message
- * @param cause the underlying cause, or {@code null}
- */
public SecurityError(String message, Throwable cause) {
super(message, cause);
}
diff --git a/src/main/java/ai/nomyo/errors/ServerError.java b/src/main/java/ai/nomyo/errors/ServerError.java
index 55415c1..b94f659 100644
--- a/src/main/java/ai/nomyo/errors/ServerError.java
+++ b/src/main/java/ai/nomyo/errors/ServerError.java
@@ -2,39 +2,17 @@ package ai.nomyo.errors;
import java.util.Map;
-/**
- * Exception thrown when the API returns a 500 (Internal Server Error) status.
- */
+/** HTTP 500 (Internal Server Error). */
public class ServerError extends APIError {
- /**
- * Constructs a {@code ServerError} with the specified detail message,
- * status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code (must be 500)
- * @param errorDetails additional error details from the server, or {@code null}
- */
public ServerError(String message, Integer statusCode, Map errorDetails) {
super(message, statusCode, errorDetails);
}
- /**
- * Constructs a {@code ServerError} with the specified detail message.
- *
- * @param message the detail message
- */
public ServerError(String message) {
super(message, 500, null);
}
- /**
- * Constructs a {@code ServerError} with the specified detail message
- * and error details.
- *
- * @param message the detail message
- * @param errorDetails additional error details from the server, or {@code null}
- */
public ServerError(String message, Map errorDetails) {
super(message, 500, errorDetails);
}
diff --git a/src/main/java/ai/nomyo/errors/ServiceUnavailableError.java b/src/main/java/ai/nomyo/errors/ServiceUnavailableError.java
index 4784fdd..c83d614 100644
--- a/src/main/java/ai/nomyo/errors/ServiceUnavailableError.java
+++ b/src/main/java/ai/nomyo/errors/ServiceUnavailableError.java
@@ -2,39 +2,17 @@ package ai.nomyo.errors;
import java.util.Map;
-/**
- * Exception thrown when the API returns a 503 (Service Unavailable) status.
- */
+/** HTTP 503 (Service Unavailable). */
public class ServiceUnavailableError extends APIError {
- /**
- * Constructs a {@code ServiceUnavailableError} with the specified detail
- * message, status code, and error details.
- *
- * @param message the detail message
- * @param statusCode the HTTP status code (must be 503)
- * @param errorDetails additional error details from the server, or {@code null}
- */
public ServiceUnavailableError(String message, Integer statusCode, Map errorDetails) {
super(message, statusCode, errorDetails);
}
- /**
- * Constructs a {@code ServiceUnavailableError} with the specified detail message.
- *
- * @param message the detail message
- */
public ServiceUnavailableError(String message) {
super(message, 503, null);
}
- /**
- * Constructs a {@code ServiceUnavailableError} with the specified detail message
- * and error details.
- *
- * @param message the detail message
- * @param errorDetails additional error details from the server, or {@code null}
- */
public ServiceUnavailableError(String message, Map errorDetails) {
super(message, 503, errorDetails);
}
diff --git a/src/main/java/ai/nomyo/util/PEMConverter.java b/src/main/java/ai/nomyo/util/PEMConverter.java
index fd9a3c3..f21bceb 100644
--- a/src/main/java/ai/nomyo/util/PEMConverter.java
+++ b/src/main/java/ai/nomyo/util/PEMConverter.java
@@ -3,11 +3,13 @@ package ai.nomyo.util;
import java.util.Base64;
/**
- * @author NieGestorben
- * Copyright© (c) 2026, All Rights Reserved.
+ * Converts raw key bytes to PEM-encoded strings.
*/
public class PEMConverter {
+ /**
+ * Encodes {@code keyData} as PEM (private or public) with 64-char base64 lines.
+ */
public static String toPEM(byte[] keyData, boolean privateKey) {
String publicKeyContent = Base64.getEncoder().encodeToString(keyData);
StringBuilder publicKeyFormatted = new StringBuilder(privateKey ? "-----BEGIN PRIVATE KEY-----" : "-----BEGIN PUBLIC KEY-----");
diff --git a/src/main/java/ai/nomyo/util/Pass2Key.java b/src/main/java/ai/nomyo/util/Pass2Key.java
index a986e4f..c22ccd0 100644
--- a/src/main/java/ai/nomyo/util/Pass2Key.java
+++ b/src/main/java/ai/nomyo/util/Pass2Key.java
@@ -10,6 +10,7 @@ import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
@@ -17,16 +18,7 @@ import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
/**
- * Password-based encryption utility using PBKDF2 key derivation and AES-GCM encryption.
- *
- * The encrypted output format stores salt + IV + ciphertext in that order,
- * all base64-encoded. This ensures the salt and IV are persisted alongside
- * the ciphertext for decryption.
- *
- * Binary Layout
- *
- * [ 16 bytes salt ][ 12 bytes IV ][ variable bytes ciphertext ]
- *
+ * Password-based encryption: PBKDF2 key derivation + AES-GCM. Output: base64(salt[16] + IV[12] + ciphertext).
*/
public final class Pass2Key {
@@ -36,7 +28,8 @@ public final class Pass2Key {
private static final int GCM_TAG_LENGTH = 128;
private static final int ITERATION_COUNT = 65536;
- private Pass2Key() {}
+ private Pass2Key() {
+ }
/**
* Encrypts the given plaintext using the specified algorithm and password.
@@ -46,10 +39,7 @@ public final class Pass2Key {
* @param password the password used to derive the encryption key
* @return base64-encoded ciphertext including salt and IV
*/
- public static String encrypt(String algorithm, String input, String password)
- throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- BadPaddingException, IllegalBlockSizeException {
+ public static String encrypt(String algorithm, String input, String password) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] salt = generateRandomBytes(SALT_LENGTH);
SecretKey key = deriveKey(password, salt);
@@ -76,10 +66,7 @@ public final class Pass2Key {
* @param password the password used to derive the decryption key
* @return the decrypted plaintext
*/
- public static String decrypt(String algorithm, String cipherText, String password)
- throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- BadPaddingException, IllegalBlockSizeException {
+ public static String decrypt(String algorithm, String cipherText, String password) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] decoded = Base64.getDecoder().decode(cipherText);
@@ -99,8 +86,6 @@ public final class Pass2Key {
}
}
- // ── Key Derivation ────────────────────────────────────────────────
-
private static SecretKey deriveKey(String password, byte[] salt) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
@@ -111,50 +96,32 @@ public final class Pass2Key {
}
}
- // ── Cipher Operations ─────────────────────────────────────────────
-
- private static byte[] encryptWithCipher(String algorithm, SecretKey key,
- GCMParameterSpec spec, String input)
- throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- BadPaddingException, IllegalBlockSizeException {
+ private static byte[] encryptWithCipher(String algorithm, SecretKey key, GCMParameterSpec spec, String input) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
- return cipher.doFinal(input.getBytes());
+ return cipher.doFinal(input.getBytes(StandardCharsets.UTF_8));
}
- private static byte[] encryptWithCipher(String algorithm, SecretKey key, String input)
- throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- BadPaddingException, IllegalBlockSizeException {
+ private static byte[] encryptWithCipher(String algorithm, SecretKey key, String input) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key);
- return cipher.doFinal(input.getBytes());
+ return cipher.doFinal(input.getBytes(StandardCharsets.UTF_8));
}
- private static String decryptWithCipher(String algorithm, SecretKey key,
- GCMParameterSpec spec, byte[] ciphertext)
- throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- BadPaddingException, IllegalBlockSizeException {
+ private static String decryptWithCipher(String algorithm, SecretKey key, GCMParameterSpec spec, byte[] ciphertext) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
- return new String(plaintext);
+ return new String(plaintext, StandardCharsets.UTF_8);
}
- private static String decryptWithCipher(String algorithm, SecretKey key, byte[] ciphertext)
- throws NoSuchPaddingException, NoSuchAlgorithmException,
- InvalidAlgorithmParameterException, InvalidKeyException,
- BadPaddingException, IllegalBlockSizeException {
+ private static String decryptWithCipher(String algorithm, SecretKey key, byte[] ciphertext) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] plaintext = cipher.doFinal(ciphertext);
- return new String(plaintext);
+ return new String(plaintext, StandardCharsets.UTF_8);
}
- // ── Helpers ───────────────────────────────────────────────────────
-
private static boolean isGcmMode(String algorithm) {
return algorithm.contains("GCM");
}
@@ -183,9 +150,7 @@ public final class Pass2Key {
public static PrivateKey convertStringToPrivateKey(String privateKeyString) throws Exception {
// Remove any header and footer information if present
- privateKeyString = privateKeyString.replace("-----BEGIN PRIVATE KEY-----", "")
- .replace("-----END PRIVATE KEY-----", "")
- .replaceAll("\\s+", "");
+ privateKeyString = privateKeyString.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\s+", "");
// Decode the Base64-encoded private key string
byte[] decodedKey = Base64.getDecoder().decode(privateKeyString);
diff --git a/src/main/java/ai/nomyo/util/Splitter.java b/src/main/java/ai/nomyo/util/Splitter.java
index 47f4dec..4f80b3e 100644
--- a/src/main/java/ai/nomyo/util/Splitter.java
+++ b/src/main/java/ai/nomyo/util/Splitter.java
@@ -4,34 +4,22 @@ import java.util.ArrayList;
import java.util.List;
/**
- * @author NieGestorben
- * Copyright© (c) 2026, All Rights Reserved.
+ * Splits a string into fixed-length substrings.
*/
public class Splitter {
+ /**
+ * Splits {@code toSplit} into substrings of at most {@code length} characters.
+ */
public static List fixedLengthString(int length, String toSplit) {
- List returnList = new ArrayList<>();
+ List parts = new ArrayList<>();
- int remaining = toSplit.length();
-
- while (remaining > 0) {
- int currentIndex = toSplit.length() - remaining;
-
- int endIndex = toSplit.length() - remaining + length;
-
- // If there are not enough characters left to create a new substring of the given length, create one with the remaining characters
- if (remaining < length) {
- endIndex = toSplit.length();
- }
-
- String split = toSplit.substring(currentIndex, endIndex);
-
- returnList.add(split);
-
- remaining -= length;
+ for (int i = 0; i < toSplit.length(); i += length) {
+ int endIndex = Math.min(i + length, toSplit.length());
+ parts.add(toSplit.substring(i, endIndex));
}
- return returnList;
+ return parts;
}
}
diff --git a/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java b/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java
index 44085ae..72c9406 100644
--- a/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java
+++ b/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java
@@ -5,13 +5,9 @@ import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
-import java.util.*;
-import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.*;
@@ -32,17 +28,12 @@ class SecureCompletionClientE2ETest {
assertTrue(keyDir.mkdirs(), "Key directory should be created");
}
- @AfterEach
- void tearDown() {
- // Cleanup is handled by @TempDir
- }
-
// ── Full Lifecycle E2E Tests ──────────────────────────────────────
@Test
@Order(1)
@DisplayName("E2E: Generate keys, save to disk, load in new client, validate")
- void e2e_fullLifecycle_generateSaveLoadValidate() throws Exception {
+ void e2e_fullLifecycle_generateSaveLoadValidate() {
// Step 1: Generate keys and save to disk
SecureCompletionClient generateClient = new SecureCompletionClient(BASE_URL, false, true, 2);
generateClient.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
@@ -83,7 +74,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(2)
@DisplayName("E2E: Generate plaintext keys, load, and validate")
- void e2e_plaintextKeys_generateLoadValidate() throws Exception {
+ void e2e_plaintextKeys_generateLoadValidate() {
// Generate plaintext keys (no password)
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
@@ -137,7 +128,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(4)
@DisplayName("E2E: HTTP status mapping covers all documented cases")
- void e2e_httpStatusMapping_allCases() throws Exception {
+ void e2e_httpStatusMapping_allCases() {
SecureCompletionClient client = new SecureCompletionClient();
// 200 - success (null exception)
@@ -187,7 +178,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(5)
@DisplayName("E2E: Retryable status codes match Constants.RETRYABLE_STATUS_CODES")
- void e2e_retryableStatusCodes_matchConstants() throws Exception {
+ void e2e_retryableStatusCodes_matchConstants() {
SecureCompletionClient client = new SecureCompletionClient();
for (int code : Constants.RETRYABLE_STATUS_CODES) {
@@ -200,13 +191,16 @@ class SecureCompletionClientE2ETest {
@Test
@Order(6)
@DisplayName("E2E: URL encoding of public key PEM")
- void e2e_urlEncoding_publicKey() throws Exception {
+ void e2e_urlEncoding_publicKey() {
SecureCompletionClient client = new SecureCompletionClient();
- String pemKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" +
- "IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBI" +
- "kAMBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" +
- "IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBIjAN\n-----END PUBLIC KEY-----";
+ String pemKey = """
+ -----BEGIN PUBLIC KEY-----
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBI\
+ kAMBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBIjAN
+ -----END PUBLIC KEY-----""";
String encoded = client.urlEncodePublicKey(pemKey);
@@ -266,7 +260,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(8)
@DisplayName("E2E: Client constructor parameters are correctly set")
- void e2e_clientConstructor_parametersSetCorrectly() throws Exception {
+ void e2e_clientConstructor_parametersSetCorrectly() {
SecureCompletionClient client = new SecureCompletionClient(
"https://custom.api.com",
true,
@@ -283,7 +277,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(9)
@DisplayName("E2E: Client strips trailing slashes from routerUrl")
- void e2e_clientConstructor_stripsTrailingSlashes() throws Exception {
+ void e2e_clientConstructor_stripsTrailingSlashes() {
SecureCompletionClient client = new SecureCompletionClient(
"https://api.example.com///",
false, true, 1
@@ -295,7 +289,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(10)
@DisplayName("E2E: Client uses default values when constructed with no args")
- void e2e_clientConstructor_defaultValues() throws Exception {
+ void e2e_clientConstructor_defaultValues() {
SecureCompletionClient client = new SecureCompletionClient();
assertEquals(Constants.DEFAULT_BASE_URL, client.getRouterUrl());
@@ -329,7 +323,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(12)
@DisplayName("E2E: Generate keys without saving produces in-memory keys")
- void e2e_generateKeys_noSave_producesInMemoryKeys() throws Exception {
+ void e2e_generateKeys_noSave_producesInMemoryKeys() {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
@@ -338,13 +332,13 @@ class SecureCompletionClientE2ETest {
assertNotNull(privateKey, "Private key should be in memory");
assertNotNull(publicPem, "Public PEM should be in memory");
- assertTrue(privateKey.getAlgorithm().equals("RSA"));
+ assertEquals("RSA", privateKey.getAlgorithm());
}
@Test
@Order(13)
@DisplayName("E2E: SecurityError is thrown for null key validation")
- void e2e_nullKeyValidation_throwsSecurityError() throws Exception {
+ void e2e_nullKeyValidation_throwsSecurityError() {
SecureCompletionClient client = new SecureCompletionClient();
SecurityError error = assertThrows(SecurityError.class,
@@ -357,7 +351,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(14)
@DisplayName("E2E: mapHttpStatus returns null for 200 status")
- void e2e_mapHttpStatus_200_returnsNull() throws Exception {
+ void e2e_mapHttpStatus_200_returnsNull() {
SecureCompletionClient client = new SecureCompletionClient();
Exception result = client.mapHttpStatus(200, "Success");
@@ -367,7 +361,7 @@ class SecureCompletionClientE2ETest {
@Test
@Order(15)
@DisplayName("E2E: mapHttpStatus includes response body in error message")
- void e2e_mapHttpStatus_includesResponseBody() throws Exception {
+ void e2e_mapHttpStatus_includesResponseBody() {
SecureCompletionClient client = new SecureCompletionClient();
Exception e400 = client.mapHttpStatus(400, "Invalid parameter: email");
diff --git a/src/test/java/ai/nomyo/SecureCompletionClientTest.java b/src/test/java/ai/nomyo/SecureCompletionClientTest.java
index aba8f05..aad3a7d 100644
--- a/src/test/java/ai/nomyo/SecureCompletionClientTest.java
+++ b/src/test/java/ai/nomyo/SecureCompletionClientTest.java
@@ -5,12 +5,7 @@ import ai.nomyo.util.Pass2Key;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -51,7 +46,7 @@ class SecureCompletionClientTest {
assertNotNull(privateKey, "Private key should not be null");
assertNotNull(publicPemKey, "Public PEM key should not be null");
- assertTrue(privateKey.getAlgorithm().equals("RSA"), "Key algorithm should be RSA");
+ assertEquals("RSA", privateKey.getAlgorithm(), "Key algorithm should be RSA");
assertTrue(publicPemKey.contains("BEGIN PUBLIC KEY"), "Public key should be valid PEM");
}
@@ -81,8 +76,8 @@ class SecureCompletionClientTest {
client2.generateKeys(false);
PrivateKey secondKey = client2.getPrivateKey();
- assertNotEquals(firstKey.getEncoded().length, secondKey.getEncoded().length,
- "Different keys should have different encoded lengths");
+ assertNotEquals(java.util.Arrays.hashCode(firstKey.getEncoded()), java.util.Arrays.hashCode(secondKey.getEncoded()),
+ "Different keys should have different encoded content");
}
// ── Key Generation with File Save Tests ───────────────────────────
@@ -90,7 +85,7 @@ class SecureCompletionClientTest {
@Test
@Order(4)
@DisplayName("generateKeys with saveToFile=true should create key files")
- void generateKeys_withSaveToFile_shouldCreateKeyFiles(@TempDir Path tempDir) throws Exception {
+ void generateKeys_withSaveToFile_shouldCreateKeyFiles(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
@@ -124,7 +119,7 @@ class SecureCompletionClientTest {
@Test
@Order(6)
@DisplayName("generateKeys should not overwrite existing key files")
- void generateKeys_shouldNotOverwriteExistingKeys(@TempDir Path tempDir) throws Exception {
+ void generateKeys_shouldNotOverwriteExistingKeys(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
@@ -143,7 +138,7 @@ class SecureCompletionClientTest {
@Test
@Order(7)
@DisplayName("loadKeys should load plaintext private key from file")
- void loadKeys_plaintext_shouldLoadPrivateKey(@TempDir Path tempDir) throws Exception {
+ void loadKeys_plaintext_shouldLoadPrivateKey(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
@@ -164,7 +159,7 @@ class SecureCompletionClientTest {
@Test
@Order(8)
@DisplayName("loadKeys should load encrypted private key with correct password")
- void loadKeys_encrypted_correctPassword_shouldLoadPrivateKey(@TempDir Path tempDir) throws Exception {
+ void loadKeys_encrypted_correctPassword_shouldLoadPrivateKey(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
@@ -186,7 +181,7 @@ class SecureCompletionClientTest {
@Test
@Order(9)
@DisplayName("loadKeys should handle wrong password gracefully")
- void loadKeys_encrypted_wrongPassword_shouldHandleGracefully(@TempDir Path tempDir) throws Exception {
+ void loadKeys_encrypted_wrongPassword_shouldHandleGracefully(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
@@ -220,7 +215,7 @@ class SecureCompletionClientTest {
@Test
@Order(11)
@DisplayName("validateRsaKey should accept valid 4096-bit key")
- void validateRsaKey_validKey_shouldPass() throws Exception {
+ void validateRsaKey_validKey_shouldPass() {
client.generateKeys(false);
PrivateKey key = client.getPrivateKey();
@@ -231,7 +226,7 @@ class SecureCompletionClientTest {
@Test
@Order(12)
@DisplayName("validateRsaKey should reject null key")
- void validateRsaKey_nullKey_shouldThrowSecurityError() throws Exception {
+ void validateRsaKey_nullKey_shouldThrowSecurityError() {
SecurityError error = assertThrows(SecurityError.class, () ->
client.validateRsaKey(null));
@@ -273,7 +268,7 @@ class SecureCompletionClientTest {
@Test
@Order(15)
@DisplayName("Full roundtrip: generate, save, load should produce same key")
- void roundtrip_generateSaveLoad_shouldProduceSameKey(@TempDir Path tempDir) throws Exception {
+ void roundtrip_generateSaveLoad_shouldProduceSameKey(@TempDir Path tempDir) {
File keyDir = tempDir.toFile();
// Generate and save