From fb46e4226262f52b64732808a489130f2dde841b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:13:19 +0000 Subject: [PATCH 1/3] Initial plan From e3a8d30440f437a4b64b4a2b2d8db2865aba8e06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:15:23 +0000 Subject: [PATCH 2/3] Initial plan checklist Agent-Logs-Url: https://github.com/ropeadope62/zeroenv/sessions/83f58a8b-035d-4ee9-85d3-846d84e2611b Co-authored-by: ropeadope62 <26491441+ropeadope62@users.noreply.github.com> --- src/zeroenv.egg-info/PKG-INFO | 534 ++++++++++++++++++ src/zeroenv.egg-info/SOURCES.txt | 17 + src/zeroenv.egg-info/dependency_links.txt | 1 + src/zeroenv.egg-info/entry_points.txt | 2 + src/zeroenv.egg-info/requires.txt | 3 + src/zeroenv.egg-info/top_level.txt | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 335 bytes .../__pycache__/crypto.cpython-312.pyc | Bin 0 -> 6937 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 343 bytes .../test_crypto.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 30058 bytes 10 files changed, 558 insertions(+) create mode 100644 src/zeroenv.egg-info/PKG-INFO create mode 100644 src/zeroenv.egg-info/SOURCES.txt create mode 100644 src/zeroenv.egg-info/dependency_links.txt create mode 100644 src/zeroenv.egg-info/entry_points.txt create mode 100644 src/zeroenv.egg-info/requires.txt create mode 100644 src/zeroenv.egg-info/top_level.txt create mode 100644 src/zeroenv/__pycache__/__init__.cpython-312.pyc create mode 100644 src/zeroenv/__pycache__/crypto.cpython-312.pyc create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_crypto.cpython-312-pytest-9.0.3.pyc diff --git a/src/zeroenv.egg-info/PKG-INFO b/src/zeroenv.egg-info/PKG-INFO new file mode 100644 index 0000000..40b1ab9 --- /dev/null +++ b/src/zeroenv.egg-info/PKG-INFO @@ -0,0 +1,534 @@ +Metadata-Version: 2.4 +Name: zeroenv +Version: 0.1.0 +Summary: Simple, secure, and git-safe secrets management.. +Author-email: "ropeadope62 (Dave C.)" +Project-URL: Homepage, https://github.com/ropeadope62/zeroenv +Project-URL: Bug Tracker, https://github.com/ropeadope62/zeroenv/issues +Classifier: Development Status :: 2 - Pre-Alpha +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Topic :: Security :: Cryptography +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Classifier: Environment :: Console +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: click +Requires-Dist: cryptography +Requires-Dist: rich +Dynamic: license-file + +

+
+ ZeroEnv +

+ +

Simple, secure, and git-safe secrets management.

+ +

+ + PyPI version + + + + + + + + + Tests + +

+ +

+ Features • + Installation • + Usage • + How It Works • + Team Workflow • + Docker • + Testing • + Security • + Command Reference +

+ +![demo](./assets/zeroenv_example.gif) + +## Features + +* AES-256-GCM encryption with unique nonces +* Git-safe encrypted storage (commit `.secrets`, not keys) +* Zero-config initialization +* No code changes required (injects env vars) +* No infrastructure dependencies +* Cross-platform (Windows, macOS, Linux) +* Export to `.env` format + +## Installation + +```bash +pip install zeroenv +``` + +From source: +```bash +git clone https://github.com/ropeadope62/zeroenv +cd zeroenv +pip install -e . +``` + +## Usage + +### Initialize + +```bash +zeroenv init # Standard tier (fast, development) +zeroenv init --tier enhanced # Enhanced security (100k iterations) +zeroenv init --tier max # Maximum security (500k iterations, production) +``` + +Creates `.secrets` (encrypted) and `.secrets.key` (master key). Automatically updates `.gitignore`. + +**Security Tiers:** +- **Standard**: Direct key usage, fastest performance (development) +- **Enhanced**: PBKDF2-SHA256 with 100k iterations (balanced security/performance) +- **Max**: PBKDF2-SHA256 with 500k iterations (production environments) + +### Add Secrets + +```bash +zeroenv add DATABASE_URL "postgresql://localhost/mydb" +zeroenv add API_KEY "sk_test_1234567890" +``` + +### Run Commands + +```bash +zeroenv run python app.py +zeroenv run npm start +zeroenv run dotnet run +``` + +Secrets are injected as environment variables. Your code remains unchanged: + +```python +import os +api_key = os.getenv('API_KEY') +``` + +### List Secrets + +```bash +zeroenv ls # Names only +zeroenv ls --values # Show values +``` + +### Get Secret + +```bash +zeroenv get API_KEY +``` + +### Export + +```bash +zeroenv export # .env format +zeroenv export > .env # Save to file +zeroenv export -f json # JSON format +``` + +### Remove Secret + +```bash +zeroenv rm SECRET_NAME +``` + +### View Configuration + +```bash +zeroenv info +``` + +Displays: +- Security tier and description +- PBKDF2 iteration count +- Encryption algorithm (AES-256-GCM) +- Number of secrets stored +- File locations + +## How It Works + +### Two-File System + +**`.secrets`** - Encrypted secrets (safe to commit) +```json +{ + "version": "1.0", + "security_tier": "enhanced", + "salt": "xY9zA1bC2dE3fG4h...", + "secrets": { + "API_KEY": { + "ciphertext": "aB3dE5fG7hI9jK1...", + "nonce": "pQ6rS8tU0vW1xY2z..." + } + } +} +``` + +**`.secrets.key`** - Master key (never commit, auto-gitignored) +``` +aB3dE5fG7hI9jK1lM2nO4pQ6rS8tU0vW1xY2zA3bC4dE5f= +``` + +### Encryption + +- **Algorithm:** AES-256-GCM +- **Key size:** 256 bits +- **Nonce size:** 96 bits (unique per secret) +- **Authenticated encryption:** Tamper-proof +- **Key derivation:** PBKDF2-HMAC-SHA256 (enhanced/max tiers) +- **KDF iterations:** 100k (enhanced) or 500k (max) + +The encryption process varies by security tier: +- **Standard:** Master key → AES-256-GCM encryption +- **Enhanced/Max:** Master key + salt → PBKDF2 → Derived key → AES-256-GCM encryption + +## Team Usage + +While ZeroEnv was designed with individual developers in mind, usage in small teams can be achieved by sharing the master key. + +### Setup + +```bash +# Developer 1 +zeroenv init +zeroenv add API_KEY "shared-key" +git add .secrets .gitignore +git commit -m "Add encrypted secrets" +git push +``` + +### Join Team + +```bash +# Developer 2 +git clone repo +cd repo +echo "MASTER_KEY" > .secrets.key # Received via secure channel +zeroenv ls # Verify +``` + +Encrypted `.secrets` syncs via git. Master key shared once through secure channel (password manager, encrypted messaging). + +## CI/CD + +Set `ZEROENV_MASTER_KEY` environment variable: + +### GitHub Actions +```yaml +- run: zeroenv run pytest + env: + ZEROENV_MASTER_KEY: ${{ secrets.ZEROENV_MASTER_KEY }} +``` + +### GitLab CI +```yaml +test: + script: + - zeroenv run pytest +``` + +### Azure Pipelines + +```yaml +- script: zeroenv run pytest + env: + ZEROENV_MASTER_KEY: $(ZEROENV_MASTER_KEY) +``` +## Containerization / Docker + +ZeroEnv works seamlessly with Docker. You can inject secrets into your container at runtime without baking them into the image. + +### Dockerfile + +Install `zeroenv` in your image: + +```dockerfile +FROM python:3.9-slim + +WORKDIR /app +COPY . . +RUN pip install zeroenv +RUN pip install -r requirements.txt + +# Don't set entrypoint here if you want to use zeroenv at runtime +CMD ["python", "app.py"] +``` + +### Docker Compose + +Mount the `.secrets` file and the master key (via environment variable): + +```yaml +version: '3' +services: + web: + build: . + volumes: + - ./.secrets:/app/.secrets:ro # Mount secrets file read-only + environment: + - ZEROENV_MASTER_KEY=${ZEROENV_MASTER_KEY} # Pass key from host + command: zeroenv run python app.py +``` + +### Running with Docker + +1. Export the master key on your host: + ```bash + export ZEROENV_MASTER_KEY=$(cat .secrets.key) + ``` + +2. Run the container: + ```bash + docker-compose up + ``` +``` + +## Examples + +### Basic Usage + +```bash +# Initialize with enhanced security +zeroenv init --tier enhanced + +# Check configuration +zeroenv info +# Output: +# ╭─────────────── ZeroEnv Configuration ───────────────╮ +# │ Security Tier: Enhanced │ +# │ PBKDF2 Iterations: 100,000 iterations │ +# │ Encryption: AES-256-GCM │ +# │ Secrets Stored: 0 │ +# ╰─────────────────────────────────────────────────────╯ + +# Add secrets +zeroenv add API_KEY "sk_test_1234" +zeroenv add DATABASE_URL "postgresql://localhost/db" + +# Verify +zeroenv ls --values +``` + +### Django +```bash +zeroenv init +zeroenv add SECRET_KEY "django-key" +zeroenv add DATABASE_URL "postgresql://..." +zeroenv run python manage.py runserver +``` + +### Node.js +```bash +zeroenv init +zeroenv add PORT "3000" +zeroenv add MONGODB_URI "mongodb://..." +zeroenv run npm start +``` + +### .NET +```bash +zeroenv init +zeroenv add ConnectionStrings__Default "Server=..." +zeroenv run dotnet run +``` + +## Security + +### Encryption Details + +- **Algorithm:** AES-256-GCM (AEAD) +- **Key derivation:** 256-bit random key via `os.urandom()` +- **Nonce generation:** 96-bit random nonce per encryption +- **Library:** Python `cryptography` package + +### Security Tiers + +ZeroEnv offers three security tiers to balance performance and security: + +#### Standard (Default) +- **Iterations:** 0 (direct key usage) +- **Performance:** Fastest +- **Use case:** Development, local testing +- **Security:** Strong encryption with AES-256-GCM + +```bash +zeroenv init # or explicitly: zeroenv init --tier standard +``` + +#### Enhanced +- **Iterations:** 100,000 (PBKDF2-HMAC-SHA256) +- **Performance:** Moderate (~100ms key derivation) +- **Use case:** Shared projects, small teams +- **Security:** Resistant to brute-force attacks on weak keys + +```bash +zeroenv init --tier enhanced +``` + +#### Max +- **Iterations:** 500,000 (PBKDF2-HMAC-SHA256) +- **Performance:** Slower (~500ms key derivation) +- **Use case:** Production environments, sensitive data +- **Security:** Maximum protection against brute-force attacks + +```bash +zeroenv init --tier max +``` + +**Note:** Security tier is set during initialization and stored in `.secrets`. All subsequent operations (add, get, run, export) automatically use the configured tier. Existing secrets files without a tier default to `standard` for backward compatibility. +- **Nonce generation:** 96-bit random nonce per encryption +- **Library:** Python `cryptography` package + +## Command Reference + +| Command | Description | +|---------|-------------| +| `init [--tier TIER]` | Initialize ZeroEnv in project (standard/enhanced/max) | +| `add NAME [VALUE]` | Add or update secret | +| `get NAME` | Retrieve secret value | +| `ls [--values]` | List secrets | +| `rm NAME` | Remove secret | +| `run COMMAND` | Run command with secrets | +| `export [-f FORMAT]` | Export secrets | +| `info` | Display security tier and configuration | + +## Dependencies + +- Python 3.10+ +- `click` - CLI framework +- `rich` - Terminal formatting +- `cryptography` - AES-256-GCM implementation + +## Testing + +This directory contains comprehensive unit tests to ensure reliability and security. + +## Why Testing is Critical + +I have added some unit tests to validate the core functionality of ZeroEnv, including the cryptography and storage, Data Integrity, CLI Reliability, Edge Case Handling, Regression Prevention, and Cross-Platform Compatibility. + +## Prerequisites + +The tests require `pytest` and `click`. You can install the development dependencies with: + +```bash +pip install pytest click cryptography rich +``` + +## Running Tests + +To run all tests, execute the following command from the project root: + +```bash +python -m pytest tests +``` + +To run tests with verbose output: + +```bash +python -m pytest tests -v +``` + +To run with coverage report: + +```bash +python -m pytest tests --cov=zeroenv --cov-report=html +``` + +## Test Structure + +### `test_crypto.py` - Cryptographic Operations +Tests the core security layer: +- **Key Generation**: Validates 256-bit keys are properly generated +- **Encryption**: Ensures plaintext is encrypted correctly with unique nonces +- **Decryption**: Verifies encrypted data can be decrypted back to original +- **Key Encoding**: Tests base64 encoding/decoding of keys +- **Invalid Inputs**: Tests behavior with malformed data or wrong keys +- **Nonce Uniqueness**: Ensures each encryption uses a unique nonce + +**Critical Tests:** +- Different values produce different ciphertexts +- Same value encrypted twice produces different ciphertexts (due to unique nonces) +- Decryption with wrong key fails properly +- Tampered ciphertext is detected + +### `test_storage.py` - File Operations & Data Management +Tests the persistence layer: +- **Initialization**: Creates `.secrets` and `.secrets.key` files correctly +- **File Integrity**: Validates JSON structure and metadata +- **Secret CRUD**: Add, retrieve, update, delete secrets +- **Key Loading**: Properly loads master key from file +- **Error Handling**: Missing files, corrupted data, permission errors +- **Metadata**: Timestamps and versioning work correctly + +**Critical Tests:** +- Secrets persist across program restarts +- Multiple secrets don't interfere with each other +- File permissions are appropriate +- Corrupted `.secrets` file is handled gracefully + +### `test_cli.py` - Command Line Interface +Tests the user-facing commands: +- **Init Command**: Creates files and updates .gitignore +- **Add Command**: Interactive and direct modes work +- **Get Command**: Retrieves secrets correctly +- **List Command**: Shows secrets with/without values +- **Remove Command**: Deletes secrets with confirmation +- **Run Command**: Injects environment variables properly +- **Export Command**: Outputs in .env and JSON formats +- **Error Messages**: Helpful errors for common mistakes + +**Critical Tests:** +- `zeroenv init` in already-initialized directory fails gracefully +- `zeroenv add` without init shows helpful error +- Environment variables are properly injected for `run` +- Confirmation prompts work for destructive operations +- Exit codes are correct (0 for success, 1 for errors) + +## Troubleshooting + +If you encounter issues with `test_cli.py` failing to capture output, ensure you are running the tests from the project root directory. + +**Common Issues:** +- **Import errors**: Run `pip install -e .` from project root +- **File permission errors**: Tests create temporary directories - ensure write permissions +- **Flaky tests**: Some CLI tests may be environment-dependent - use `pytest-xdist` for isolation + + +## Development + +```bash +git clone https://github.com/ropeadope62/zeroenv +cd zeroenv +pip install -e ".[dev]" +pytest +``` + +## License + +MIT + +## Credits + +Built by Dave C. (ropeadope62) +[artoffailing.com](https://artoffailing.com) + +--- + +> GitHub [@ropeadope62](https://github.com/ropeadope62) diff --git a/src/zeroenv.egg-info/SOURCES.txt b/src/zeroenv.egg-info/SOURCES.txt new file mode 100644 index 0000000..b33d09e --- /dev/null +++ b/src/zeroenv.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +LICENSE +README.md +pyproject.toml +src/zeroenv/__init__.py +src/zeroenv/cli.py +src/zeroenv/crypto.py +src/zeroenv/storage.py +src/zeroenv/ui.py +src/zeroenv.egg-info/PKG-INFO +src/zeroenv.egg-info/SOURCES.txt +src/zeroenv.egg-info/dependency_links.txt +src/zeroenv.egg-info/entry_points.txt +src/zeroenv.egg-info/requires.txt +src/zeroenv.egg-info/top_level.txt +tests/test_cli.py +tests/test_crypto.py +tests/test_storage.py \ No newline at end of file diff --git a/src/zeroenv.egg-info/dependency_links.txt b/src/zeroenv.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/zeroenv.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/zeroenv.egg-info/entry_points.txt b/src/zeroenv.egg-info/entry_points.txt new file mode 100644 index 0000000..32a98f3 --- /dev/null +++ b/src/zeroenv.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +zeroenv = zeroenv.cli:main diff --git a/src/zeroenv.egg-info/requires.txt b/src/zeroenv.egg-info/requires.txt new file mode 100644 index 0000000..02decdf --- /dev/null +++ b/src/zeroenv.egg-info/requires.txt @@ -0,0 +1,3 @@ +click +cryptography +rich diff --git a/src/zeroenv.egg-info/top_level.txt b/src/zeroenv.egg-info/top_level.txt new file mode 100644 index 0000000..98bcf33 --- /dev/null +++ b/src/zeroenv.egg-info/top_level.txt @@ -0,0 +1 @@ +zeroenv diff --git a/src/zeroenv/__pycache__/__init__.cpython-312.pyc b/src/zeroenv/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..035cf6e63a92e22e82b0ca3f2fb5cccc35cad46d GIT binary patch literal 335 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

6Dwp1|4&A*x(p9 z*{ghU#i77IhYpA9Kdc}}a3fx_PjUpU;2R;qNY4MJhQ?UBp9<0Deq%a(n2PMyvbaNHjpmhw(&Fz?`)?PuA3M4achO&BGar00~HjuW1p zdwwL3!=czLW2n{;`gQCzOu=cesU92GAzd<;--`A(}0Z z*UhK?w~I50FVl>A+<+F&JzVLa#D1g>JNNd+nZ(0{{fO}a>UzZZC_*JE{J3%Rqp-@j z>D+?`TwHnhVBl9PRvy7OKMsbD1R}o-4EZUo<*v4`ITv3FPEUx%X-PD=lLaMSKiq=U)CX7&T$o zeCfdlgSG(M-b&y;0^11GFHY%zvb)z}>~3t*IJ9g_kFxbe7|pRmfxH8Va?2CHs9_mu zSZP;dQZ804S%^%l&Z7XBY0K_;wfB|YOU93$#tC2Z*RkVuGpYaW(oEt>nn}mmN_L#B zWXBEnBQYxvw+4RI8alkXek~&^!l=U*8 z0$BNAe^lj&kn%jCv$((anXi; zFI}DVbb&~uFug=fi3yPi{!-eh7#;@9+h~F8yM`^0m}o&n3T06`>q7I)p=F0%+Vgn$ z|EdpH2;&?)bK+wr*#1OmAA5=G&V3y!Yh0Pd+u8sGmAVLeP>u@ZppghQ6FTHI|P^XH)%puUBmzG}dH zQJI&Hw=+%$TRrA{s31e!mb;`_R6XxD4>HxL;4jcD=PdxNBK7vMK?tF6rNFO+rhlXdZ0blJJHIJyDnTFRta#7*Nf z_&(HQX9*Btt2MjdpzPwGY~&nwkg|!O^QWwT@|)%t`1u=OwEqbxv`{;H06qv9hVh|4 tWCTB2;xq7}&OdGX4 Date: Tue, 28 Apr 2026 02:18:48 +0000 Subject: [PATCH 3/3] Implement SecurityTier enum, _derive_key(), salt conversion methods, and tests Agent-Logs-Url: https://github.com/ropeadope62/zeroenv/sessions/83f58a8b-035d-4ee9-85d3-846d84e2611b Co-authored-by: ropeadope62 <26491441+ropeadope62@users.noreply.github.com> --- .gitignore | 9 ++ .../__pycache__/crypto.cpython-312.pyc | Bin 6937 -> 9082 bytes src/zeroenv/crypto.py | 58 ++++++- .../test_crypto.cpython-312-pytest-9.0.3.pyc | Bin 30058 -> 58817 bytes tests/test_crypto.py | 142 +++++++++++++++++- 5 files changed, 204 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9c22a5a..d39a77d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ # ZeroEnv master key .secrets.key + +# Python build artifacts +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +.pytest_cache/ diff --git a/src/zeroenv/__pycache__/crypto.cpython-312.pyc b/src/zeroenv/__pycache__/crypto.cpython-312.pyc index e3b072dd96f1a29d45923fb5dc5abe191edac884..3294faaf5007ac2f82b5f5d7ef310d3d325b2dbd 100644 GIT binary patch delta 3046 zcmZuzZ%kX)6~EVh&lvy5#tt?RUVs!{utSpuMroHNA#|aQP!fZ(xNC{y_Y5Y+j_!Lz z1u@Y|XyZdSrMs0%H>t~~DJkl{WSZ7}SUYLbK6D>~h(z~VrfQoeO`BFSvPI2@YUkW% zJEq%}_}zQXJ?EW!{+hQ3A3fLek>Bqju(n!u3JdLo{27Vra#aqyufNeVc1ELGL(x0u z%lIg9kZ%&ry+X7`-LFgQk}a4C2)+mS-gQTX7u0-uDDTkxD^jLu!J!7;c=HFcG+LUU z;~n&rhgNO0@vF4;QpU9)t1ioz)^qc$U@lA)bhg-2r4$oLC0}OBxl=Eke(tDpRbNmv zofWQU%|f|kxeYU0(y~mm+)Qbd`A#d;qt}E*j_mmYyDC zjmSl4fr~82R0^I_1Ko3@=<15m7#Y`qt<0k63 zO~9?#gKvU8EwT)T+*<33TvH8$P#aze6YX{C5xChe9^(e%9d$R&a3ek^bd2%iJ#1{N&x5Zy{JiNsA2K#RzaAVx^%w-KzXH$?kOTug}h7FS`OG-7xm~cxnvPCmj zsVd>V4CsWvT{q=#I`jD@x>q@N+mZ^@YQ1ZU z4u!8=&lcx({za%Ou(yDDYf~S8Tk4=b{=U?@YKAXJcV(D z4TAiM=jK!y!m1bZtQDB74FUR{SOLA1V++@?e^AH3NGrIQ6oDf@ zEO+{!frC*5c$GW|^Os~b_gU+~Rq39)<I4a9ThQob-f&vZ>ODpN(w5f6XIzn{-E`IfIN zn7X0#8reBr$r_3sb1_?R)Kq7RP*Oc2HUO&3uq-T3UA()1!Ja_q0Z=JB1e<~GDGYv*?B&B#&p-acILf0g1Uf)M?OVZ~jbP6$X{&c=qj%_+-5(r!|Imlp z?`D59yBR!x*L~iOW8<}Awoo$lSIj%_%4Z!^u?rk zY=daJu9}4ZC%E66px1b>w?M=EOWzECN3`socm5a^U}Xy2I$tUGB7Zw?<{?f@Fvp)p&WF2j$^ z%$(&a>ZNIOMsx&gforSbN;Y>@FKMdRPIDkVIx=}-Y~s??#MtPCH0#9oDF~+#us|8s zuH{OfIeBzo(Dnjni{-NO3$e6YPZS9Do^8o_Rm*=hPW^DpzYHhnk9a(C50dlGNDuAC zjNEpLj4*6TJNlG~s;N{Ne>&Pvf6EI|6|{KNOjV#29)Fl*s3MTe-Z1a8~m@a z{US#EKauI!PiqxEQZ8NBnW^Ah;jFnECtDK|!%7-Z5p5&7X!%su&reLbwN7|Kr@A+rOhSwn5D~;hT3en7r#w3Wrn& z^DKkJxVn$|VP^q^AOil#+LiY;JUx$q^=Zi>SuBYR972{89%V^dA!o87WS&5#4`!WN z$j#~IOj%>6kUxrm<;cz>Od?z)XIJrST1PbRvl%+DwKsf)js=v>BrXWvE;|4#~gFCuvvq0T6f zkKE7Bm9_bzew^ij#DX!-0PHv^rS}~&m39M!dOjikZ4%uk$G6FmZE|9p3_Wm3G<4&s fHR%C?!^6-^PMW$;5W_ns8X9Qt+T;TQn{fZXP8QU> delta 1527 zcmZ8hO>7%g5PomHYp-`>$Bt9mv6DD)k~-TYgr-SI0VNHPQj5xmI+bXI^0a&FboUA4%w}1@;Ea?)|}ikc1_L)~$Jx5$H_xT+exKS$fw=eDE$@5$HoeJ~Kmf3Z?5S8aC1 zcD$U~mw0vaYwff|W5O30>4^h{w;%mYk>rv5^n_FrOC~I0^iW{ z$a48Po&wQOcm@{7KR-TdveJslI7|(3e6ZoQi#9jvBsbR)kli%(gqYe=05D-Aw8dpPYn{Mh#-+41M6PtO+e z*|`}$j1CR2cx0Gf0k4=SMD#E!bB=768HbV=EB0nJvf-h+NW>;rBVaTX$6u^0uJE$G zwCs2*th#6~@i7dj13138sdJ0Hcj)5s)#h($u!8Aq#=aQejGu>^fv#N*#VkA911 zXs_ECkJ}jDkKS$*D4zn^!ij`pi;2WA{Y0Gah>AB85%ByVq0*!w)x8FRWKjOXL5DAr z!$V)80?Q9W7bT(S{pQrY=G1L%yLn(s9dK(4Ap$_i8LdK>o?1mR??reP0dMap^EI1U zJb|*uvZ3T=xn%P`Bn+Xl$+C-fxwOper4^GujrwBn(7Nu1_asdjtj~h$)*+5`e_V7$H{Fw=oya1qGG4S<;lEeU zbCBu|{45+0e1)W`?Y`c3Pw(5-Gh1?|UXCC?1!nF}Wk#GxAN9IVyi$8OT+*G diff --git a/src/zeroenv/crypto.py b/src/zeroenv/crypto.py index 9f168c6..8c7a911 100644 --- a/src/zeroenv/crypto.py +++ b/src/zeroenv/crypto.py @@ -7,12 +7,20 @@ import os import base64 +from enum import Enum from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.backends import default_backend +class SecurityTier(str, Enum): + """Security tier for PBKDF2 key derivation""" + standard = 'standard' + enhanced = 'enhanced' + max = 'max' + + class ZeroEnvCrypto: """Handles all cryptographic operations for ZeroEnv""" @@ -48,17 +56,33 @@ class ZeroEnvCrypto: SALT_SIZE = 16 # 128 bits for PBKDF2 salt - def __init__(self, master_key: bytes): + def __init__(self, master_key: bytes, tier: "str | SecurityTier" = 'standard', salt: bytes = None): """ Initialize crypto with master key - + Args: master_key: 32-byte encryption key + tier: Security tier ('standard', 'enhanced', or 'max'), or SecurityTier enum value + salt: Salt for PBKDF2 key derivation (required for non-standard tiers) """ if len(master_key) != self.KEY_SIZE: raise ValueError(f"Master key must be {self.KEY_SIZE} bytes") - - self.aesgcm = AESGCM(master_key) + + self._master_key = master_key + self._tier = tier.value if isinstance(tier, SecurityTier) else tier + self._salt = salt + + encryption_key = self._derive_key() + self.aesgcm = AESGCM(encryption_key) + + def _derive_key(self) -> bytes: + """ + Derive encryption key from master key using configured security tier + + Returns: + Derived key bytes (same as master key for standard tier) + """ + return ZeroEnvCrypto.derive_key(self._master_key, self._tier, self._salt) def encrypt(self, plaintext: str) -> dict: """ @@ -195,6 +219,32 @@ def string_to_key(key_string: str) -> bytes: """ return base64.b64decode(key_string) + @staticmethod + def salt_to_string(salt: bytes) -> str: + """ + Convert salt bytes to base64 string for storage + + Args: + salt: Salt bytes + + Returns: + Base64-encoded salt string + """ + return base64.b64encode(salt).decode('utf-8') + + @staticmethod + def string_to_salt(salt_string: str) -> bytes: + """ + Convert base64 salt string back to bytes + + Args: + salt_string: Base64-encoded salt + + Returns: + Salt bytes + """ + return base64.b64decode(salt_string) + def generate_master_key() -> bytes: """ diff --git a/tests/__pycache__/test_crypto.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_crypto.cpython-312-pytest-9.0.3.pyc index cc83ce527254baf3f1d69125171aa7cf3ee4e56f..00189af8812ea60f441ce5101d4812e6057ea5df 100644 GIT binary patch literal 58817 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 V-B~0x@}qZh(tfc}7cCPW{R3>1hdclP diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 3a53ce1..6b115c7 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -9,7 +9,7 @@ import pytest import base64 import os -from zeroenv.crypto import ZeroEnvCrypto, generate_master_key +from zeroenv.crypto import ZeroEnvCrypto, SecurityTier, generate_master_key class TestZeroEnvCrypto: @pytest.fixture @@ -186,3 +186,143 @@ def test_encrypt_decrypt_with_derived_key(self, master_key): decrypted = crypto.decrypt(encrypted) assert decrypted == plaintext + + +class TestSecurityTierEnum: + """Tests for the SecurityTier enum""" + + def test_enum_values(self): + """Test that SecurityTier enum has the correct values""" + assert SecurityTier.standard == 'standard' + assert SecurityTier.enhanced == 'enhanced' + assert SecurityTier.max == 'max' + + def test_enum_members(self): + """Test that all expected members exist""" + members = {tier.value for tier in SecurityTier} + assert members == {'standard', 'enhanced', 'max'} + + def test_enum_is_string(self): + """Test that SecurityTier values behave as strings""" + assert isinstance(SecurityTier.standard, str) + assert SecurityTier.enhanced == 'enhanced' + + def test_enum_from_value(self): + """Test creating enum from string value""" + assert SecurityTier('standard') is SecurityTier.standard + assert SecurityTier('enhanced') is SecurityTier.enhanced + assert SecurityTier('max') is SecurityTier.max + + +class TestZeroEnvCryptoInitWithTier: + """Tests for ZeroEnvCrypto.__init__ with tier and salt parameters""" + + @pytest.fixture + def master_key(self): + return generate_master_key() + + def test_init_standard_tier_default(self, master_key): + """Test that standard tier (default) uses key directly (no PBKDF2)""" + crypto_default = ZeroEnvCrypto(master_key) + crypto_explicit = ZeroEnvCrypto(master_key, tier='standard') + # Both should encrypt/decrypt the same data interchangeably + plaintext = "standard_secret" + encrypted = crypto_default.encrypt(plaintext) + assert crypto_explicit.decrypt(encrypted) == plaintext + + def test_init_standard_tier_explicit(self, master_key): + """Test explicit standard tier initialization""" + crypto = ZeroEnvCrypto(master_key, tier='standard') + assert crypto.decrypt(crypto.encrypt("test")) == "test" + + def test_init_enhanced_tier_with_salt(self, master_key): + """Test enhanced tier initialization with salt""" + salt = ZeroEnvCrypto.generate_salt() + crypto = ZeroEnvCrypto(master_key, tier='enhanced', salt=salt) + # Should be able to encrypt and decrypt + plaintext = "enhanced_secret" + assert crypto.decrypt(crypto.encrypt(plaintext)) == plaintext + + def test_init_max_tier_with_salt(self, master_key): + """Test max tier initialization with salt""" + salt = ZeroEnvCrypto.generate_salt() + crypto = ZeroEnvCrypto(master_key, tier='max', salt=salt) + # Should be able to encrypt and decrypt + plaintext = "max_secret" + assert crypto.decrypt(crypto.encrypt(plaintext)) == plaintext + + def test_init_enhanced_tier_missing_salt_raises(self, master_key): + """Test that enhanced tier without salt raises ValueError""" + with pytest.raises(ValueError, match="Salt required"): + ZeroEnvCrypto(master_key, tier='enhanced', salt=None) + + def test_init_max_tier_missing_salt_raises(self, master_key): + """Test that max tier without salt raises ValueError""" + with pytest.raises(ValueError, match="Salt required"): + ZeroEnvCrypto(master_key, tier='max', salt=None) + + def test_init_with_security_tier_enum(self, master_key): + """Test that SecurityTier enum values work in __init__""" + salt = ZeroEnvCrypto.generate_salt() + crypto = ZeroEnvCrypto(master_key, tier=SecurityTier.enhanced, salt=salt) + plaintext = "enum_tier_secret" + assert crypto.decrypt(crypto.encrypt(plaintext)) == plaintext + + def test_encrypt_decrypt_standard_via_init(self, master_key): + """Test encrypt/decrypt with standard tier via __init__""" + crypto = ZeroEnvCrypto(master_key, tier='standard') + plaintext = "standard_secret" + assert crypto.decrypt(crypto.encrypt(plaintext)) == plaintext + + def test_encrypt_decrypt_enhanced_via_init(self, master_key): + """Test encrypt/decrypt with enhanced tier via __init__""" + salt = ZeroEnvCrypto.generate_salt() + crypto = ZeroEnvCrypto(master_key, tier='enhanced', salt=salt) + plaintext = "enhanced_secret" + assert crypto.decrypt(crypto.encrypt(plaintext)) == plaintext + + def test_encrypt_decrypt_max_via_init(self, master_key): + """Test encrypt/decrypt with max tier via __init__""" + salt = ZeroEnvCrypto.generate_salt() + crypto = ZeroEnvCrypto(master_key, tier='max', salt=salt) + plaintext = "max_secret" + assert crypto.decrypt(crypto.encrypt(plaintext)) == plaintext + + def test_different_tiers_incompatible(self, master_key): + """Test that data encrypted with one tier cannot be decrypted with another""" + salt = ZeroEnvCrypto.generate_salt() + crypto_standard = ZeroEnvCrypto(master_key, tier='standard') + crypto_enhanced = ZeroEnvCrypto(master_key, tier='enhanced', salt=salt) + + encrypted = crypto_standard.encrypt("secret") + with pytest.raises(Exception): + crypto_enhanced.decrypt(encrypted) + + +class TestSaltConversionMethods: + """Tests for salt_to_string and string_to_salt conversion methods""" + + def test_salt_to_string_returns_str(self): + """Test that salt_to_string returns a string""" + salt = ZeroEnvCrypto.generate_salt() + result = ZeroEnvCrypto.salt_to_string(salt) + assert isinstance(result, str) + + def test_string_to_salt_returns_bytes(self): + """Test that string_to_salt returns bytes""" + salt = ZeroEnvCrypto.generate_salt() + salt_str = ZeroEnvCrypto.salt_to_string(salt) + result = ZeroEnvCrypto.string_to_salt(salt_str) + assert isinstance(result, bytes) + + def test_salt_roundtrip(self): + """Test that salt survives a to_string/from_string roundtrip""" + salt = ZeroEnvCrypto.generate_salt() + assert ZeroEnvCrypto.string_to_salt(ZeroEnvCrypto.salt_to_string(salt)) == salt + + def test_salt_to_string_is_valid_base64(self): + """Test that salt_to_string produces valid base64""" + salt = ZeroEnvCrypto.generate_salt() + salt_str = ZeroEnvCrypto.salt_to_string(salt) + decoded = base64.b64decode(salt_str) + assert decoded == salt