diff --git a/AGENTS.md b/AGENTS.md index 9f87804..5a5be98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # nomyo4J — Agent Instructions -Java port of the NOMYO Python client. Hybrid encryption (RSA-4096 + AES-256-GCM) for secure API communication. +Java 25 port of the NOMYO Python client. Hybrid encryption (RSA-4096 + AES-256-GCM) for secure API communication. ## Build & Run @@ -16,27 +16,26 @@ mvn test -Dtest=ClassName # single test class - **`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) +- **`errors/`** — 9 exception classes, all `extends Exception` (checked), all `extends APIError` - **`util/`** — `Pass2Key` (PBKDF2 + AES-GCM), `PEMConverter`, `Splitter` +- **`EncryptedRequest`** — wire format model with Gson `@SerializedName` annotations -## Critical: This is a partial/in-progress port +## Stubbed methods (check before implementing) -Many methods are stubbed with `UnsupportedOperationException`. Before implementing, check `TRANSLATION_REFERENCE.md` for the Python reference. Stubbed methods: +- `SecureMemory.lock()` — always returns `false` (FFM doesn't support locking) +- `SecureMemory.unlock()` — always returns `false` +- `SecureMemory.initMemoryLocking()` — always returns `false` -- `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` +## Dependencies -**No JSON library** (Jackson/Gson) in `pom.xml` — needed for wire format serialization. +- **Gson** (2.13.2) — JSON serialization, in `pom.xml` +- **Lombok** (1.18.44, `provided` scope) — annotation processor configured in maven-compiler-plugin +- **JUnit Jupiter** (5.12.1, `test` scope) +- **Maven Surefire** (3.5.0) ## Key files -- `TRANSLATION_REFERENCE.md` — **primary documentation**. Cross-language spec derived from Python reference. Read before implementing any method. +- `TRANSLATION_REFERENCE.md` — **primary documentation**. Cross-language spec 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. @@ -45,7 +44,12 @@ Many methods are stubbed with `UnsupportedOperationException`. Before implementi - 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" +- Error classes: checked exceptions with `statusCode` and `errorDetails` (immutable, via `Collections.unmodifiableMap`) +- Key files: `PosixFilePermissions.fromString("rw-------")` for private, `"rw-r--r--"` for public +- RSA: 4096-bit, exponent 65537, OAEP-SHA256 (MGF1 with SHA-256) +- Protocol constants in `Constants.java` — marked "never change" (downgrade detection) +- `SecureChatCompletion.acreate()` is a sync alias, not async — delegates to `create()` +- Streaming is explicitly rejected with `IllegalArgumentException` +- `SecureCompletionClient.ValueError` — inner class, maps to Python `ValueError` +- Retry: 429/500/502/503/504 + network errors, exponential backoff 2^(attempt-1)s, default 2 retries +- All HTTP endpoints use `application/octet-stream` content type for encrypted payloads diff --git a/TRANSLATION_REFERENCE.md b/TRANSLATION_REFERENCE.md deleted file mode 100644 index 213c271..0000000 --- a/TRANSLATION_REFERENCE.md +++ /dev/null @@ -1,478 +0,0 @@ -# NOMYO Python Client — Translation Reference - -> Target: Port this library to another language. Every class, method, signature, constant, wire format, and error mapping is documented below. - ---- - -## 1. Package Layout - -| File (relative to package root) | Purpose | -|---|---| -| `nomyo/__init__.py` | Public exports, version string | -| `nomyo/nomyo.py` | `SecureChatCompletion` — OpenAI-compatible entrypoint | -| `nomyo/SecureCompletionClient.py` | Key mgmt, hybrid encryption, HTTP roundtrip, retries | -| `nomyo/SecureMemory.py` | Cross-platform memory locking + secure zeroing (optional, platform-specific) | - -**Python version:** `>= 3.10` -**Build:** `hatchling` (pyproject.toml) -**Dependencies:** `anyio`, `certifi`, `cffi`, `cryptography`, `exceptiongroup`, `h11`, `httpcore`, `httpx`, `idna`, `pycparser`, `typing_extensions` - ---- - -## 2. Public API Surface (`__all__`) - -| Export | Type | Source file | -|---|---|---| -| `SecureChatCompletion` | class | `nomyo.py` | -| `SecurityError` | exception | `SecureCompletionClient.py` | -| `APIError` | exception (base) | `SecureCompletionClient.py` | -| `AuthenticationError` | exception (401) | `SecureCompletionClient.py` | -| `InvalidRequestError` | exception (400) | `SecureCompletionClient.py` | -| `APIConnectionError` | exception (network) | `SecureCompletionClient.py` | -| `ForbiddenError` | exception (403) | `SecureCompletionClient.py` | -| `RateLimitError` | exception (429) | `SecureCompletionClient.py` | -| `ServerError` | exception (500) | `SecureCompletionClient.py` | -| `ServiceUnavailableError` | exception (503) | `SecureCompletionClient.py` | -| `get_memory_protection_info` | function | `SecureMemory.py` | -| `disable_secure_memory` | function | `SecureMemory.py` | -| `enable_secure_memory` | function | `SecureMemory.py` | -| `secure_bytearray` | context manager | `SecureMemory.py` | -| `secure_bytes` | context manager (deprecated) | `SecureMemory.py` | -| `SecureBuffer` | class | `SecureMemory.py` | - ---- - -## 3. `SecureChatCompletion` (entrypoint) - -### Constructor - -```python -SecureChatCompletion( - base_url: str = "https://api.nomyo.ai", - allow_http: bool = False, - api_key: Optional[str] = None, - secure_memory: bool = True, - key_dir: Optional[str] = None, - max_retries: int = 2 -) -``` - -| Param | Default | Description | -|---|---|---| -| `base_url` | `"https://api.nomyo.ai"` | NOMYO Router base URL. HTTPS enforced unless `allow_http=True`. | -| `allow_http` | `False` | Permit `http://` URLs (dev only). | -| `api_key` | `None` | Bearer token for auth. Can also be passed per-call via `create()`. | -| `secure_memory` | `True` | Enable memory locking/zeroing. Warns if unavailable. | -| `key_dir` | `None` | Directory to persist RSA keys. `None` = ephemeral (in-memory only). | -| `max_retries` | `2` | Retries on 429/500/502/503/504 + network errors. Exponential backoff: 1s, 2s, 4s… | - -### `create(model, messages, **kwargs) -> Dict[str, Any]` - -Async method. Returns a **dict** (not an object). Same signature as `openai.ChatCompletion.create()`. - -| Param | Type | Required | Description | -|---|---|---|---| -| `model` | `str` | yes | Model identifier, e.g. `"Qwen/Qwen3-0.6B"` | -| `messages` | `List[Dict]` | yes | OpenAI-format messages: `[{"role": "user", "content": "..."}]` | -| `temperature` | `float` | no | 0–2 | -| `max_tokens` | `int` | no | | -| `top_p` | `float` | no | | -| `stop` | `str \| List[str]` | no | | -| `presence_penalty` | `float` | no | -2.0 to 2.0 | -| `frequency_penalty` | `float` | no | -2.0 to 2.0 | -| `n` | `int` | no | Number of completions | -| `best_of` | `int` | no | | -| `seed` | `int` | no | | -| `logit_bias` | `Dict[str, float]` | no | | -| `user` | `str` | no | | -| `tools` | `List[Dict]` | no | Tool definitions passed through to llama.cpp | -| `tool_choice` | `str` | no | `"auto"`, `"none"`, or specific tool name | -| `response_format` | `Dict` | no | `{"type": "json_object"}` or `{"type": "json_schema", ...}` | -| `stream` | `bool` | no | **NOT supported.** Server rejects with HTTP 400. Always use `False`. | -| `base_url` | `str` | no | Per-call override (creates temp client internally). | -| `security_tier` | `str` | no | `"standard"`, `"high"`, or `"maximum"`. Invalid values raise `ValueError`. | -| `api_key` | `str` | no | Per-call override of instance `api_key`. | - -**Return value:** `Dict[str, Any]` — OpenAI-compatible response dict (see §6.2). - -### `acreate(model, messages, **kwargs) -> Dict[str, Any]` - -Async alias for `create()`. Identical behavior. - ---- - -## 4. `SecureCompletionClient` (low-level) - -### Constructor - -```python -SecureCompletionClient( - router_url: str = "https://api.nomyo.ai", - allow_http: bool = False, - secure_memory: bool = True, - max_retries: int = 2 -) -``` - -Same semantics as `SecureChatCompletion` constructor (maps directly to inner client). - -### Instance attributes - -| Attribute | Type | Description | -|---|---|---| -| `router_url` | `str` | Base URL (trailing slash stripped). | -| `private_key` | `rsa.RSAPrivateKey \| None` | Loaded/generated RSA private key. | -| `public_key_pem` | `str \| None` | PEM-encoded public key string. | -| `key_size` | `int` | Always `4096`. | -| `allow_http` | `bool` | HTTP allowance flag. | -| `max_retries` | `int` | Retry count. | -| `_use_secure_memory` | `bool` | Whether secure memory ops are active. | - -### `generate_keys(save_to_file: bool = False, key_dir: str = "client_keys", password: Optional[str] = None) -> None` - -Generates a 4096-bit RSA key pair (public exponent `65537`). If `save_to_file=True`: -- Creates `key_dir/` (mode 755). -- Writes `private_key.pem` with mode `0o600`. -- Writes `public_key.pem` with mode `0o644`. -- If `password` is given, private key is encrypted with `BestAvailableEncryption`. - -### `load_keys(private_key_path: str, public_key_path: Optional[str] = None, password: Optional[str] = None) -> None` - -Loads an RSA private key from disk. If `public_key_path` is omitted, derives the public key from the loaded private key. Validates key size >= 2048 bits. - -### `fetch_server_public_key() -> str` (async) - -`GET {router_url}/pki/public_key` -- Returns server PEM public key as string. -- Validates it parses as a valid PEM public key. -- Raises `SecurityError` if URL is not HTTPS and `allow_http=False`. - -### `encrypt_payload(payload: Dict[str, Any]) -> bytes` (async) - -Encrypts a dict payload using hybrid encryption. Returns raw encrypted bytes (JSON package, serialized to bytes). - -**Encryption process:** -1. Serialize payload to JSON → `bytearray`. -2. Validate size <= 10 MB. -3. Generate 256-bit AES key via `secrets.token_bytes(32)` → `bytearray`. -4. If secure memory enabled: lock both payload and AES key in memory. -5. Call `_do_encrypt()` (see below). -6. Zero/destroy payload and AES key from memory on exit. - -### `_do_encrypt(payload_bytes: bytes \| bytearray, aes_key: bytes \| bytearray) -> bytes` (async) - -Core hybrid encryption routine. **This is the wire format constructor.** - -``` -1. nonce = secrets.token_bytes(12) # 96-bit GCM nonce -2. ciphertext = AES-256-GCM_encrypt(aes_key, nonce, payload_bytes) -3. tag = GCM_tag -4. server_pubkey = await fetch_server_public_key() -5. encrypted_aes_key = RSA-OAEP-SHA256_encrypt(server_pubkey, aes_key_bytes) -6. Build JSON package (see §6.1) -7. Return json.dumps(package).encode('utf-8') -``` - -### `decrypt_response(encrypted_response: bytes, payload_id: str) -> Dict[str, Any]` (async) - -Decrypts a server response. - -**Validation chain:** -1. Parse JSON. -2. Check `version == "1.0"` — raises `ValueError` if mismatch. -3. Check `algorithm == "hybrid-aes256-rsa4096"` — raises `ValueError` if mismatch. -4. Validate `encrypted_payload` has `ciphertext`, `nonce`, `tag`. -5. Require `self.private_key` is not `None`. -6. Decrypt AES key: `RSA-OAEP-SHA256_decrypt(private_key, encrypted_aes_key)`. -7. Decrypt payload: `AES-256-GCM_decrypt(aes_key, nonce, tag, ciphertext)`. -8. Parse decrypted bytes as JSON → response dict. -9. Attach `_metadata` if not present (see §6.2). - -Any decryption failure (except JSON parse errors) raises `SecurityError("Decryption failed: integrity check or authentication failed")`. - -### `send_secure_request(payload, payload_id, api_key=None, security_tier=None) -> Dict[str, Any]` (async) - -Full request lifecycle: encrypt → HTTP POST → retry → decrypt → return. - -**Request headers:** -``` -Content-Type: application/octet-stream -X-Payload-ID: {payload_id} -X-Public-Key: {url_encoded_pem_public_key} -Authorization: Bearer {api_key} (if api_key is provided) -X-Security-Tier: {tier} (if security_tier is provided) -``` - -**POST** to `{router_url}/v1/chat/secure_completion` with encrypted payload as body. - -**Retry logic:** -- Retryable status codes: `{429, 500, 502, 503, 504}`. -- Backoff: `2^(attempt-1)` seconds (1s, 2s, 4s…). -- Total attempts: `max_retries + 1`. -- Network errors also retry. -- Non-retryable exceptions propagate immediately. - -**Status → exception mapping:** - -| Status | Exception | -|---|---| -| 200 | Return decrypted response dict | -| 400 | `InvalidRequestError` | -| 401 | `AuthenticationError` | -| 403 | `ForbiddenError` | -| 404 | `APIError` | -| 429 | `RateLimitError` | -| 500 | `ServerError` | -| 503 | `ServiceUnavailableError` | -| 502/504 | `APIError` (retryable) | -| other | `APIError` (non-retryable) | -| network error | `APIConnectionError` | - ---- - -## 5. Encryption Wire Format - -The encrypted package is a JSON object sent as `application/octet-stream`: - -```json -{ - "version": "1.0", - "algorithm": "hybrid-aes256-rsa4096", - "encrypted_payload": { - "ciphertext": "", - "nonce": "", - "tag": "" - }, - "encrypted_aes_key": "", - "key_algorithm": "RSA-OAEP-SHA256", - "payload_algorithm": "AES-256-GCM" -} -``` - -| Field | Encoding | Description | -|---|---|---| -| `version` | string | Protocol version. **Never change** — used for downgrade detection. | -| `algorithm` | string | `"hybrid-aes256-rsa4096"`. **Never change** — used for downgrade detection. | -| `encrypted_payload.ciphertext` | base64 | AES-256-GCM encrypted payload. | -| `encrypted_payload.nonce` | base64 | 12-byte GCM nonce. | -| `encrypted_payload.tag` | base64 | 16-byte GCM authentication tag. | -| `encrypted_aes_key` | base64 | RSA-OAEP-SHA256 encrypted 32-byte AES key. | -| `key_algorithm` | string | `"RSA-OAEP-SHA256"` | -| `payload_algorithm` | string | `"AES-256-GCM"` | - ---- - -## 6. Data Structures - -### 6.1 Encrypted Request Payload (before encryption) - -The dict passed to `encrypt_payload()` has this structure: - -```json -{ - "model": "Qwen/Qwen3-0.6B", - "messages": [ - {"role": "user", "content": "Hello"} - ], - "temperature": 0.7, - "...": "any other OpenAI-compatible param" -} -``` - -**Important:** `api_key` is **never** included in the encrypted payload. It is sent only as the `Authorization: Bearer` HTTP header. - -### 6.2 Response Dict (after decryption) - -```json -{ - "id": "chatcmpl-123", - "object": "chat.completion", - "created": 1234567890, - "model": "Qwen/Qwen3-0.6B", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "The capital of France is Paris.", - "tool_calls": [...], - "reasoning_content": "..." - }, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 10, - "completion_tokens": 20, - "total_tokens": 30 - }, - "_metadata": { - "payload_id": "openai-compat-abc123", - "processed_at": 1765250382, - "is_encrypted": true, - "encryption_algorithm": "hybrid-aes256-rsa4096", - "security_tier": "standard", - "memory_protection": { - "platform": "linux", - "memory_locking": true, - "secure_zeroing": true, - "core_dump_prevention": true - }, - "cuda_device": { - "available": true, - "device_hash": "sha256_hex" - } - } -} -``` - -### 6.3 Security Tier Values - -| Value | Hardware | Use case | -|---|---|---| -| `"standard"` | GPU | General secure inference | -| `"high"` | CPU/GPU | Sensitive business data | -| `"maximum"` | CPU only | PHI, classified data | - -Sent as `X-Security-Tier` HTTP header. Invalid values raise `ValueError`. - ---- - -## 7. Error Class Hierarchy - -All errors are exceptions. `APIError` subclasses carry `status_code` and `error_details`. - -``` -Exception -└── APIError (base, has message/status_code/error_details) - ├── AuthenticationError (status_code=401) - ├── InvalidRequestError (status_code=400) - ├── RateLimitError (status_code=429) - ├── ForbiddenError (status_code=403) - ├── ServerError (status_code=500) - └── ServiceUnavailableError (status_code=503) - -Exception -└── SecurityError (crypto/key failure, no status_code) - -Exception -└── APIConnectionError (network failure, no status_code) -``` - -**APIError constructor:** -```python -APIError(message: str, status_code: Optional[int] = None, error_details: Optional[Dict] = None) -``` - ---- - -## 8. SecureMemory Module - -Optional, platform-specific. Fails gracefully if unavailable (e.g. Windows on some Python builds). - -### `SecureBuffer` class - -Wraps a `bytearray` with memory locking and guaranteed zeroing on exit. - -| Attribute/Method | Type | Description | -|---|---|---| -| `data` | `bytearray` | Underlying mutable buffer | -| `address` | `int` | Memory address (via ctypes) | -| `size` | `int` | Buffer size in bytes | -| `lock() -> bool` | method | Attempt memory lock | -| `unlock() -> bool` | method | Unlock memory | -| `zero()` | method | Securely zero contents | -| `__enter__` / `__exit__` | context mgr | Auto-lock on enter, auto-zero+unlock on exit | - -### `secure_bytearray(data: bytes \| bytearray, lock: bool = True) -> SecureBuffer` (context manager) - -Recommended secure handling. Converts input to `bytearray`, locks (best-effort), yields `SecureBuffer`. Always zeros on exit, even on exception. - -### `secure_bytes(data: bytes, lock: bool = True) -> SecureBuffer` (context manager, **deprecated**) - -Same as `secure_bytearray` but accepts immutable `bytes`. Emits deprecation warning. Original bytes cannot be zeroed. - -### `get_memory_protection_info() -> Dict[str, Any]` - -Returns protection capabilities: - -```json -{ - "enabled": true, - "platform": "linux", - "protection_level": "full", - "has_memory_locking": true, - "has_secure_zeroing": true, - "supports_full_protection": true, - "page_size": 4096 -} -``` - -`protection_level` values: `"full"`, `"zeroing_only"`, `"none"`. - -### `disable_secure_memory()` / `enable_secure_memory()` - -Globally disable/re-enable secure memory operations. - ---- - -## 9. Constants - -| Constant | Value | Location | Notes | -|---|---|---|---| -| Protocol version | `"1.0"` | `SecureCompletionClient.py` | **Never change** — downgrade detection | -| Algorithm string | `"hybrid-aes256-rsa4096"` | `SecureCompletionClient.py` | **Never change** — downgrade detection | -| RSA key size | `4096` | `SecureCompletionClient.py` | Fixed | -| RSA public exponent | `65537` | `SecureCompletionClient.py` | Fixed | -| AES key size | `32` bytes (256-bit) | `SecureCompletionClient.py` | Per-request ephemeral | -| GCM nonce size | `12` bytes (96-bit) | `SecureCompletionClient.py` | Per-request via `secrets.token_bytes` | -| Max payload size | `10 * 1024 * 1024` (10 MB) | `SecureCompletionClient.py` | DoS protection | -| Default max retries | `2` | Both client classes | Exponential backoff: 1s, 2s, 4s… | -| Private key file mode | `0o600` | `SecureCompletionClient.py` | Owner read/write only | -| Public key file mode | `0o644` | `SecureCompletionClient.py` | Owner rw, group/others r | -| Min RSA key size (validation) | `2048` | `SecureCompletionClient.py` | `_validate_rsa_key` | -| Valid security tiers | `["standard", "high", "maximum"]` | `SecureCompletionClient.py` | Case-sensitive | -| Retryable status codes | `{429, 500, 502, 503, 504}` | `SecureCompletionClient.py` | | -| Package version | `"0.2.7"` | `pyproject.toml` + `__init__.py` | Bump both | - ---- - -## 10. Endpoint URLs - -| Endpoint | Method | Purpose | -|---|---|---| -| `{router_url}/pki/public_key` | GET | Fetch server RSA public key | -| `{router_url}/v1/chat/secure_completion` | POST | Encrypted chat completion | - ---- - -## 11. Key Lifecycle - -1. **First `create()` call** → `_ensure_keys()` runs (async, double-checked locking via `asyncio.Lock`). -2. If `key_dir` is set: - - Try `load_keys()` from `{key_dir}/private_key.pem` + `{key_dir}/public_key.pem`. - - If that fails → `generate_keys(save_to_file=True, key_dir=key_dir)`. -3. If `key_dir` is `None` → `generate_keys()` (ephemeral, in-memory only). -4. Keys are reused across all subsequent calls until the client is discarded. - ---- - -## 12. HTTP Client Details - -- Uses `httpx.AsyncClient` with `timeout=60.0`. -- SSL verification enabled for HTTPS URLs; disabled for `http://`. -- Request body is raw bytes (not JSON) — `Content-Type: application/octet-stream`. -- Public key is URL-encoded in the `X-Public-Key` header. - ---- - -## 13. Memory Protection Platform Matrix - -| Platform | Locking | Zeroing | -|---|---|---| -| Linux | `mlock()` via `libc.so.6` | `memset()` via `libc.so.6` | -| Windows | `VirtualLock()` via `kernel32` | `RtlZeroMemory()` via `ntdll` + Python-level fallback | -| macOS | `mlock()` via `libc.dylib` | `memset()` via `libc.dylib` | -| Other | No lock | Python-level byte-by-byte zeroing | - -mlock may fail with `EPERM` (need `CAP_IPC_LOCK` or `ulimit -l` increase) — degrades to zeroing-only gracefully. diff --git a/src/main/java/ai/nomyo/Constants.java b/src/main/java/ai/nomyo/Constants.java index aacd1b4..740532f 100644 --- a/src/main/java/ai/nomyo/Constants.java +++ b/src/main/java/ai/nomyo/Constants.java @@ -1,6 +1,5 @@ package ai.nomyo; - import java.util.Set; /** @@ -26,7 +25,6 @@ public final class Constants { * AES-256-GCM payload encryption algorithm. */ public static final String PAYLOAD_ALGORITHM = "AES-256-GCM"; - // ── Cryptographic Constants ───────────────────────────────────── /** @@ -53,7 +51,6 @@ public final class Constants { * Minimum RSA key size for validation (bits). */ public static final int MIN_RSA_KEY_SIZE = 2048; - // ── Payload Limits ────────────────────────────────────────────── /** @@ -75,7 +72,6 @@ public final class Constants { * Retryable HTTP status codes. */ public static final Set RETRYABLE_STATUS_CODES = Set.of(429, 500, 502, 503, 504); - // ── File Permission Constants ─────────────────────────────────── /** @@ -86,7 +82,6 @@ public final class Constants { * Public key file permission (owner rw, group/others r). */ public static final String PUBLIC_KEY_FILE_MODE = "rw-r--r--"; - // ── Security Tier Constants ───────────────────────────────────── /** @@ -105,7 +100,6 @@ public final class Constants { * CPU only for PHI/classified data. */ public static final String SECURITY_TIER_MAXIMUM = "maximum"; - // ── Endpoint Paths ────────────────────────────────────────────── /** @@ -116,7 +110,6 @@ public final class Constants { * Secure chat completion endpoint. */ public static final String SECURE_COMPLETION_PATH = "/v1/chat/secure_completion"; - // ── HTTP Headers ──────────────────────────────────────────────── /** @@ -139,7 +132,6 @@ public final class Constants { * Bearer token prefix. */ public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer "; - // ── Default Values ────────────────────────────────────────────── /** @@ -158,7 +150,6 @@ public final class Constants { * Default public key file name. */ public static final String DEFAULT_PUBLIC_KEY_FILE = "public_key.pem"; - // ── Memory Protection Constants ───────────────────────────────── /** diff --git a/src/main/java/ai/nomyo/EncryptedRequest.java b/src/main/java/ai/nomyo/EncryptedRequest.java index b498cf0..575a0d8 100644 --- a/src/main/java/ai/nomyo/EncryptedRequest.java +++ b/src/main/java/ai/nomyo/EncryptedRequest.java @@ -15,7 +15,6 @@ public class EncryptedRequest { private static final Gson GSON = new GsonBuilder().create(); - // Getters and Setters @SerializedName("version") private String version; @@ -26,7 +25,7 @@ public class EncryptedRequest { private EncryptedPayload encryptedPayload; @SerializedName("encrypted_aes_key") - private String encryptedAESKey; // Java variable name corrected to proper spelling + private String encryptedAESKey; @SerializedName("key_algorithm") private String keyAlgorithm; @@ -41,7 +40,6 @@ public class EncryptedRequest { @Getter public static class EncryptedPayload { - // Getters and Setters @SerializedName("ciphertext") private String ciphertext; diff --git a/src/main/java/ai/nomyo/SecureChatCompletion.java b/src/main/java/ai/nomyo/SecureChatCompletion.java index 5d4e9e1..71eb57f 100644 --- a/src/main/java/ai/nomyo/SecureChatCompletion.java +++ b/src/main/java/ai/nomyo/SecureChatCompletion.java @@ -146,24 +146,11 @@ public class SecureChatCompletion { /** * Convenience variant with no additional parameters. */ + @SuppressWarnings("UnusedReturnValue") public Map create(String model, List> messages) { return create(model, messages, null); } - /** - * Async alias for {@link #create(String, List, Map)}. - */ - public Map acreate(String model, List> messages, Map kwargs) { - return create(model, messages, kwargs); - } - - /** - * Async alias for {@link #create(String, List)}. - */ - public Map acreate(String model, List> messages) { - return create(model, messages); - } - /** * Delegates to {@link SecureCompletionClient#close()}. */ diff --git a/src/main/java/ai/nomyo/SecureCompletionClient.java b/src/main/java/ai/nomyo/SecureCompletionClient.java index 69d2a4e..5bead2d 100644 --- a/src/main/java/ai/nomyo/SecureCompletionClient.java +++ b/src/main/java/ai/nomyo/SecureCompletionClient.java @@ -295,6 +295,7 @@ public class SecureCompletionClient { * @return encrypted bytes (JSON package) * @throws SecurityError if encryption fails or keys not loaded */ + @SuppressWarnings("JavadocDeclaration") public CompletableFuture encryptPayload(Map payload) { return CompletableFuture.supplyAsync(() -> { try { diff --git a/src/main/java/ai/nomyo/SecureMemory.java b/src/main/java/ai/nomyo/SecureMemory.java index 07e1121..164e011 100644 --- a/src/main/java/ai/nomyo/SecureMemory.java +++ b/src/main/java/ai/nomyo/SecureMemory.java @@ -10,6 +10,7 @@ import java.util.Map; /** * Cross-platform memory locking and secure zeroing for sensitive cryptographic buffers. Fails gracefully if unavailable. */ +@SuppressWarnings("SameReturnValue") public final class SecureMemory { @Getter @@ -75,6 +76,7 @@ public final class SecureMemory { /** * Wraps bytes with memory locking and guaranteed zeroing on close. AutoCloseable for try-with-resources. */ + @SuppressWarnings("SameReturnValue") public static class SecureBuffer implements AutoCloseable { private final Arena arena; diff --git a/src/main/java/ai/nomyo/errors/APIError.java b/src/main/java/ai/nomyo/errors/APIError.java index 801bd9d..970f1c1 100644 --- a/src/main/java/ai/nomyo/errors/APIError.java +++ b/src/main/java/ai/nomyo/errors/APIError.java @@ -31,5 +31,4 @@ public class APIError extends Exception { public APIError(String message) { this(message, null, null); } - } diff --git a/src/main/java/ai/nomyo/util/PEMConverter.java b/src/main/java/ai/nomyo/util/PEMConverter.java index 10d8a38..031b6a4 100644 --- a/src/main/java/ai/nomyo/util/PEMConverter.java +++ b/src/main/java/ai/nomyo/util/PEMConverter.java @@ -11,36 +11,31 @@ 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-----"); - publicKeyFormatted.append(System.lineSeparator()); - for (final String row : Splitter.fixedLengthString(64, publicKeyContent)) { - publicKeyFormatted.append(row); - publicKeyFormatted.append(System.lineSeparator()); + String b64 = Base64.getEncoder().encodeToString(keyData); + String begin = privateKey ? "-----BEGIN PRIVATE KEY-----" : "-----BEGIN PUBLIC KEY-----"; + String end = privateKey ? "-----END PRIVATE KEY-----" : "-----END PUBLIC KEY-----"; + StringBuilder sb = new StringBuilder(begin).append(System.lineSeparator()); + for (String row : Splitter.fixedLengthString(64, b64)) { + sb.append(row).append(System.lineSeparator()); } - - publicKeyFormatted.append(privateKey ? "-----END PRIVATE KEY-----" : "-----END PUBLIC KEY-----"); - - return publicKeyFormatted.toString(); + sb.append(end); + return sb.toString(); } public static byte[] fromPEM(String pem) { - pem = pem.replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replaceAll("\\s+", ""); - - return Base64.getDecoder().decode(pem); + String cleaned = pem.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + return Base64.getDecoder().decode(cleaned); } public static boolean validatePEM(String keyIn) { if (keyIn == null || keyIn.isBlank()) { return false; } - String trimmed = keyIn.trim(); - return trimmed.startsWith("-----BEGIN PUBLIC KEY-----") && trimmed.endsWith("-----END PUBLIC KEY-----"); } diff --git a/src/main/java/ai/nomyo/util/Pass2Key.java b/src/main/java/ai/nomyo/util/Pass2Key.java index 6e7cc7c..522ccaf 100644 --- a/src/main/java/ai/nomyo/util/Pass2Key.java +++ b/src/main/java/ai/nomyo/util/Pass2Key.java @@ -1,6 +1,7 @@ package ai.nomyo.util; import ai.nomyo.errors.SecurityError; + import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; diff --git a/src/main/java/ai/nomyo/util/Splitter.java b/src/main/java/ai/nomyo/util/Splitter.java index 4f80b3e..4ca2d01 100644 --- a/src/main/java/ai/nomyo/util/Splitter.java +++ b/src/main/java/ai/nomyo/util/Splitter.java @@ -13,12 +13,10 @@ public class Splitter { */ public static List fixedLengthString(int length, String toSplit) { List parts = new ArrayList<>(); - for (int i = 0; i < toSplit.length(); i += length) { int endIndex = Math.min(i + length, toSplit.length()); parts.add(toSplit.substring(i, endIndex)); } - return parts; } diff --git a/src/test/java/ai/nomyo/CloseTest.java b/src/test/java/ai/nomyo/CloseTest.java index 4fb77c1..6eb470c 100644 --- a/src/test/java/ai/nomyo/CloseTest.java +++ b/src/test/java/ai/nomyo/CloseTest.java @@ -58,8 +58,8 @@ class CloseTest { SecureCompletionClient client = new SecureCompletionClient(); client.generateKeys(false); - assertDoesNotThrow(() -> client.close(), "First close should not throw"); - assertDoesNotThrow(() -> client.close(), "Second close should not throw"); - assertDoesNotThrow(() -> client.close(), "Third close should not throw"); + assertDoesNotThrow(client::close, "First close should not throw"); + assertDoesNotThrow(client::close, "Second close should not throw"); + assertDoesNotThrow(client::close, "Third close should not throw"); } } diff --git a/src/test/java/ai/nomyo/DecryptResponseTest.java b/src/test/java/ai/nomyo/DecryptResponseTest.java index 4ef53cf..672df8d 100644 --- a/src/test/java/ai/nomyo/DecryptResponseTest.java +++ b/src/test/java/ai/nomyo/DecryptResponseTest.java @@ -18,6 +18,9 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.security.spec.X509EncodedKeySpec; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; +import java.security.spec.MGF1ParameterSpec; import static org.junit.jupiter.api.Assertions.*; @@ -31,7 +34,6 @@ class DecryptResponseTest { void decryptResponse_validPackage_shouldReturnDecryptedMap() throws Exception { SecureCompletionClient client = new SecureCompletionClient(); client.generateKeys(false); - PrivateKey privateKey = client.getPrivateKey(); String plaintext = "{\"content\":\"Hello, world!\",\"role\":\"assistant\"}"; byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8); @@ -57,7 +59,8 @@ class DecryptResponseTest { PublicKey serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec); Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); - rsaCipher.init(Cipher.ENCRYPT_MODE, serverPublicKey); + OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), PSource.PSpecified.DEFAULT); + rsaCipher.init(Cipher.ENCRYPT_MODE, serverPublicKey, oaepParams); byte[] encryptedAESKey = rsaCipher.doFinal(aesKey.getEncoded()); JsonObject packageJson = new JsonObject(); @@ -97,7 +100,6 @@ class DecryptResponseTest { void decryptResponse_missingProcessedAt_shouldSetNull() throws Exception { SecureCompletionClient client = new SecureCompletionClient(); client.generateKeys(false); - PrivateKey privateKey = client.getPrivateKey(); String plaintext = "{\"response\":\"ok\"}"; byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8); @@ -121,7 +123,8 @@ class DecryptResponseTest { PublicKey serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(pubKeyBytes)); Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); - rsaCipher.init(Cipher.ENCRYPT_MODE, serverPublicKey); + OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), PSource.PSpecified.DEFAULT); + rsaCipher.init(Cipher.ENCRYPT_MODE, serverPublicKey, oaepParams); byte[] encryptedAESKey = rsaCipher.doFinal(aesKey.getEncoded()); JsonObject packageJson = new JsonObject(); @@ -155,8 +158,7 @@ class DecryptResponseTest { CompletableFuture> future = client.decryptResponse(new byte[0], "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, - "Should throw ValueError for empty response"); + assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for empty response"); assertTrue(error.getCause().getMessage().contains("Empty"), "Error message should mention empty"); } @@ -171,8 +173,7 @@ class DecryptResponseTest { CompletableFuture> future = client.decryptResponse(null, "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, - "Should throw ValueError for null response"); + assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for null response"); } @Test @@ -186,8 +187,7 @@ class DecryptResponseTest { CompletableFuture> future = client.decryptResponse(invalidJson, "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, - "Should throw ValueError for malformed JSON"); + assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for malformed JSON"); assertTrue(error.getCause().getMessage().contains("malformed JSON") || error.getCause().getMessage().contains("JSON"), "Error message should mention JSON"); } @@ -207,8 +207,7 @@ class DecryptResponseTest { CompletableFuture> future = client.decryptResponse(encryptedResponse, "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, - "Should throw ValueError for missing fields"); + assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for missing fields"); assertTrue(error.getCause().getMessage().contains("Missing required fields"), "Error message should mention missing fields"); } @@ -220,22 +219,11 @@ class DecryptResponseTest { SecureCompletionClient client = new SecureCompletionClient(); client.generateKeys(false); - JsonObject packageJson = new JsonObject(); - packageJson.addProperty("version", "9.9"); - packageJson.addProperty("algorithm", Constants.HYBRID_ALGORITHM); - packageJson.addProperty("encrypted_aes_key", "dGVzdA=="); - JsonObject encryptedPayload = new JsonObject(); - encryptedPayload.addProperty("ciphertext", "dGVzdA=="); - encryptedPayload.addProperty("nonce", "dGVzdA=="); - encryptedPayload.addProperty("tag", "dGVzdA=="); - packageJson.add("encrypted_payload", encryptedPayload); - - byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8); + byte[] encryptedResponse = getJsonResponse("9.9", Constants.HYBRID_ALGORITHM); CompletableFuture> future = client.decryptResponse(encryptedResponse, "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, - "Should throw ValueError for wrong version"); + assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for wrong version"); assertTrue(error.getCause().getMessage().contains("Unsupported protocol version"), "Error message should mention unsupported version"); } @@ -247,9 +235,19 @@ class DecryptResponseTest { SecureCompletionClient client = new SecureCompletionClient(); client.generateKeys(false); + byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, "wrong-algorithm"); + CompletableFuture> future = client.decryptResponse(encryptedResponse, "test-id"); + + ExecutionException error = assertThrows(ExecutionException.class, future::get); + assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for wrong algorithm"); + assertTrue(error.getCause().getMessage().contains("Unsupported encryption algorithm"), + "Error message should mention unsupported algorithm"); + } + + private static byte[] getJsonResponse(String protocolVersion, String value) { JsonObject packageJson = new JsonObject(); - packageJson.addProperty("version", Constants.PROTOCOL_VERSION); - packageJson.addProperty("algorithm", "wrong-algorithm"); + packageJson.addProperty("version", protocolVersion); + packageJson.addProperty("algorithm", value); packageJson.addProperty("encrypted_aes_key", "dGVzdA=="); JsonObject encryptedPayload = new JsonObject(); encryptedPayload.addProperty("ciphertext", "dGVzdA=="); @@ -257,38 +255,20 @@ class DecryptResponseTest { encryptedPayload.addProperty("tag", "dGVzdA=="); packageJson.add("encrypted_payload", encryptedPayload); - byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8); - CompletableFuture> future = client.decryptResponse(encryptedResponse, "test-id"); - - ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, - "Should throw ValueError for wrong algorithm"); - assertTrue(error.getCause().getMessage().contains("Unsupported encryption algorithm"), - "Error message should mention unsupported algorithm"); + 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() throws Exception { + void decryptResponse_noPrivateKey_shouldThrowSecurityError() { SecureCompletionClient client = new SecureCompletionClient(); - JsonObject packageJson = new JsonObject(); - 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=="); - encryptedPayload.addProperty("nonce", "dGVzdA=="); - encryptedPayload.addProperty("tag", "dGVzdA=="); - packageJson.add("encrypted_payload", encryptedPayload); - - byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8); + byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, Constants.HYBRID_ALGORITHM); CompletableFuture> future = client.decryptResponse(encryptedResponse, "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecurityError, - "Should throw SecurityError when no private key"); + assertInstanceOf(SecurityError.class, error.getCause(), "Should throw SecurityError when no private key"); assertTrue(error.getCause().getMessage().contains("Private key not initialized"), "Error message should mention private key not initialized"); } @@ -343,7 +323,6 @@ class DecryptResponseTest { CompletableFuture> future = client2.decryptResponse(encryptedResponse, "test-id"); ExecutionException error = assertThrows(ExecutionException.class, future::get); - assertTrue(error.getCause() instanceof SecurityError, - "Should throw SecurityError for wrong private key"); + assertInstanceOf(SecurityError.class, error.getCause(), "Should throw SecurityError for wrong private key"); } } diff --git a/src/test/java/ai/nomyo/SecureChatCompletionTest.java b/src/test/java/ai/nomyo/SecureChatCompletionTest.java index 011adb8..6f7c7aa 100644 --- a/src/test/java/ai/nomyo/SecureChatCompletionTest.java +++ b/src/test/java/ai/nomyo/SecureChatCompletionTest.java @@ -138,7 +138,7 @@ class SecureChatCompletionTest { } catch (RuntimeException e) { Throwable cause = e.getCause(); if (cause instanceof ExecutionException) { - throw (ExecutionException) cause; + throw cause; } throw new ExecutionException(cause); } @@ -158,7 +158,7 @@ class SecureChatCompletionTest { } catch (RuntimeException e) { Throwable cause = e.getCause(); if (cause instanceof ExecutionException) { - throw (ExecutionException) cause; + throw cause; } throw new ExecutionException(cause); } @@ -176,7 +176,7 @@ class SecureChatCompletionTest { } catch (RuntimeException e) { Throwable cause = e.getCause(); if (cause instanceof ExecutionException) { - throw (ExecutionException) cause; + throw cause; } throw new ExecutionException(cause); } @@ -203,7 +203,7 @@ class SecureChatCompletionTest { } catch (RuntimeException e) { Throwable cause = e.getCause(); if (cause instanceof ExecutionException) { - throw (ExecutionException) cause; + throw cause; } throw new ExecutionException(cause); } @@ -229,8 +229,8 @@ class SecureChatCompletionTest { void chatCompletion_close_multipleCalls_shouldNotThrow() { SecureChatCompletion chat = new SecureChatCompletion(); - assertDoesNotThrow(() -> chat.close(), "First close should not throw"); - assertDoesNotThrow(() -> chat.close(), "Second close should not throw"); + assertDoesNotThrow(chat::close, "First close should not throw"); + assertDoesNotThrow(chat::close, "Second close should not throw"); } @Test diff --git a/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java b/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java index 5db1a73..96da2c8 100644 --- a/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java +++ b/src/test/java/ai/nomyo/SecureCompletionClientE2ETest.java @@ -218,8 +218,8 @@ class SecureCompletionClientE2ETest { void e2e_multipleClients_independentOperations() throws Exception { File dir1 = tempDir.resolve("dir1").toFile(); File dir2 = tempDir.resolve("dir2").toFile(); - dir1.mkdirs(); - dir2.mkdirs(); + if (!dir1.mkdirs()) return; + if (!dir2.mkdirs()) return; // Client 1 SecureCompletionClient client1 = new SecureCompletionClient(); diff --git a/src/test/java/ai/nomyo/SecureMemoryTest.java b/src/test/java/ai/nomyo/SecureMemoryTest.java index d3b4c5c..32ee5b1 100644 --- a/src/test/java/ai/nomyo/SecureMemoryTest.java +++ b/src/test/java/ai/nomyo/SecureMemoryTest.java @@ -104,11 +104,12 @@ class SecureMemoryTest { @DisplayName("SecureBuffer zero should clear all bytes") void secureBuffer_zero_shouldClearBytes() { byte[] data = new byte[]{(byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF}; - SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data); + try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data)) { - buffer.zero(); + buffer.zero(); - assertDoesNotThrow(() -> buffer.zero(), "Zeroing should not throw"); + assertDoesNotThrow(buffer::zero, "Zeroing should not throw"); + } } @Test @@ -127,28 +128,31 @@ class SecureMemoryTest { @DisplayName("SecureBuffer close should be idempotent") void secureBuffer_close_idempotent() { byte[] data = new byte[]{1, 2, 3}; - SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data); + try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data)) { - assertDoesNotThrow(() -> buffer.close(), "First close should not throw"); - assertDoesNotThrow(() -> buffer.close(), "Second close should not throw"); + assertDoesNotThrow(buffer::close, "First close should not throw"); + assertDoesNotThrow(buffer::close, "Second close should not throw"); + } } @Test @DisplayName("SecureBuffer lock should return false (not supported)") void secureBuffer_lock_shouldReturnFalse() { byte[] data = new byte[]{1, 2, 3}; - SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true); + try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true)) { - assertFalse(buffer.lock(), "Lock should return false (not supported)"); + assertFalse(buffer.lock(), "Lock should return false (not supported)"); + } } @Test @DisplayName("SecureBuffer unlock should return false") void secureBuffer_unlock_shouldReturnFalse() { byte[] data = new byte[]{1, 2, 3}; - SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true); + try (SecureMemory.SecureBuffer buffer = SecureMemory.secureByteArray(data, true)) { - assertFalse(buffer.unlock(), "Unlock should return false"); + assertFalse(buffer.unlock(), "Unlock should return false"); + } } @Test diff --git a/src/test/java/ai/nomyo/SplitterTest.java b/src/test/java/ai/nomyo/SplitterTest.java index fcd783f..907520f 100644 --- a/src/test/java/ai/nomyo/SplitterTest.java +++ b/src/test/java/ai/nomyo/SplitterTest.java @@ -36,7 +36,7 @@ class SplitterTest { List result = Splitter.fixedLengthString(10, "1234567890"); assertEquals(1, result.size(), "Should have 1 part"); - assertEquals("1234567890", result.get(0), "Single part should be the full string"); + assertEquals("1234567890", result.getFirst(), "Single part should be the full string"); } @Test @@ -45,7 +45,7 @@ class SplitterTest { List result = Splitter.fixedLengthString(100, "hello"); assertEquals(1, result.size(), "Should have 1 part"); - assertEquals("hello", result.get(0), "Single part should be 'hello'"); + assertEquals("hello", result.getFirst(), "Single part should be 'hello'"); } @Test @@ -81,9 +81,9 @@ class SplitterTest { @Test @DisplayName("fixedLengthString should handle unicode characters") void fixedLengthString_unicode_shouldSplitCorrectly() { - List result = Splitter.fixedLengthString(2, "ab\u00e9\u00fc"); + List result = Splitter.fixedLengthString(2, "abéü"); assertEquals(2, result.size(), "Should have 2 parts"); - assertEquals("ab", result.get(0)); + assertEquals("ab", result.getFirst()); } }