Misc cleanup

This commit is contained in:
Oracle 2026-04-29 16:59:33 +02:00
parent 9b5fa56215
commit 084ce14451
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
17 changed files with 101 additions and 620 deletions

View file

@ -1,6 +1,6 @@
# nomyo4J — Agent Instructions # 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 ## Build & Run
@ -16,27 +16,26 @@ mvn test -Dtest=ClassName # single test class
- **`SecureChatCompletion`** — high-level OpenAI-compatible surface (`create()`, `acreate()`) - **`SecureChatCompletion`** — high-level OpenAI-compatible surface (`create()`, `acreate()`)
- **`Constants`** — all protocol/crypto constants (version, algorithms, timeouts) - **`Constants`** — all protocol/crypto constants (version, algorithms, timeouts)
- **`SecureMemory`** — Java 25 FFM `SecureBuffer` for locked/zeroed memory - **`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` - **`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` ## Dependencies
- `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. - **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 ## 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. - `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. - `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` - Package: `ai.nomyo`
- Lombok: `@Getter` on fields, `@Setter` on static flags - Lombok: `@Getter` on fields, `@Setter` on static flags
- Tests: `@TestMethodOrder(OrderAnnotation.class)`, `@DisplayName` on every test - Tests: `@TestMethodOrder(OrderAnnotation.class)`, `@DisplayName` on every test
- Error classes: checked exceptions with `status_code` and `error_details` - Error classes: checked exceptions with `statusCode` and `errorDetails` (immutable, via `Collections.unmodifiableMap`)
- Key files: `PosixFilePermissions.OWNER_READ` only (mode 400) - Key files: `PosixFilePermissions.fromString("rw-------")` for private, `"rw-r--r--"` for public
- RSA: 4096-bit, exponent 65537, OAEP-SHA256 padding - RSA: 4096-bit, exponent 65537, OAEP-SHA256 (MGF1 with SHA-256)
- Protocol constants in `Constants.java` — marked "never change" - 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

View file

@ -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 | 02 |
| `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": "<base64>",
"nonce": "<base64>",
"tag": "<base64>"
},
"encrypted_aes_key": "<base64>",
"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.

View file

@ -1,6 +1,5 @@
package ai.nomyo; package ai.nomyo;
import java.util.Set; import java.util.Set;
/** /**
@ -26,7 +25,6 @@ public final class Constants {
* AES-256-GCM payload encryption algorithm. * AES-256-GCM payload encryption algorithm.
*/ */
public static final String PAYLOAD_ALGORITHM = "AES-256-GCM"; public static final String PAYLOAD_ALGORITHM = "AES-256-GCM";
// Cryptographic Constants // Cryptographic Constants
/** /**
@ -53,7 +51,6 @@ public final class Constants {
* Minimum RSA key size for validation (bits). * Minimum RSA key size for validation (bits).
*/ */
public static final int MIN_RSA_KEY_SIZE = 2048; public static final int MIN_RSA_KEY_SIZE = 2048;
// Payload Limits // Payload Limits
/** /**
@ -75,7 +72,6 @@ public final class Constants {
* Retryable HTTP status codes. * Retryable HTTP status codes.
*/ */
public static final Set<Integer> RETRYABLE_STATUS_CODES = Set.of(429, 500, 502, 503, 504); public static final Set<Integer> RETRYABLE_STATUS_CODES = Set.of(429, 500, 502, 503, 504);
// File Permission Constants // File Permission Constants
/** /**
@ -86,7 +82,6 @@ public final class Constants {
* Public key file permission (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--"; public static final String PUBLIC_KEY_FILE_MODE = "rw-r--r--";
// Security Tier Constants // Security Tier Constants
/** /**
@ -105,7 +100,6 @@ public final class Constants {
* CPU only for PHI/classified data. * CPU only for PHI/classified data.
*/ */
public static final String SECURITY_TIER_MAXIMUM = "maximum"; public static final String SECURITY_TIER_MAXIMUM = "maximum";
// Endpoint Paths // Endpoint Paths
/** /**
@ -116,7 +110,6 @@ public final class Constants {
* Secure chat completion endpoint. * Secure chat completion endpoint.
*/ */
public static final String SECURE_COMPLETION_PATH = "/v1/chat/secure_completion"; public static final String SECURE_COMPLETION_PATH = "/v1/chat/secure_completion";
// HTTP Headers // HTTP Headers
/** /**
@ -139,7 +132,6 @@ public final class Constants {
* Bearer token prefix. * Bearer token prefix.
*/ */
public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer "; public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer ";
// Default Values // Default Values
/** /**
@ -158,7 +150,6 @@ public final class Constants {
* Default public key file name. * Default public key file name.
*/ */
public static final String DEFAULT_PUBLIC_KEY_FILE = "public_key.pem"; public static final String DEFAULT_PUBLIC_KEY_FILE = "public_key.pem";
// Memory Protection Constants // Memory Protection Constants
/** /**

View file

@ -15,7 +15,6 @@ public class EncryptedRequest {
private static final Gson GSON = new GsonBuilder().create(); private static final Gson GSON = new GsonBuilder().create();
// Getters and Setters
@SerializedName("version") @SerializedName("version")
private String version; private String version;
@ -26,7 +25,7 @@ public class EncryptedRequest {
private EncryptedPayload encryptedPayload; private EncryptedPayload encryptedPayload;
@SerializedName("encrypted_aes_key") @SerializedName("encrypted_aes_key")
private String encryptedAESKey; // Java variable name corrected to proper spelling private String encryptedAESKey;
@SerializedName("key_algorithm") @SerializedName("key_algorithm")
private String keyAlgorithm; private String keyAlgorithm;
@ -41,7 +40,6 @@ public class EncryptedRequest {
@Getter @Getter
public static class EncryptedPayload { public static class EncryptedPayload {
// Getters and Setters
@SerializedName("ciphertext") @SerializedName("ciphertext")
private String ciphertext; private String ciphertext;

View file

@ -146,24 +146,11 @@ public class SecureChatCompletion {
/** /**
* Convenience variant with no additional parameters. * Convenience variant with no additional parameters.
*/ */
@SuppressWarnings("UnusedReturnValue")
public Map<String, Object> create(String model, List<Map<String, Object>> messages) { public Map<String, Object> create(String model, List<Map<String, Object>> messages) {
return create(model, messages, null); return create(model, messages, null);
} }
/**
* Async alias for {@link #create(String, List, Map)}.
*/
public Map<String, Object> acreate(String model, List<Map<String, Object>> messages, Map<String, Object> kwargs) {
return create(model, messages, kwargs);
}
/**
* Async alias for {@link #create(String, List)}.
*/
public Map<String, Object> acreate(String model, List<Map<String, Object>> messages) {
return create(model, messages);
}
/** /**
* Delegates to {@link SecureCompletionClient#close()}. * Delegates to {@link SecureCompletionClient#close()}.
*/ */

View file

@ -295,6 +295,7 @@ public class SecureCompletionClient {
* @return encrypted bytes (JSON package) * @return encrypted bytes (JSON package)
* @throws SecurityError if encryption fails or keys not loaded * @throws SecurityError if encryption fails or keys not loaded
*/ */
@SuppressWarnings("JavadocDeclaration")
public CompletableFuture<byte[]> encryptPayload(Map<String, Object> payload) { public CompletableFuture<byte[]> encryptPayload(Map<String, Object> payload) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
try { try {

View file

@ -10,6 +10,7 @@ import java.util.Map;
/** /**
* Cross-platform memory locking and secure zeroing for sensitive cryptographic buffers. Fails gracefully if unavailable. * Cross-platform memory locking and secure zeroing for sensitive cryptographic buffers. Fails gracefully if unavailable.
*/ */
@SuppressWarnings("SameReturnValue")
public final class SecureMemory { public final class SecureMemory {
@Getter @Getter
@ -75,6 +76,7 @@ public final class SecureMemory {
/** /**
* Wraps bytes with memory locking and guaranteed zeroing on close. AutoCloseable for try-with-resources. * Wraps bytes with memory locking and guaranteed zeroing on close. AutoCloseable for try-with-resources.
*/ */
@SuppressWarnings("SameReturnValue")
public static class SecureBuffer implements AutoCloseable { public static class SecureBuffer implements AutoCloseable {
private final Arena arena; private final Arena arena;

View file

@ -31,5 +31,4 @@ public class APIError extends Exception {
public APIError(String message) { public APIError(String message) {
this(message, null, null); this(message, null, null);
} }
} }

View file

@ -11,36 +11,31 @@ public class PEMConverter {
* Encodes {@code keyData} as PEM (private or public) with 64-char base64 lines. * Encodes {@code keyData} as PEM (private or public) with 64-char base64 lines.
*/ */
public static String toPEM(byte[] keyData, boolean privateKey) { public static String toPEM(byte[] keyData, boolean privateKey) {
String publicKeyContent = Base64.getEncoder().encodeToString(keyData); String b64 = Base64.getEncoder().encodeToString(keyData);
StringBuilder publicKeyFormatted = new StringBuilder(privateKey ? "-----BEGIN PRIVATE KEY-----" : "-----BEGIN PUBLIC KEY-----"); String begin = privateKey ? "-----BEGIN PRIVATE KEY-----" : "-----BEGIN PUBLIC KEY-----";
publicKeyFormatted.append(System.lineSeparator()); String end = privateKey ? "-----END PRIVATE KEY-----" : "-----END PUBLIC KEY-----";
for (final String row : Splitter.fixedLengthString(64, publicKeyContent)) { StringBuilder sb = new StringBuilder(begin).append(System.lineSeparator());
publicKeyFormatted.append(row); for (String row : Splitter.fixedLengthString(64, b64)) {
publicKeyFormatted.append(System.lineSeparator()); sb.append(row).append(System.lineSeparator());
} }
sb.append(end);
publicKeyFormatted.append(privateKey ? "-----END PRIVATE KEY-----" : "-----END PUBLIC KEY-----"); return sb.toString();
return publicKeyFormatted.toString();
} }
public static byte[] fromPEM(String pem) { public static byte[] fromPEM(String pem) {
pem = pem.replace("-----BEGIN PRIVATE KEY-----", "") String cleaned = pem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "")
.replace("-----END PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", ""); .replaceAll("\\s+", "");
return Base64.getDecoder().decode(cleaned);
return Base64.getDecoder().decode(pem);
} }
public static boolean validatePEM(String keyIn) { public static boolean validatePEM(String keyIn) {
if (keyIn == null || keyIn.isBlank()) { if (keyIn == null || keyIn.isBlank()) {
return false; return false;
} }
String trimmed = keyIn.trim(); String trimmed = keyIn.trim();
return trimmed.startsWith("-----BEGIN PUBLIC KEY-----") return trimmed.startsWith("-----BEGIN PUBLIC KEY-----")
&& trimmed.endsWith("-----END PUBLIC KEY-----"); && trimmed.endsWith("-----END PUBLIC KEY-----");
} }

View file

@ -1,6 +1,7 @@
package ai.nomyo.util; package ai.nomyo.util;
import ai.nomyo.errors.SecurityError; import ai.nomyo.errors.SecurityError;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;

View file

@ -13,12 +13,10 @@ public class Splitter {
*/ */
public static List<String> fixedLengthString(int length, String toSplit) { public static List<String> fixedLengthString(int length, String toSplit) {
List<String> parts = new ArrayList<>(); List<String> parts = new ArrayList<>();
for (int i = 0; i < toSplit.length(); i += length) { for (int i = 0; i < toSplit.length(); i += length) {
int endIndex = Math.min(i + length, toSplit.length()); int endIndex = Math.min(i + length, toSplit.length());
parts.add(toSplit.substring(i, endIndex)); parts.add(toSplit.substring(i, endIndex));
} }
return parts; return parts;
} }

View file

@ -58,8 +58,8 @@ class CloseTest {
SecureCompletionClient client = new SecureCompletionClient(); SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false); client.generateKeys(false);
assertDoesNotThrow(() -> client.close(), "First close should not throw"); assertDoesNotThrow(client::close, "First close should not throw");
assertDoesNotThrow(() -> client.close(), "Second close should not throw"); assertDoesNotThrow(client::close, "Second close should not throw");
assertDoesNotThrow(() -> client.close(), "Third close should not throw"); assertDoesNotThrow(client::close, "Third close should not throw");
} }
} }

View file

@ -18,6 +18,9 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.security.spec.X509EncodedKeySpec; 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.*; import static org.junit.jupiter.api.Assertions.*;
@ -31,7 +34,6 @@ class DecryptResponseTest {
void decryptResponse_validPackage_shouldReturnDecryptedMap() throws Exception { void decryptResponse_validPackage_shouldReturnDecryptedMap() throws Exception {
SecureCompletionClient client = new SecureCompletionClient(); SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false); client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
String plaintext = "{\"content\":\"Hello, world!\",\"role\":\"assistant\"}"; String plaintext = "{\"content\":\"Hello, world!\",\"role\":\"assistant\"}";
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8); byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
@ -57,7 +59,8 @@ class DecryptResponseTest {
PublicKey serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec); PublicKey serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); 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()); byte[] encryptedAESKey = rsaCipher.doFinal(aesKey.getEncoded());
JsonObject packageJson = new JsonObject(); JsonObject packageJson = new JsonObject();
@ -97,7 +100,6 @@ class DecryptResponseTest {
void decryptResponse_missingProcessedAt_shouldSetNull() throws Exception { void decryptResponse_missingProcessedAt_shouldSetNull() throws Exception {
SecureCompletionClient client = new SecureCompletionClient(); SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false); client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
String plaintext = "{\"response\":\"ok\"}"; String plaintext = "{\"response\":\"ok\"}";
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8); byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
@ -121,7 +123,8 @@ class DecryptResponseTest {
PublicKey serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(pubKeyBytes)); PublicKey serverPublicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(pubKeyBytes));
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); 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()); byte[] encryptedAESKey = rsaCipher.doFinal(aesKey.getEncoded());
JsonObject packageJson = new JsonObject(); JsonObject packageJson = new JsonObject();
@ -155,8 +158,7 @@ class DecryptResponseTest {
CompletableFuture<Map<String, Object>> future = client.decryptResponse(new byte[0], "test-id"); CompletableFuture<Map<String, Object>> future = client.decryptResponse(new byte[0], "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for empty response");
"Should throw ValueError for empty response");
assertTrue(error.getCause().getMessage().contains("Empty"), assertTrue(error.getCause().getMessage().contains("Empty"),
"Error message should mention empty"); "Error message should mention empty");
} }
@ -171,8 +173,7 @@ class DecryptResponseTest {
CompletableFuture<Map<String, Object>> future = client.decryptResponse(null, "test-id"); CompletableFuture<Map<String, Object>> future = client.decryptResponse(null, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for null response");
"Should throw ValueError for null response");
} }
@Test @Test
@ -186,8 +187,7 @@ class DecryptResponseTest {
CompletableFuture<Map<String, Object>> future = client.decryptResponse(invalidJson, "test-id"); CompletableFuture<Map<String, Object>> future = client.decryptResponse(invalidJson, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for malformed JSON");
"Should throw ValueError for malformed JSON");
assertTrue(error.getCause().getMessage().contains("malformed JSON") || error.getCause().getMessage().contains("JSON"), assertTrue(error.getCause().getMessage().contains("malformed JSON") || error.getCause().getMessage().contains("JSON"),
"Error message should mention JSON"); "Error message should mention JSON");
} }
@ -207,8 +207,7 @@ class DecryptResponseTest {
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id"); CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for missing fields");
"Should throw ValueError for missing fields");
assertTrue(error.getCause().getMessage().contains("Missing required fields"), assertTrue(error.getCause().getMessage().contains("Missing required fields"),
"Error message should mention missing fields"); "Error message should mention missing fields");
} }
@ -220,22 +219,11 @@ class DecryptResponseTest {
SecureCompletionClient client = new SecureCompletionClient(); SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false); client.generateKeys(false);
JsonObject packageJson = new JsonObject(); byte[] encryptedResponse = getJsonResponse("9.9", Constants.HYBRID_ALGORITHM);
packageJson.addProperty("version", "9.9");
packageJson.addProperty("algorithm", Constants.HYBRID_ALGORITHM);
packageJson.addProperty("encrypted_aes_key", "dGVzdA==");
JsonObject encryptedPayload = new JsonObject();
encryptedPayload.addProperty("ciphertext", "dGVzdA==");
encryptedPayload.addProperty("nonce", "dGVzdA==");
encryptedPayload.addProperty("tag", "dGVzdA==");
packageJson.add("encrypted_payload", encryptedPayload);
byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8);
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id"); CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecureCompletionClient.ValueError, assertInstanceOf(SecureCompletionClient.ValueError.class, error.getCause(), "Should throw ValueError for wrong version");
"Should throw ValueError for wrong version");
assertTrue(error.getCause().getMessage().contains("Unsupported protocol version"), assertTrue(error.getCause().getMessage().contains("Unsupported protocol version"),
"Error message should mention unsupported version"); "Error message should mention unsupported version");
} }
@ -247,9 +235,19 @@ class DecryptResponseTest {
SecureCompletionClient client = new SecureCompletionClient(); SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false); client.generateKeys(false);
byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, "wrong-algorithm");
CompletableFuture<Map<String, Object>> 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(); JsonObject packageJson = new JsonObject();
packageJson.addProperty("version", Constants.PROTOCOL_VERSION); packageJson.addProperty("version", protocolVersion);
packageJson.addProperty("algorithm", "wrong-algorithm"); packageJson.addProperty("algorithm", value);
packageJson.addProperty("encrypted_aes_key", "dGVzdA=="); packageJson.addProperty("encrypted_aes_key", "dGVzdA==");
JsonObject encryptedPayload = new JsonObject(); JsonObject encryptedPayload = new JsonObject();
encryptedPayload.addProperty("ciphertext", "dGVzdA=="); encryptedPayload.addProperty("ciphertext", "dGVzdA==");
@ -257,38 +255,20 @@ class DecryptResponseTest {
encryptedPayload.addProperty("tag", "dGVzdA=="); encryptedPayload.addProperty("tag", "dGVzdA==");
packageJson.add("encrypted_payload", encryptedPayload); packageJson.add("encrypted_payload", encryptedPayload);
byte[] encryptedResponse = packageJson.toString().getBytes(StandardCharsets.UTF_8); return packageJson.toString().getBytes(StandardCharsets.UTF_8);
CompletableFuture<Map<String, Object>> 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");
} }
@Test @Test
@Execution(ExecutionMode.SAME_THREAD) @Execution(ExecutionMode.SAME_THREAD)
@DisplayName("decryptResponse should throw SecurityError when private key not initialized") @DisplayName("decryptResponse should throw SecurityError when private key not initialized")
void decryptResponse_noPrivateKey_shouldThrowSecurityError() throws Exception { void decryptResponse_noPrivateKey_shouldThrowSecurityError() {
SecureCompletionClient client = new SecureCompletionClient(); SecureCompletionClient client = new SecureCompletionClient();
JsonObject packageJson = new JsonObject(); byte[] encryptedResponse = getJsonResponse(Constants.PROTOCOL_VERSION, Constants.HYBRID_ALGORITHM);
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);
CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id"); CompletableFuture<Map<String, Object>> future = client.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecurityError, assertInstanceOf(SecurityError.class, error.getCause(), "Should throw SecurityError when no private key");
"Should throw SecurityError when no private key");
assertTrue(error.getCause().getMessage().contains("Private key not initialized"), assertTrue(error.getCause().getMessage().contains("Private key not initialized"),
"Error message should mention private key not initialized"); "Error message should mention private key not initialized");
} }
@ -343,7 +323,6 @@ class DecryptResponseTest {
CompletableFuture<Map<String, Object>> future = client2.decryptResponse(encryptedResponse, "test-id"); CompletableFuture<Map<String, Object>> future = client2.decryptResponse(encryptedResponse, "test-id");
ExecutionException error = assertThrows(ExecutionException.class, future::get); ExecutionException error = assertThrows(ExecutionException.class, future::get);
assertTrue(error.getCause() instanceof SecurityError, assertInstanceOf(SecurityError.class, error.getCause(), "Should throw SecurityError for wrong private key");
"Should throw SecurityError for wrong private key");
} }
} }

View file

@ -138,7 +138,7 @@ class SecureChatCompletionTest {
} catch (RuntimeException e) { } catch (RuntimeException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof ExecutionException) { if (cause instanceof ExecutionException) {
throw (ExecutionException) cause; throw cause;
} }
throw new ExecutionException(cause); throw new ExecutionException(cause);
} }
@ -158,7 +158,7 @@ class SecureChatCompletionTest {
} catch (RuntimeException e) { } catch (RuntimeException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof ExecutionException) { if (cause instanceof ExecutionException) {
throw (ExecutionException) cause; throw cause;
} }
throw new ExecutionException(cause); throw new ExecutionException(cause);
} }
@ -176,7 +176,7 @@ class SecureChatCompletionTest {
} catch (RuntimeException e) { } catch (RuntimeException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof ExecutionException) { if (cause instanceof ExecutionException) {
throw (ExecutionException) cause; throw cause;
} }
throw new ExecutionException(cause); throw new ExecutionException(cause);
} }
@ -203,7 +203,7 @@ class SecureChatCompletionTest {
} catch (RuntimeException e) { } catch (RuntimeException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof ExecutionException) { if (cause instanceof ExecutionException) {
throw (ExecutionException) cause; throw cause;
} }
throw new ExecutionException(cause); throw new ExecutionException(cause);
} }
@ -229,8 +229,8 @@ class SecureChatCompletionTest {
void chatCompletion_close_multipleCalls_shouldNotThrow() { void chatCompletion_close_multipleCalls_shouldNotThrow() {
SecureChatCompletion chat = new SecureChatCompletion(); SecureChatCompletion chat = new SecureChatCompletion();
assertDoesNotThrow(() -> chat.close(), "First close should not throw"); assertDoesNotThrow(chat::close, "First close should not throw");
assertDoesNotThrow(() -> chat.close(), "Second close should not throw"); assertDoesNotThrow(chat::close, "Second close should not throw");
} }
@Test @Test

View file

@ -218,8 +218,8 @@ class SecureCompletionClientE2ETest {
void e2e_multipleClients_independentOperations() throws Exception { void e2e_multipleClients_independentOperations() throws Exception {
File dir1 = tempDir.resolve("dir1").toFile(); File dir1 = tempDir.resolve("dir1").toFile();
File dir2 = tempDir.resolve("dir2").toFile(); File dir2 = tempDir.resolve("dir2").toFile();
dir1.mkdirs(); if (!dir1.mkdirs()) return;
dir2.mkdirs(); if (!dir2.mkdirs()) return;
// Client 1 // Client 1
SecureCompletionClient client1 = new SecureCompletionClient(); SecureCompletionClient client1 = new SecureCompletionClient();

View file

@ -104,11 +104,12 @@ class SecureMemoryTest {
@DisplayName("SecureBuffer zero should clear all bytes") @DisplayName("SecureBuffer zero should clear all bytes")
void secureBuffer_zero_shouldClearBytes() { void secureBuffer_zero_shouldClearBytes() {
byte[] data = new byte[]{(byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF}; 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 @Test
@ -127,28 +128,31 @@ class SecureMemoryTest {
@DisplayName("SecureBuffer close should be idempotent") @DisplayName("SecureBuffer close should be idempotent")
void secureBuffer_close_idempotent() { void secureBuffer_close_idempotent() {
byte[] data = new byte[]{1, 2, 3}; 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, "First close should not throw");
assertDoesNotThrow(() -> buffer.close(), "Second close should not throw"); assertDoesNotThrow(buffer::close, "Second close should not throw");
}
} }
@Test @Test
@DisplayName("SecureBuffer lock should return false (not supported)") @DisplayName("SecureBuffer lock should return false (not supported)")
void secureBuffer_lock_shouldReturnFalse() { void secureBuffer_lock_shouldReturnFalse() {
byte[] data = new byte[]{1, 2, 3}; 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 @Test
@DisplayName("SecureBuffer unlock should return false") @DisplayName("SecureBuffer unlock should return false")
void secureBuffer_unlock_shouldReturnFalse() { void secureBuffer_unlock_shouldReturnFalse() {
byte[] data = new byte[]{1, 2, 3}; 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 @Test

View file

@ -36,7 +36,7 @@ class SplitterTest {
List<String> result = Splitter.fixedLengthString(10, "1234567890"); List<String> result = Splitter.fixedLengthString(10, "1234567890");
assertEquals(1, result.size(), "Should have 1 part"); 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 @Test
@ -45,7 +45,7 @@ class SplitterTest {
List<String> result = Splitter.fixedLengthString(100, "hello"); List<String> result = Splitter.fixedLengthString(100, "hello");
assertEquals(1, result.size(), "Should have 1 part"); 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 @Test
@ -81,9 +81,9 @@ class SplitterTest {
@Test @Test
@DisplayName("fixedLengthString should handle unicode characters") @DisplayName("fixedLengthString should handle unicode characters")
void fixedLengthString_unicode_shouldSplitCorrectly() { void fixedLengthString_unicode_shouldSplitCorrectly() {
List<String> result = Splitter.fixedLengthString(2, "ab\u00e9\u00fc"); List<String> result = Splitter.fixedLengthString(2, "abéü");
assertEquals(2, result.size(), "Should have 2 parts"); assertEquals(2, result.size(), "Should have 2 parts");
assertEquals("ab", result.get(0)); assertEquals("ab", result.getFirst());
} }
} }