Initial commit

This commit is contained in:
Oracle 2026-04-21 17:24:11 +02:00
commit 8acf584d28
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
24 changed files with 2408 additions and 0 deletions

39
.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

10
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/encodings.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

18
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="temurin-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

7
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

478
TRANSLATION_REFERENCE.md Normal file
View file

@ -0,0 +1,478 @@
# 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.

42
pom.xml Normal file
View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ai.nomyo</groupId>
<artifactId>nomyo4J</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>6.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,199 @@
package ai.nomyo;
import java.util.Set;
/**
* Constants used throughout the NOMYO Java client library.
*
* <p>These values correspond to the documented constants in the Python
* reference. Protocol version and algorithm strings are immutable and
* must never be changed they are used for downgrade detection.</p>
*/
public final class Constants {
private Constants() {
// Utility class prevents instantiation
}
// Protocol Constants
/**
* Protocol version string. Never change used for downgrade detection.
*/
public static final String PROTOCOL_VERSION = "1.0";
/**
* Hybrid encryption algorithm identifier. Never change used for downgrade detection.
*/
public static final String HYBRID_ALGORITHM = "hybrid-aes256-rsa4096";
/**
* RSA-OAEP-SHA256 key wrapping algorithm identifier.
*/
public static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-SHA256";
/**
* AES-256-GCM payload encryption algorithm identifier.
*/
public static final String PAYLOAD_ALGORITHM = "AES-256-GCM";
// Cryptographic Constants
/**
* RSA key size in bits. Fixed at 4096.
*/
public static final int RSA_KEY_SIZE = 4096;
/**
* RSA public exponent. Fixed at 65537.
*/
public static final int RSA_PUBLIC_EXPONENT = 65537;
/**
* AES key size in bytes (256-bit). Per-request ephemeral.
*/
public static final int AES_KEY_SIZE = 32;
/**
* GCM nonce size in bytes (96-bit). Per-request.
*/
public static final int GCM_NONCE_SIZE = 12;
/**
* GCM authentication tag size in bytes.
*/
public static final int GCM_TAG_SIZE = 16;
/**
* Minimum RSA key size for validation (bits).
*/
public static final int MIN_RSA_KEY_SIZE = 2048;
// Payload Limits
/**
* Maximum payload size in bytes (10 MB). Used for DoS protection.
*/
public static final long MAX_PAYLOAD_SIZE = 10L * 1024 * 1024;
// HTTP / Retry Constants
/**
* Default HTTP request timeout in seconds.
*/
public static final int DEFAULT_TIMEOUT_SECONDS = 60;
/**
* Default number of retries on retryable errors.
* Exponential backoff: 1s, 2s, 4s
*/
public static final int DEFAULT_MAX_RETRIES = 2;
/**
* Set of HTTP status codes that are eligible for retry.
*/
public static final Set<Integer> RETRYABLE_STATUS_CODES = Set.of(429, 500, 502, 503, 504);
// File Permission Constants
/**
* File permission for private key files (owner read/write only).
*/
public static final String PRIVATE_KEY_FILE_MODE = "rw-------";
/**
* File permission for public key files (owner rw, group/others r).
*/
public static final String PUBLIC_KEY_FILE_MODE = "rw-r--r--";
// Security Tier Constants
/**
* Valid security tier values. Case-sensitive.
*/
public static final Set<String> VALID_SECURITY_TIERS = Set.of("standard", "high", "maximum");
/**
* Standard security tier GPU general secure inference.
*/
public static final String SECURITY_TIER_STANDARD = "standard";
/**
* High security tier CPU/GPU for sensitive business data.
*/
public static final String SECURITY_TIER_HIGH = "high";
/**
* Maximum security tier CPU only for PHI/classified data.
*/
public static final String SECURITY_TIER_MAXIMUM = "maximum";
// Endpoint Paths
/**
* PKI public key endpoint path.
*/
public static final String PKI_PUBLIC_KEY_PATH = "/pki/public_key";
/**
* Secure chat completion endpoint path.
*/
public static final String SECURE_COMPLETION_PATH = "/v1/chat/secure_completion";
// HTTP Headers
/**
* Content-Type for encrypted payloads.
*/
public static final String CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
/**
* HTTP header name for payload ID.
*/
public static final String HEADER_PAYLOAD_ID = "X-Payload-ID";
/**
* HTTP header name for client public key.
*/
public static final String HEADER_PUBLIC_KEY = "X-Public-Key";
/**
* HTTP header name for security tier.
*/
public static final String HEADER_SECURITY_TIER = "X-Security-Tier";
/**
* HTTP header prefix for Bearer token authorization.
*/
public static final String AUTHORIZATION_BEARER_PREFIX = "Bearer ";
// Default Values
/**
* Default NOMYO router base URL.
*/
public static final String DEFAULT_BASE_URL = "https://api.nomyo.ai";
/**
* Default key directory name for persisted keys.
*/
public static final String DEFAULT_KEY_DIR = "client_keys";
/**
* Default private key file name.
*/
public static final String DEFAULT_PRIVATE_KEY_FILE = "private_key.pem";
/**
* Default public key file name.
*/
public static final String DEFAULT_PUBLIC_KEY_FILE = "public_key.pem";
// Memory Protection Constants
/**
* Page size used for memory locking calculations (typically 4096 bytes).
*/
public static final int PAGE_SIZE = 4096;
}

View file

@ -0,0 +1,27 @@
package ai.nomyo;
import ai.nomyo.errors.SecurityError;
/**
* @author NieGestorben
* Copyright© (c) 2026, All Rights Reserved.
*/
public class Main {
static void main() {
SecureCompletionClient secureCompletionClient = new SecureCompletionClient();
//secureCompletionClient.generateKeys(true, "client_keys", "pokemon");
secureCompletionClient.loadKeys("client_keys/private_key.pem", "pokemon");
try {
secureCompletionClient.validateRsaKey(secureCompletionClient.getPrivateKey());
} catch (SecurityError e) {
System.out.println("RSA Key is to short!");
return;
}
System.out.println("RSA Key has correct length!");
}
}

View file

@ -0,0 +1,188 @@
package ai.nomyo;
import ai.nomyo.errors.*;
import lombok.Getter;
import java.util.List;
import java.util.Map;
/**
* High-level OpenAI-compatible entrypoint for the NOMYO secure API.
*
* <p>This class provides a familiar API surface matching {@code openai.ChatCompletion.create()}.
* All requests are automatically encrypted using hybrid AES-256-GCM + RSA-4096 encryption
* before being sent to the NOMYO router.</p>
*
* <h3>Usage</h3>
* <pre>{@code
* SecureChatCompletion client = new SecureChatCompletion(
* "https://api.nomyo.ai",
* false,
* "your-api-key",
* true,
* "/path/to/keys",
* 2
* );
*
* Map<String, Object> response = client.create(
* "Qwen/Qwen3-0.6B",
* List.of(Map.of("role", "user", "content", "Hello, world!"))
* );
* }</pre>
*
* <h3>Streaming</h3>
* <p>Streaming is <b>not supported</b>. The server rejects streaming requests with HTTP 400.
* Always use {@code stream=false} (the default).</p>
*
* <h3>Security Tiers</h3>
* <p>The {@code security_tier} parameter controls the hardware isolation level:</p>
* <ul>
* <li>{@code "standard"} GPU inference (general secure inference)</li>
* <li>{@code "high"} CPU/GPU (sensitive business data)</li>
* <li>{@code "maximum"} CPU only (PHI, classified data)</li>
* </ul>
*
* <h3>Key Persistence</h3>
* <p>Set {@code keyDir} to a directory path to persist RSA keys to disk.
* Keys are generated on first use and reused across all calls.
* Set {@code keyDir} to {@code null} for ephemeral keys (in-memory only, lost on restart).</p>
*/
@Getter
public class SecureChatCompletion {
private final SecureCompletionClient client;
private final String apiKey;
private final String keyDir;
/**
* Constructs a {@code SecureChatCompletion} with default settings.
*
* <p>Uses the default NOMYO router URL ({@code https://api.nomyo.ai}),
* HTTPS-only, secure memory enabled, ephemeral keys, and 2 retries.</p>
*/
public SecureChatCompletion() {
this(Constants.DEFAULT_BASE_URL, false, null, true, null, Constants.DEFAULT_MAX_RETRIES);
}
/**
* Constructs a {@code SecureChatCompletion} with the specified settings.
*
* @param baseUrl NOMYO Router base URL (HTTPS enforced unless {@code allowHttp} is {@code true})
* @param allowHttp permit {@code http://} URLs (development only)
* @param apiKey Bearer token for authentication (can also be passed per-call via {@link #create})
* @param secureMemory enable memory locking/zeroing (warns if unavailable)
* @param keyDir directory to persist RSA keys; {@code null} = ephemeral (in-memory only)
* @param maxRetries retries on 429/500/502/503/504 + network errors (exponential backoff: 1s, 2s, 4s)
*/
public SecureChatCompletion(
String baseUrl,
boolean allowHttp,
String apiKey,
boolean secureMemory,
String keyDir,
int maxRetries
) {
this.client = new SecureCompletionClient(baseUrl, allowHttp, secureMemory, maxRetries);
this.apiKey = apiKey;
this.keyDir = keyDir;
}
/**
* Creates a chat completion with the specified parameters.
*
* <p>This is the main entrypoint, with the same signature as
* {@code openai.ChatCompletion.create()}. Returns a map (not an object)
* containing the OpenAI-compatible response.</p>
*
* <h3>Parameters</h3>
* <table>
* <tr><th>Param</th><th>Type</th><th>Required</th><th>Description</th></tr>
* <tr><td>{@code model}</td><td>{@code String}</td><td>yes</td><td>Model identifier, e.g. "Qwen/Qwen3-0.6B"</td></tr>
* <tr><td>{@code messages}</td><td>{@code List<Map>}</td><td>yes</td><td>OpenAI-format messages</td></tr>
* <tr><td>{@code temperature}</td><td>{@code Double}</td><td>no</td><td>02</td></tr>
* <tr><td>{@code maxTokens}</td><td>{@code Integer}</td><td>no</td><td>Maximum tokens in response</td></tr>
* <tr><td>{@code topP}</td><td>{@code Double}</td><td>no</td><td>Top-p sampling parameter</td></tr>
* <tr><td>{@code stop}</td><td>{@code String | List<String>}</td><td>no</td><td>Stop sequences</td></tr>
* <tr><td>{@code presencePenalty}</td><td>{@code Double}</td><td>no</td><td>-2.0 to 2.0</td></tr>
* <tr><td>{@code frequencyPenalty}</td><td>{@code Double}</td><td>no</td><td>-2.0 to 2.0</td></tr>
* <tr><td>{@code n}</td><td>{@code Integer}</td><td>no</td><td>Number of completions</td></tr>
* <tr><td>{@code bestOf}</td><td>{@code Integer}</td><td>no</td><td></td></tr>
* <tr><td>{@code seed}</td><td>{@code Integer}</td><td>no</td><td>Reproducibility seed</td></tr>
* <tr><td>{@code logitBias}</td><td>{@code Map<String, Double>}</td><td>no</td><td>Token bias map</td></tr>
* <tr><td>{@code user}</td><td>{@code String}</td><td>no</td><td>End-user identifier</td></tr>
* <tr><td>{@code tools}</td><td>{@code List<Map>}</td><td>no</td><td>Tool definitions passed through to llama.cpp</td></tr>
* <tr><td>{@code toolChoice}</td><td>{@code String}</td><td>no</td><td>"auto", "none", or specific tool name</td></tr>
* <tr><td>{@code responseFormat}</td><td>{@code Map}</td><td>no</td><td>{"type": "json_object"} or {"type": "json_schema", ...}</td></tr>
* <tr><td>{@code stream}</td><td>{@code Boolean}</td><td>no</td><td><b>NOT supported.</b> Server rejects with HTTP 400. Always use {@code false}.</td></tr>
* <tr><td>{@code baseUrl}</td><td>{@code String}</td><td>no</td><td>Per-call override (creates temp client internally)</td></tr>
* <tr><td>{@code securityTier}</td><td>{@code String}</td><td>no</td><td>"standard", "high", or "maximum". Invalid values raise {@code ValueError}.</td></tr>
* <tr><td>{@code apiKey}</td><td>{@code String}</td><td>no</td><td>Per-call override of instance {@code apiKey}.</td></tr>
* </table>
*
* @param model model identifier (required)
* @param messages OpenAI-format message list (required)
* @param kwargs additional OpenAI-compatible parameters
* @return OpenAI-compatible response map (see §6.2 of reference docs)
* @throws SecurityError if encryption/decryption fails
* @throws APIConnectionError if a network error occurs
* @throws InvalidRequestError if the API returns 400
* @throws AuthenticationError if the API returns 401
* @throws ForbiddenError if the API returns 403
* @throws RateLimitError if the API returns 429
* @throws ServerError if the API returns 500
* @throws ServiceUnavailableError if the API returns 503
* @throws APIError for other errors
*/
public Map<String, Object> create(String model, List<Map<String, Object>> messages, Map<String, Object> kwargs) {
// Build payload from model, messages, and kwargs
// Validate stream is false
// Validate securityTier if provided
// Use per-call api_key override if provided, else instance apiKey
// Create temp client if baseUrl override provided
// Send secure request
// Return decrypted response map
return null;
}
/**
* Creates a chat completion with the specified model and messages.
* Convenience variant with no additional parameters.
*
* @param model model identifier (required)
* @param messages OpenAI-format message list (required)
* @return OpenAI-compatible response map
*/
public Map<String, Object> create(String model, List<Map<String, Object>> messages) {
return create(model, messages, null);
}
/**
* Async alias for {@link #create(String, List, Map)}. Identical behavior.
*
* @param model model identifier (required)
* @param messages OpenAI-format message list (required)
* @param kwargs additional OpenAI-compatible parameters
* @return OpenAI-compatible response 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)}. Identical behavior.
*
* @param model model identifier (required)
* @param messages OpenAI-format message list (required)
* @return OpenAI-compatible response map
*/
public Map<String, Object> acreate(String model, List<Map<String, Object>> messages) {
return create(model, messages);
}
/**
* Closes the client and releases any resources.
*/
public void close() {
client.close();
}
}

View file

@ -0,0 +1,513 @@
package ai.nomyo;
import ai.nomyo.errors.*;
import ai.nomyo.util.PEMConverter;
import ai.nomyo.util.Pass2Key;
import lombok.Getter;
import javax.crypto.*;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAKeyGenParameterSpec;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
/**
* Low-level secure completion client for the NOMYO API.
*
* <p>This class handles key management, hybrid encryption, HTTP communication
* with retry logic, and response decryption. It is the core of the NOMYO
* Java client and is used internally by {@link SecureChatCompletion}.</p>
*
* <h3>Encryption Wire Format</h3>
* <p>Encrypted payloads use hybrid encryption (AES-256-GCM + RSA-4096-OAEP-SHA256):</p>
* <ul>
* <li>A per-request 256-bit AES key encrypts the payload via AES-256-GCM</li>
* <li>The AES key is encrypted via RSA-4096-OAEP-SHA256 using the server's public key</li>
* <li>The result is a JSON package with base64-encoded ciphertext, nonce, tag, and encrypted AES key</li>
* </ul>
*
* <h3>Key Lifecycle</h3>
* <p>Client RSA keys are generated on first use (if not loaded from disk) and
* reused across all subsequent calls until the client is discarded. Keys can
* be persisted to disk via {@link #generateKeys(boolean, String, String)} or
* loaded from disk via {@link #loadKeys(String, String, String)}.</p>
*/
public class SecureCompletionClient {
// Instance Attributes
/**
* Base URL of the NOMYO router (trailing slash stripped).
*/
@Getter
private final String routerUrl;
/**
* Whether HTTP (non-HTTPS) URLs are permitted.
*/
@Getter
private final boolean allowHttp;
/**
* RSA key size in bits. Always {@link Constants#RSA_KEY_SIZE}.
*/
@Getter
private final int keySize;
/**
* Maximum number of retries for retryable errors.
*/
@Getter
private final int maxRetries;
/**
* Whether secure memory operations are active.
*/
@Getter
private final boolean useSecureMemory;
/**
* Lock for double-checked key initialization.
*/
private final ReentrantLock keyInitLock = new ReentrantLock();
/**
* RSA private key, or {@code null} if not yet loaded/generated.
*/
@Getter
private PrivateKey privateKey;
// Internal State
/**
* PEM-encoded public key string, or {@code null} if not yet loaded/generated.
*/
@Getter
private String publicPemKey;
/**
* Whether keys have been initialized.
*/
private volatile boolean keysInitialized = false;
/**
* Constructs a {@code SecureCompletionClient} with default settings.
*/
public SecureCompletionClient() {
this(Constants.DEFAULT_BASE_URL, false, true, Constants.DEFAULT_MAX_RETRIES);
}
/**
* Constructs a {@code SecureCompletionClient} with the specified settings.
*
* @param routerUrl the NOMYO router base URL
* @param allowHttp whether to permit HTTP (non-HTTPS) URLs
* @param secureMemory whether to enable memory locking/zeroing
* @param maxRetries number of retries on retryable errors
*/
public SecureCompletionClient(String routerUrl, boolean allowHttp, boolean secureMemory, int maxRetries) {
this.routerUrl = routerUrl != null ? routerUrl.replaceAll("/+$", "") : Constants.DEFAULT_BASE_URL;
this.allowHttp = allowHttp;
this.useSecureMemory = secureMemory;
this.keySize = Constants.RSA_KEY_SIZE;
this.maxRetries = maxRetries;
}
// Key Management
private static String getEncryptedPrivateKeyFromFile(String privateKeyPath) {
File myObj = new File(privateKeyPath);
StringBuilder builder = new StringBuilder();
try (Scanner myReader = new Scanner(myObj)) {
while (myReader.hasNextLine()) {
builder.append(myReader.nextLine());
}
} catch (FileNotFoundException e) {
throw new RuntimeException("Tried to load private key from disk but no file found" + e.getMessage());
}
return builder.toString();
}
/**
* Generates a new 4096-bit RSA key pair.
*
* <p>The public exponent is fixed at 65537. The generated key pair
* is stored in memory. Use {@code saveToDir} to persist to disk.</p>
*
* @param saveToFile whether to save the keys to disk
* @param keyDir directory to save keys (ignored if {@code saveToFile} is {@code false})
* @param password optional password to encrypt the private key file
*/
public void generateKeys(boolean saveToFile, String keyDir, String password) {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(new RSAKeyGenParameterSpec(Constants.RSA_KEY_SIZE, BigInteger.valueOf(Constants.RSA_PUBLIC_EXPONENT)));
KeyPair pair = generator.generateKeyPair();
String privatePem = PEMConverter.toPEM(pair.getPrivate().getEncoded(), true);
String publicPem = PEMConverter.toPEM(pair.getPublic().getEncoded(), false);
if (saveToFile) {
File keyFolder = new File(keyDir);
if (!keyFolder.exists() && !keyFolder.mkdirs()) {
throw new IOException("Failed to create key directory: " + keyDir);
}
Path privateKeyPath = Path.of(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
if (!Files.exists(privateKeyPath)) {
Set<PosixFilePermission> filePermissions = PosixFilePermissions.fromString(Constants.PRIVATE_KEY_FILE_MODE);
Files.createFile(privateKeyPath, PosixFilePermissions.asFileAttribute(filePermissions));
try (FileWriter fileWriter = new FileWriter(privateKeyPath.toFile())) {
if (password == null || password.isEmpty()) {
System.out.println("WARNING: Saving keys in plaintext!");
} else {
try {
privatePem = Pass2Key.encrypt("AES/GCM/NoPadding", privatePem, password);
} catch (NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException |
InvalidKeyException e) {
throw new RuntimeException(e);
}
}
fileWriter.write(privatePem);
fileWriter.flush();
}
}
Path publicKeyPath = Path.of(keyDir, Constants.DEFAULT_PUBLIC_KEY_FILE);
if (!Files.exists(publicKeyPath)) {
Set<PosixFilePermission> publicPermissions = PosixFilePermissions.fromString(Constants.PUBLIC_KEY_FILE_MODE);
Files.createFile(publicKeyPath, PosixFilePermissions.asFileAttribute(publicPermissions));
try (FileWriter fileWriter = new FileWriter(publicKeyPath.toFile())) {
fileWriter.write(publicPem);
fileWriter.flush();
}
}
}
this.privateKey = pair.getPrivate();
this.publicPemKey = publicPem;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RSA not available: " + e.getMessage(), e);
} catch (InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException("Failed to save keys: " + e.getMessage(), e);
}
}
/**
* Generates a new 4096-bit RSA key pair and saves to the default directory.
*
* @param saveToFile whether to save the keys to disk
*/
public void generateKeys(boolean saveToFile) {
generateKeys(saveToFile, Constants.DEFAULT_KEY_DIR, null);
}
/**
* Loads an RSA private key from disk.
*
* <p>If {@code publicPemKeyPath} is {@code null}, the public key is
* derived from the loaded private key. Validates that the key size
* is at least {@link Constants#MIN_RSA_KEY_SIZE} bits.</p>
*
* @param privateKeyPath path to the private key PEM file
* @param publicPemKeyPath optional path to the public key PEM file
* @param password optional password for the encrypted private key
*/
public void loadKeys(String privateKeyPath, String publicPemKeyPath, String password) {
if (password != null && !password.isEmpty()) {
String cipherText = getEncryptedPrivateKeyFromFile(privateKeyPath);
try {
cipherText = Pass2Key.decrypt("AES/GCM/NoPadding", cipherText, password);
} catch (NoSuchPaddingException | NoSuchAlgorithmException
| BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException |
InvalidKeyException e) {
System.out.println("Wrong password!");
}
try {
this.privateKey = Pass2Key.convertStringToPrivateKey(cipherText);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* Loads an RSA private key from disk, deriving the public key.
*
* @param privateKeyPath path to the private key PEM file
* @param password optional password for the encrypted private key
*/
public void loadKeys(String privateKeyPath, String password) {
loadKeys(privateKeyPath, null, password);
}
// Server Key Fetching
/**
* Fetches the server's RSA public key from the PKI endpoint.
*
* <p>Performs a GET request to {@code {routerUrl}/pki/public_key}
* and returns the server PEM public key as a string. Validates that
* the response parses as a valid PEM public key.</p>
*
* @return the server's PEM-encoded public key string
* @throws SecurityError if the URL is not HTTPS and {@code allowHttp} is {@code false},
* or if the response does not contain a valid PEM public key
*/
public CompletableFuture<String> fetchServerPublicKey() {
throw new UnsupportedOperationException("Not yet implemented");
}
// Encryption
/**
* Encrypts a payload dict using hybrid encryption.
*
* <p>Serializes the payload to JSON, then encrypts it using:
* <ol>
* <li>A per-request 256-bit AES key (AES-256-GCM)</li>
* <li>RSA-OAEP-SHA256 wrapping of the AES key with the server's public key</li>
* </ol>
* </p>
*
* @param payload the payload to encrypt (OpenAI-compatible chat parameters)
* @return raw encrypted bytes (JSON package serialized to bytes)
* @throws SecurityError if encryption fails or keys are not loaded
*/
public CompletableFuture<byte[]> encryptPayload(Map<String, Object> payload) {
throw new UnsupportedOperationException("Not yet implemented");
}
/**
* Core hybrid encryption routine.
*/
public CompletableFuture<byte[]> doEncrypt(byte[] payloadBytes, byte[] aesKey) {
throw new UnsupportedOperationException("Not yet implemented");
}
// Decryption
/**
* Decrypts a server response.
*/
public CompletableFuture<Map<String, Object>> decryptResponse(byte[] encryptedResponse, String payloadId) {
throw new UnsupportedOperationException("Not yet implemented");
}
// Secure Request Lifecycle
/**
* Full request lifecycle: encrypt HTTP POST retry decrypt return.
*
* <h3>Request Headers</h3>
* <pre>
* Content-Type: application/octet-stream
* X-Payload-ID: {payloadId}
* X-Public-Key: {urlEncodedPublicPemKey}
* Authorization: Bearer {apiKey} (if apiKey is provided)
* X-Security-Tier: {tier} (if securityTier is provided)
* </pre>
*
* <h3>POST</h3>
* {@code {routerUrl}/v1/chat/secure_completion} with encrypted payload as body.
*
* <h3>Retry Logic</h3>
* <ul>
* <li>Retryable status codes: {@code {429, 500, 502, 503, 504}}</li>
* <li>Backoff: {@code 2^(attempt-1)} seconds (1s, 2s, 4s)</li>
* <li>Total attempts: {@code maxRetries + 1}</li>
* <li>Network errors also retry</li>
* <li>Non-retryable exceptions propagate immediately</li>
* </ul>
*
* <h3>Status Exception Mapping</h3>
* <table>
* <tr><th>Status</th><th>Result</th></tr>
* <tr><td>200</td><td>Return decrypted response map</td></tr>
* <tr><td>400</td><td>{@code InvalidRequestError}</td></tr>
* <tr><td>401</td><td>{@code AuthenticationError}</td></tr>
* <tr><td>403</td><td>{@code ForbiddenError}</td></tr>
* <tr><td>404</td><td>{@code APIError}</td></tr>
* <tr><td>429</td><td>{@code RateLimitError}</td></tr>
* <tr><td>500</td><td>{@code ServerError}</td></tr>
* <tr><td>503</td><td>{@code ServiceUnavailableError}</td></tr>
* <tr><td>502/504</td><td>{@code APIError} (retryable)</td></tr>
* <tr><td>other</td><td>{@code APIError} (non-retryable)</td></tr>
* <tr><td>network error</td><td>{@code APIConnectionError}</td></tr>
* </table>
*
* @param payload the payload to send (OpenAI-compatible chat parameters)
* @param payloadId unique payload identifier
* @param apiKey optional API key for authentication
* @param securityTier optional security tier ({@code "standard"}, {@code "high"}, or {@code "maximum"})
* @return the decrypted response map
* @throws SecurityError if encryption/decryption fails
* @throws APIConnectionError if a network error occurs
* @throws InvalidRequestError if the API returns 400
* @throws AuthenticationError if the API returns 401
* @throws ForbiddenError if the API returns 403
* @throws RateLimitError if the API returns 429
* @throws ServerError if the API returns 500
* @throws ServiceUnavailableError if the API returns 503
* @throws APIError for other non-retryable errors
*/
public CompletableFuture<Map<String, Object>> sendSecureRequest(
Map<String, Object> payload,
String payloadId,
String apiKey,
String securityTier
) {
throw new UnsupportedOperationException("Not yet implemented");
}
/**
* Sends a secure request without a security tier.
*
* @param payload the payload to send
* @param payloadId unique payload identifier
* @param apiKey optional API key for authentication
* @return the decrypted response map
*/
public CompletableFuture<Map<String, Object>> sendSecureRequest(
Map<String, Object> payload,
String payloadId,
String apiKey
) {
return sendSecureRequest(payload, payloadId, apiKey, null);
}
/**
* Sends a secure request with no API key or security tier.
*
* @param payload the payload to send
* @param payloadId unique payload identifier
* @return the decrypted response map
*/
public CompletableFuture<Map<String, Object>> sendSecureRequest(
Map<String, Object> payload,
String payloadId
) {
return sendSecureRequest(payload, payloadId, null, null);
}
// Key Initialization
/**
* Ensures RSA keys are loaded or generated.
*
* <p>Uses double-checked locking via {@link ReentrantLock} to ensure
* thread-safe initialization. If {@code keyDir} is set, attempts to
* load keys from disk first; if that fails, generates new keys.</p>
*
* @param keyDir directory to persist keys, or {@code null} for ephemeral
*/
public void ensureKeys(String keyDir) {
if (keysInitialized) return;
keyInitLock.lock();
try {
if (keysInitialized) return;
// TODO: implement key loading/generation
keysInitialized = true;
} finally {
keyInitLock.unlock();
}
}
// Key Validation
/**
* Validates that an RSA key meets the minimum size requirement.
*
* @param key the RSA key to validate
* @throws SecurityError if the key size is less than {@link Constants#MIN_RSA_KEY_SIZE} bits
*/
public void validateRsaKey(PrivateKey key) throws SecurityError {
if (key == null) {
throw new SecurityError("RSA key is null");
}
int keySize = key.getEncoded() != null ? key.getEncoded().length * 8 : 0;
System.out.println("Keysize: " + keySize);
if (keySize < Constants.MIN_RSA_KEY_SIZE) {
throw new SecurityError(
"RSA key size " + keySize + " bits is below minimum " + Constants.MIN_RSA_KEY_SIZE + " bits"
);
}
}
// HTTP Status Exception Mapping
/**
* Maps an HTTP status code to the appropriate exception.
*/
public Exception mapHttpStatus(int statusCode, String responseBody) {
switch (statusCode) {
case 200:
return null;
case 400:
return new InvalidRequestError("Invalid request: " + (responseBody != null ? responseBody : "no body"));
case 401:
return new AuthenticationError("Authentication failed: " + (responseBody != null ? responseBody : "no body"));
case 403:
return new ForbiddenError("Access forbidden: " + (responseBody != null ? responseBody : "no body"));
case 404:
return new APIError("Not found: " + (responseBody != null ? responseBody : "no body"));
case 429:
return new RateLimitError("Rate limit exceeded: " + (responseBody != null ? responseBody : "no body"));
case 500:
return new ServerError("Internal server error: " + (responseBody != null ? responseBody : "no body"));
case 503:
return new ServiceUnavailableError("Service unavailable: " + (responseBody != null ? responseBody : "no body"));
case 502:
case 504:
return new APIError("Gateway error: " + (responseBody != null ? responseBody : "no body"));
default:
return new APIError("Unexpected status " + statusCode + ": " + (responseBody != null ? responseBody : "no body"));
}
}
// URL Encoding
/**
* URL-encodes a public key PEM string for use in the {@code X-Public-Key} header.
*/
public String urlEncodePublicKey(String pemKey) {
return java.net.URLEncoder.encode(pemKey, StandardCharsets.UTF_8);
}
// Getters
/**
* Closes the client and releases any resources.
*/
public void close() {
throw new UnsupportedOperationException("Not yet implemented");
}
}

View file

@ -0,0 +1,237 @@
package ai.nomyo;
import lombok.Getter;
import lombok.Setter;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.Map;
/**
* Cross-platform memory locking and secure zeroing utilities.
*
* <p>This module provides optional memory protection for sensitive
* cryptographic buffers. It fails gracefully if memory locking is
* unavailable on the current platform (e.g., Windows on some JVM
* configurations).</p>
*
* <h3>Protection Levels</h3>
* <ul>
* <li><b>"full"</b> Memory locking and secure zeroing both available</li>
* <li><b>"zeroing_only"</b> Only secure zeroing available</li>
* <li><b>"none"</b> No memory protection available</li>
* </ul>
*
* <h3>Usage</h3>
* <pre>{@code
* try (SecureBuffer buf = SecureMemory.secureBytearray(sensitiveData)) {
* // buf is locked and ready to use
* process(buf.getData());
* }
* // buf is automatically zeroed and unlocked on exit
* }</pre>
*/
public final class SecureMemory {
@Getter
@Setter
private static volatile boolean secureMemoryEnabled = true;
@Getter
private static final boolean HAS_MEMORY_LOCKING;
@Getter
private static final boolean HAS_SECURE_ZEROING;
static {
boolean locking = false;
boolean zeroing = false;
try {
locking = initMemoryLocking();
zeroing = true; // Secure zeroing is always available at the JVM level
} catch (Throwable t) {
// Degrade gracefully
}
HAS_MEMORY_LOCKING = locking;
HAS_SECURE_ZEROING = zeroing;
}
private static boolean initMemoryLocking() {
// FFM doesn't support memory locking at this point in time
// TODO: Bypass this with native libraries
return false;
}
/**
* Wraps a byte array with memory locking and guaranteed zeroing on exit.
*
* <p>Implements {@link AutoCloseable} for use with try-with-resources.
* The buffer is automatically zeroed and unlocked when closed, even
* if an exception occurs.</p>
*/
public static class SecureBuffer implements AutoCloseable {
private final Arena arena;
@Getter
private final MemorySegment data;
@Getter
private final long size;
@Getter
private final long address;
private boolean locked;
private boolean closed;
/**
* Creates a new SecureBuffer wrapping the given data.
*
* @param data the byte array to wrap
* @param lock whether to attempt memory locking
*/
public SecureBuffer(byte[] data, boolean lock) {
this.arena = Arena.ofConfined();
this.data = data != null ? this.arena.allocate(data.length) : MemorySegment.NULL;
if (data != null) {
this.data.asByteBuffer().put(data);
}
this.size = this.data.byteSize();
this.address = this.data.address();
this.locked = false;
this.closed = false;
if (lock && SecureMemory.secureMemoryEnabled) {
this.locked = lock();
}
}
/**
* Attempts to lock the buffer in memory, preventing swapping to disk.
*
* @return {@code true} if locking succeeded, {@code false} otherwise
*/
public boolean lock() {
//data = data.asReadOnly();
return false;
}
/**
* Unlocks the buffer, allowing it to be swapped to disk.
*
* @return {@code true} if unlocking succeeded, {@code false} otherwise
*/
public boolean unlock() {
if (!locked) return false;
locked = false;
return false;
}
/**
* Securely zeros the buffer contents.
*/
public void zero() {
if (data != null) {
data.fill((byte) 0);
}
}
@Override
public void close() {
if (closed) return;
zero();
unlock();
arena.close();
closed = true;
}
}
/**
* Creates a SecureBuffer for the given data with memory locking.
*
* <p>This is the recommended way to handle sensitive data. The returned
* buffer should be used within a try-with-resources block to ensure
* secure zeroing on exit.</p>
*
* @param data the sensitive data bytes
* @param lock whether to attempt memory locking
* @return a new SecureBuffer
*/
public static SecureBuffer secureByteArray(byte[] data, boolean lock) {
return new SecureBuffer(data, lock);
}
/**
* Creates a SecureBuffer for the given data with memory locking.
* Convenience variant that always attempts locking.
*
* @param data the sensitive data bytes
* @return a new SecureBuffer
*/
public static SecureBuffer secureByteArray(byte[] data) {
return secureByteArray(data, true);
}
/**
* Creates a SecureBuffer for the given data with memory locking.
*
* <p><b>Deprecated:</b> Use {@link #secureByteArray(byte[])} instead.
* This method exists for compatibility with the Python reference.</p>
*
* @param data the sensitive data bytes
* @param lock whether to attempt memory locking
* @return a new SecureBuffer
* @deprecated Use {@link #secureByteArray(byte[])} instead
*/
@Deprecated
public static SecureBuffer secureBytes(byte[] data, boolean lock) {
return new SecureBuffer(data, lock);
}
/**
* Creates a SecureBuffer for the given data with memory locking.
*
* <p><b>Deprecated:</b> Use {@link #secureByteArray(byte[])} instead.
* This method exists for compatibility with the Python reference.</p>
*
* @param data the sensitive data bytes
* @return a new SecureBuffer
* @deprecated Use {@link #secureByteArray(byte[])} instead
*/
@Deprecated
public static SecureBuffer secureBytes(byte[] data) {
return secureBytes(data, true);
}
/**
* Returns information about the current memory protection capabilities.
*
* @return a map of protection capabilities
*/
public static Map<String, Object> getMemoryProtectionInfo() {
String protectionLevel;
if (HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING) {
protectionLevel = "full";
} else if (HAS_SECURE_ZEROING) {
protectionLevel = "zeroing_only";
} else {
protectionLevel = "none";
}
boolean supportsFull = HAS_MEMORY_LOCKING && HAS_SECURE_ZEROING && secureMemoryEnabled;
return Map.of(
"enabled", secureMemoryEnabled,
"protection_level", protectionLevel,
"has_memory_locking", HAS_MEMORY_LOCKING,
"has_secure_zeroing", HAS_SECURE_ZEROING,
"supports_full_protection", supportsFull,
"page_size", Constants.PAGE_SIZE
);
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
/**
* Exception thrown when a network failure occurs during communication
* with the NOMYO API server.
*
* <p>This includes connection timeouts, DNS resolution failures,
* TLS handshake failures, and other network-level errors. Unlike
* {@link APIError}, this exception does not carry an HTTP status code.</p>
*/
public class APIConnectionError extends Exception {
/**
* Constructs an {@code APIConnectionError} with the specified detail message.
*
* @param message the detail message
*/
public APIConnectionError(String message) {
super(message);
}
/**
* Constructs an {@code APIConnectionError} with the specified detail message
* and cause.
*
* @param message the detail message
* @param cause the cause of this exception, or {@code null}
*/
public APIConnectionError(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs an {@code APIConnectionError} with the specified cause.
*
* @param cause the cause of this exception
*/
public APIConnectionError(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,59 @@
package ai.nomyo.errors;
import java.util.Collections;
import java.util.Map;
/**
* Base exception for all NOMYO API errors.
*
* <p>All API error subclasses carry a {@code status_code} and optional
* {@code error_details} from the server response.</p>
*/
public class APIError extends Exception {
private final Integer statusCode;
private final Map<String, Object> errorDetails;
/**
* Constructs an {@code APIError} with the specified detail message,
* status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code, or {@code null} if not applicable
* @param errorDetails additional error details from the server, or {@code null}
*/
public APIError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message);
this.statusCode = statusCode;
this.errorDetails = errorDetails != null
? Collections.unmodifiableMap(errorDetails)
: Collections.emptyMap();
}
/**
* Constructs an {@code APIError} with the specified detail message.
*
* @param message the detail message
*/
public APIError(String message) {
this(message, null, null);
}
/**
* Returns the HTTP status code associated with this error, or {@code null}.
*
* @return the status code, or {@code null}
*/
public Integer getStatusCode() {
return statusCode;
}
/**
* Returns an unmodifiable map of additional error details from the server.
*
* @return the error details map (never {@code null})
*/
public Map<String, Object> getErrorDetails() {
return errorDetails;
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
import java.util.Map;
/**
* Exception thrown when the API returns a 401 (Unauthorized) status.
*/
public class AuthenticationError extends APIError {
/**
* Constructs an {@code AuthenticationError} with the specified detail message,
* status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code (must be 401)
* @param errorDetails additional error details from the server, or {@code null}
*/
public AuthenticationError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message, statusCode, errorDetails);
}
/**
* Constructs an {@code AuthenticationError} with the specified detail message.
*
* @param message the detail message
*/
public AuthenticationError(String message) {
super(message, 401, null);
}
/**
* Constructs an {@code AuthenticationError} with the specified detail message
* and error details.
*
* @param message the detail message
* @param errorDetails additional error details from the server, or {@code null}
*/
public AuthenticationError(String message, Map<String, Object> errorDetails) {
super(message, 401, errorDetails);
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
import java.util.Map;
/**
* Exception thrown when the API returns a 403 (Forbidden) status.
*/
public class ForbiddenError extends APIError {
/**
* Constructs a {@code ForbiddenError} with the specified detail message,
* status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code (must be 403)
* @param errorDetails additional error details from the server, or {@code null}
*/
public ForbiddenError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message, statusCode, errorDetails);
}
/**
* Constructs a {@code ForbiddenError} with the specified detail message.
*
* @param message the detail message
*/
public ForbiddenError(String message) {
super(message, 403, null);
}
/**
* Constructs a {@code ForbiddenError} with the specified detail message
* and error details.
*
* @param message the detail message
* @param errorDetails additional error details from the server, or {@code null}
*/
public ForbiddenError(String message, Map<String, Object> errorDetails) {
super(message, 403, errorDetails);
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
import java.util.Map;
/**
* Exception thrown when the API returns a 400 (Bad Request) status.
*/
public class InvalidRequestError extends APIError {
/**
* Constructs an {@code InvalidRequestError} with the specified detail message,
* status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code (must be 400)
* @param errorDetails additional error details from the server, or {@code null}
*/
public InvalidRequestError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message, statusCode, errorDetails);
}
/**
* Constructs an {@code InvalidRequestError} with the specified detail message.
*
* @param message the detail message
*/
public InvalidRequestError(String message) {
super(message, 400, null);
}
/**
* Constructs an {@code InvalidRequestError} with the specified detail message
* and error details.
*
* @param message the detail message
* @param errorDetails additional error details from the server, or {@code null}
*/
public InvalidRequestError(String message, Map<String, Object> errorDetails) {
super(message, 400, errorDetails);
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
import java.util.Map;
/**
* Exception thrown when the API returns a 429 (Too Many Requests) status.
*/
public class RateLimitError extends APIError {
/**
* Constructs a {@code RateLimitError} with the specified detail message,
* status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code (must be 429)
* @param errorDetails additional error details from the server, or {@code null}
*/
public RateLimitError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message, statusCode, errorDetails);
}
/**
* Constructs a {@code RateLimitError} with the specified detail message.
*
* @param message the detail message
*/
public RateLimitError(String message) {
super(message, 429, null);
}
/**
* Constructs a {@code RateLimitError} with the specified detail message
* and error details.
*
* @param message the detail message
* @param errorDetails additional error details from the server, or {@code null}
*/
public RateLimitError(String message, Map<String, Object> errorDetails) {
super(message, 429, errorDetails);
}
}

View file

@ -0,0 +1,34 @@
package ai.nomyo.errors;
/**
* Exception thrown for cryptographic and key-related failures.
*
* <p>Unlike {@link APIError}, this exception carries no HTTP status code.
* It is used for issues such as invalid key sizes, encryption/decryption
* failures, and missing keys.</p>
*
* <p>Any decryption failure (except JSON parse errors) raises
* {@code SecurityError("Decryption failed: integrity check or authentication failed")}.</p>
*/
public class SecurityError extends Exception {
/**
* Constructs a {@code SecurityError} with the specified detail message.
*
* @param message the detail message
*/
public SecurityError(String message) {
super(message);
}
/**
* Constructs a {@code SecurityError} with the specified detail message
* and cause.
*
* @param message the detail message
* @param cause the underlying cause, or {@code null}
*/
public SecurityError(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
import java.util.Map;
/**
* Exception thrown when the API returns a 500 (Internal Server Error) status.
*/
public class ServerError extends APIError {
/**
* Constructs a {@code ServerError} with the specified detail message,
* status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code (must be 500)
* @param errorDetails additional error details from the server, or {@code null}
*/
public ServerError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message, statusCode, errorDetails);
}
/**
* Constructs a {@code ServerError} with the specified detail message.
*
* @param message the detail message
*/
public ServerError(String message) {
super(message, 500, null);
}
/**
* Constructs a {@code ServerError} with the specified detail message
* and error details.
*
* @param message the detail message
* @param errorDetails additional error details from the server, or {@code null}
*/
public ServerError(String message, Map<String, Object> errorDetails) {
super(message, 500, errorDetails);
}
}

View file

@ -0,0 +1,41 @@
package ai.nomyo.errors;
import java.util.Map;
/**
* Exception thrown when the API returns a 503 (Service Unavailable) status.
*/
public class ServiceUnavailableError extends APIError {
/**
* Constructs a {@code ServiceUnavailableError} with the specified detail
* message, status code, and error details.
*
* @param message the detail message
* @param statusCode the HTTP status code (must be 503)
* @param errorDetails additional error details from the server, or {@code null}
*/
public ServiceUnavailableError(String message, Integer statusCode, Map<String, Object> errorDetails) {
super(message, statusCode, errorDetails);
}
/**
* Constructs a {@code ServiceUnavailableError} with the specified detail message.
*
* @param message the detail message
*/
public ServiceUnavailableError(String message) {
super(message, 503, null);
}
/**
* Constructs a {@code ServiceUnavailableError} with the specified detail message
* and error details.
*
* @param message the detail message
* @param errorDetails additional error details from the server, or {@code null}
*/
public ServiceUnavailableError(String message, Map<String, Object> errorDetails) {
super(message, 503, errorDetails);
}
}

View file

@ -0,0 +1,24 @@
package ai.nomyo.util;
import java.util.Base64;
/**
* @author NieGestorben
* Copyright© (c) 2026, All Rights Reserved.
*/
public class PEMConverter {
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());
}
publicKeyFormatted.append(privateKey ? "-----END PRIVATE KEY-----" : "-----END PUBLIC KEY-----");
return publicKeyFormatted.toString();
}
}

View file

@ -0,0 +1,202 @@
package ai.nomyo.util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
/**
* Password-based encryption utility using PBKDF2 key derivation and AES-GCM encryption.
*
* <p>The encrypted output format stores salt + IV + ciphertext in that order,
* all base64-encoded. This ensures the salt and IV are persisted alongside
* the ciphertext for decryption.</p>
*
* <h3>Binary Layout</h3>
* <pre>
* [ 16 bytes salt ][ 12 bytes IV ][ variable bytes ciphertext ]
* </pre>
*/
public final class Pass2Key {
private static final SecureRandom RANDOM = new SecureRandom();
private static final int SALT_LENGTH = 16;
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
private static final int ITERATION_COUNT = 65536;
private Pass2Key() {}
/**
* Encrypts the given plaintext using the specified algorithm and password.
*
* @param algorithm the cipher algorithm (e.g. {@code "AES/GCM/NoPadding"})
* @param input the plaintext to encrypt
* @param password the password used to derive the encryption key
* @return base64-encoded ciphertext including salt and IV
*/
public static String encrypt(String algorithm, String input, String password)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
byte[] salt = generateRandomBytes(SALT_LENGTH);
SecretKey key = deriveKey(password, salt);
byte[] payload;
if (isGcmMode(algorithm)) {
byte[] iv = generateRandomBytes(GCM_IV_LENGTH);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
byte[] ciphertext = encryptWithCipher(algorithm, key, spec, input);
payload = assemblePayloadGcm(salt, iv, ciphertext);
} else {
byte[] ciphertext = encryptWithCipher(algorithm, key, input);
payload = assemblePayloadSalt(salt, ciphertext);
}
return Base64.getEncoder().encodeToString(payload);
}
/**
* Decrypts the given base64-encoded ciphertext using the specified algorithm and password.
*
* @param algorithm the cipher algorithm (e.g. {@code "AES/GCM/NoPadding"})
* @param cipherText the base64-encoded ciphertext (with embedded salt and IV)
* @param password the password used to derive the decryption key
* @return the decrypted plaintext
*/
public static String decrypt(String algorithm, String cipherText, String password)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
byte[] decoded = Base64.getDecoder().decode(cipherText);
byte[] salt = new byte[SALT_LENGTH];
System.arraycopy(decoded, 0, salt, 0, SALT_LENGTH);
SecretKey key = deriveKey(password, salt);
if (isGcmMode(algorithm)) {
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(decoded, SALT_LENGTH, iv, 0, GCM_IV_LENGTH);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
byte[] ciphertext = copyFrom(decoded, SALT_LENGTH + GCM_IV_LENGTH);
return decryptWithCipher(algorithm, key, spec, ciphertext);
} else {
byte[] ciphertext = copyFrom(decoded, SALT_LENGTH);
return decryptWithCipher(algorithm, key, ciphertext);
}
}
// Key Derivation
private static SecretKey deriveKey(String password, byte[] salt) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, 256);
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException("Could not derive key: " + e.getMessage());
}
}
// Cipher Operations
private static byte[] encryptWithCipher(String algorithm, SecretKey key,
GCMParameterSpec spec, String input)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
return cipher.doFinal(input.getBytes());
}
private static byte[] encryptWithCipher(String algorithm, SecretKey key, String input)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(input.getBytes());
}
private static String decryptWithCipher(String algorithm, SecretKey key,
GCMParameterSpec spec, byte[] ciphertext)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
}
private static String decryptWithCipher(String algorithm, SecretKey key, byte[] ciphertext)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
}
// Helpers
private static boolean isGcmMode(String algorithm) {
return algorithm.contains("GCM");
}
private static byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
RANDOM.nextBytes(bytes);
return bytes;
}
private static byte[] copyFrom(byte[] source, int offset) {
return java.util.Arrays.copyOfRange(source, offset, source.length);
}
private static byte[] assemblePayloadGcm(byte[] salt, byte[] iv, byte[] ciphertext) {
ByteBuffer buffer = ByteBuffer.allocate(salt.length + iv.length + ciphertext.length);
buffer.put(salt).put(iv).put(ciphertext);
return buffer.array();
}
private static byte[] assemblePayloadSalt(byte[] salt, byte[] ciphertext) {
ByteBuffer buffer = ByteBuffer.allocate(salt.length + ciphertext.length);
buffer.put(salt).put(ciphertext);
return buffer.array();
}
public static PrivateKey convertStringToPrivateKey(String privateKeyString) throws Exception {
// Remove any header and footer information if present
privateKeyString = privateKeyString.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
// Decode the Base64-encoded private key string
byte[] decodedKey = Base64.getDecoder().decode(privateKeyString);
// Create a PKCS8EncodedKeySpec object
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
// Get an instance of the KeyFactory for the desired algorithm (e.g., RSA)
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// Generate the private key object
return keyFactory.generatePrivate(keySpec);
}
}

View file

@ -0,0 +1,37 @@
package ai.nomyo.util;
import java.util.ArrayList;
import java.util.List;
/**
* @author NieGestorben
* Copyright© (c) 2026, All Rights Reserved.
*/
public class Splitter {
public static List<String> fixedLengthString(int length, String toSplit) {
List<String> returnList = new ArrayList<>();
int remaining = toSplit.length();
while (remaining > 0) {
int currentIndex = toSplit.length() - remaining;
int endIndex = toSplit.length() - remaining + length;
// If there are not enough characters left to create a new substring of the given length, create one with the remaining characters
if (remaining < length) {
endIndex = toSplit.length();
}
String split = toSplit.substring(currentIndex, endIndex);
returnList.add(split);
remaining -= length;
}
return returnList;
}
}