Metadata-Version: 2.4
Name: ENCRYPTION-SERVICE
Version: 1.7.10
Summary: AES-256-CTR encryption/decryption utilities with deterministic IV generation for cross-language interoperability
Author-email: rocky <rocky@null.net>
License: MIT
Project-URL: Homepage, https://github.com/rocky/ENCRYPTION-SERVICE
Project-URL: Bug Reports, https://github.com/rocky/ENCRYPTION-SERVICE/issues
Project-URL: Source, https://github.com/rocky/ENCRYPTION-SERVICE
Keywords: encryption,decryption,aes-256,ctr,cryptography,cross-language
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42.0.8
Requires-Dist: python-dotenv>=1.2.2
Provides-Extra: dev
Requires-Dist: pytest>=9.0.2; extra == "dev"
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
Dynamic: license-file

# ENCRYPTION_SERVICE

AES-256-CTR encryption/decryption utilities with deterministic IV generation for cross-language interoperability.

## Installation

```bash
pip install ENCRYPTION_SERVICE
```

## Quick Start

### Set environment variables in .env
```bash
ENCRYPTION_KEY=your-secret-phrase   # MUST be ≥ 32 bytes, cryptographically random
```

### Import and use
```python
from ENCRYPTION_SERVICE import Aes256CtrEncryption

# Encrypt plaintext to base64
token = Aes256CtrEncryption.encrypt("s3cr3t")   # → "XXXXX..."

# Decrypt back to plaintext
plain = Aes256CtrEncryption.decrypt(token)      # → "s3cr3t"

# Decrypt a nested field in JSON
payload = {"db": {"pass": Aes256CtrEncryption.encrypt("hunter2")}}
pw = Aes256CtrEncryption.decrypt_json_field(payload, "db.pass")  # → "hunter2"
```

⚠️ **SECURITY**: `ENCRYPTION_KEY` must be ≥ 32 bytes of cryptographically random data.
Never use a human-chosen password. A warning fires on first use if key is too short.

## Configuration

### Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `ENCRYPTION_KEY` | Yes | — | AES-256 key material (≥ 32 bytes, cryptographically random) |

## API Reference

### Aes256CtrEncryption.encrypt(plaintext: str) -> str
Encrypts arbitrary plaintext to a base64 string.
- `plaintext`: String to encrypt
- Returns: Base64-encoded ciphertext

### Aes256CtrEncryption.decrypt(encoded_ciphertext: str) -> str
Decrypts base64-encoded ciphertext back to plaintext.
- `encoded_ciphertext`: Base64-encoded ciphertext from encrypt()
- Returns: Original plaintext string
- Raises: `ValueError` if ciphertext is malformed (shorter than 16 bytes)

### Aes256CtrEncryption.decrypt_json_field(encrypted_json: dict, json_path: str) -> str
Decrypts a single nested field in a JSON object.
- `encrypted_json`: Dictionary containing encrypted fields
- `json_path`: Dot-notation path (e.g. "user.credentials.password")
- Returns: Decrypted plaintext string
- Raises: `ValueError` if path doesn't exist or points to non-string

## Key Features

1. **Deterministic IV Generation** — Same plaintext always produces same ciphertext (cross-language compatible)
2. **AES-256-CTR Mode** — High-performance encryption with no padding issues
3. **SHA-256 Key Derivation** — Compatible with Rust sibling implementation
4. **Retry with Exponential Backoff + Jitter** — 3 attempts with exponential backoff and ±25% jitter
5. **Lazy Config Loading** — No side effects on import; validated on first use
6. **Pure Cryptographic Functions** — No side effects, thread-safe, deterministic

## Security Notes

- **Key Requirements**: Use cryptographically random keys ≥ 32 bytes. SHA-256 derivation provides no brute-force resistance.
- **Deterministic Encryption**: Identical plaintexts produce identical ciphertexts — acceptable for this use case.
- **Cross-Language Interop**: IV derivation matches Rust implementation exactly.

## Performance

```
┌──────────────────────┬──────────┬──────────┬─────────────┬──────────┬──────────┐
│ Test                 │ Ops      │ Time     │ Throughput  │ Target   │ Status   │
├──────────────────────┼──────────┼──────────┼─────────────┼──────────┼──────────┤
│ Encrypt (1-thread)   │  1,000   │ 0.038 s  │ 26,253/sec  │ 10k/sec  │ ✅ PASS  │
│ Decrypt (1-thread)   │  1,000   │ 0.028 s  │ 35,920/sec  │ 10k/sec  │ ✅ PASS  │
│ Round-trip           │    100   │ 0.058 ms │    avg/call │  < 1 ms  │ ✅ PASS  │
│ JSON field decrypt   │    500   │ 0.014 s  │ 37,171/sec  │ 10k/sec  │ ✅ PASS  │
│ Encrypt (4-thread)   │  1,000   │ 0.092 s  │ 11,052/sec  │ 10k/sec  │ ✅ PASS  │
│ Encrypt (8-thread)   │  2,000   │ 0.197 s  │ 10,163/sec  │ 10k/sec  │ ✅ PASS  │
└──────────────────────┴──────────┴──────────┴─────────────┴──────────┴──────────┘
System: Ubuntu 24.04.4 LTS | AMD Ryzen 7 5800H (16 cores, 13Gi RAM) | Python 3.12.3
Tested: 2026-04-03 16:56 UTC
```

**GIL note:** Multi-thread throughput is limited by Python's GIL — AES-CTR is CPU-bound.
Use `multiprocessing` if CPU-bound parallelism is required.

## Error Handling

The library will raise `ValueError` if:
- `ENCRYPTION_KEY` is not set (raised on first crypto call, not at import)
- Ciphertext is shorter than 16 bytes (truncated IV)
- JSON path does not exist in the provided dict
- JSON path resolves to a non-string leaf (not an encrypted value)

`UserWarning` fires on first use if `ENCRYPTION_KEY` is shorter than 32 bytes.

## Retry Policy

All cryptographic operations use `@with_retry` decorator:
- **Max attempts**: 3
- **Delay**: Exponential backoff (1s, 2s, 4s) with ±25% jitter
- **Retryable**: All exceptions (local crypto has no 4xx-equivalent)
- **Logging**: WARNING per attempt, ERROR on exhaustion

## Test Coverage

```bash
python3 -m pytest ENCRYPTION_SERVICE.py -v
```

| Function | Tier | Tests | What is tested |
|----------|------|-------|----------------|
| `encrypt()` | 1 | 4 | non-empty base64, determinism, different inputs, empty string |
| `decrypt()` | 1 | 4 | round-trip, empty string, too-short input, unicode |
| `decrypt_json_field()` | 1 | 4 | nested path, single-level, missing key, wrong type |
| `with_retry()` | 2 | 4 | first-attempt success, third-attempt success, exhaustion, all exception types |
| `_derive_256bit_key()` | 2 | 3 | 32 bytes output, determinism, different passwords |
| `_generate_deterministic_iv()` | 2 | 3 | 16 bytes output, determinism, different plaintexts |
| `Import validation` | 3 | 1 | missing key raises, short key warns |
