From e5228a86dcccc6cbc88738dd5e9958fb07d0e3ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:39:13 +0000 Subject: [PATCH 1/3] Initial plan From 1b95934460f7fb78e14c04f9c0566ee450dc327b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:40:43 +0000 Subject: [PATCH 2/3] chore: initial plan for storage module updates Agent-Logs-Url: https://github.com/ropeadope62/zeroenv/sessions/c319246f-d5ac-46d1-80f1-bdf699234d1c 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 .../__pycache__/storage.cpython-312.pyc | Bin 0 -> 10292 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 343 bytes .../test_storage.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 40472 bytes 11 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 src/zeroenv/__pycache__/storage.cpython-312.pyc create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_storage.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..c09c6e2807b73b8e49b4878856b9720f39a66e5a GIT binary patch literal 335 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 R`@fx6?7Zi99Hz9d{{>?MGqC^w literal 0 HcmV?d00001 diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..564bcdf7ce0e7a2725c998c6fed805034e937479 GIT binary patch literal 343 zcmX|7u}T9$5Z%2bBwh#y5^RF7U}K8hB??wzqrpm>SY5L$*@;=a+YP&WC%Gzqg&$z$ zxA+4gmEaGYq)X*qP^WnF9&Z@t%}1x>BUhitrFe7ws}g^Of62D%$SdL~MjXeOd$GrB zacx=eH;SGggH~gilL2`EV}*PqcZz2T7{6!?!%;Xax>_xOaR(P?^HHKlaZrZfj_gpyA9l(Nju zm3BOE6JVxVEbQKVN82jL?mU{S6e68T2|9XK`Y9^h=^*p}Ze0KqQ7WXcl!lAtu63F2 o!(8bDx1)N9={YEC7~^lx!}YR-f}>S?-8otL>%;L&pqtMB0bl-SSO5S3 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..881074b4cc7044fff35873c19e0b0377c6c9e2ef GIT binary patch literal 40472 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 Date: Tue, 28 Apr 2026 02:42:20 +0000 Subject: [PATCH 3/3] feat: bump version to 1.1, add create_crypto() and get_project_info() to SecretsStorage Agent-Logs-Url: https://github.com/ropeadope62/zeroenv/sessions/c319246f-d5ac-46d1-80f1-bdf699234d1c Co-authored-by: ropeadope62 <26491441+ropeadope62@users.noreply.github.com> --- .gitignore | 8 + .../__pycache__/storage.cpython-312.pyc | Bin 10292 -> 11524 bytes src/zeroenv/storage.py | 29 +++- .../test_storage.cpython-312-pytest-9.0.3.pyc | Bin 40472 -> 59272 bytes tests/test_storage.py | 157 +++++++++++++++++- 5 files changed, 192 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9c22a5a..29bb86d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ # ZeroEnv master key .secrets.key + +# Python build artifacts +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ diff --git a/src/zeroenv/__pycache__/storage.cpython-312.pyc b/src/zeroenv/__pycache__/storage.cpython-312.pyc index 5bed65964d5f9ef737222f203cf531c51652406b..2d31a0ca85d34c2ef2e0acd3462af5e38dcd5fb7 100644 GIT binary patch delta 1545 zcmZ`(O>7%Q6yCAd>z_2SOC0}uv-xpSBU55kpjM<5g`!GTMN`t25JfR&XPi1scEfnw z#umsNf(VxiGzSu?Rf0gGQZA{s7eGC615v14sg)IpaN$rcfgBJAB;KqYM^s{@{pQX0 z-n@DHzS+GyzC6|SLuaQ?f^kaSEr$E=bj=|2B40qU9u>QYLfldC6&zSuc1?M3FTaWO znwN9~b9h;v3gV{y4A?!mn{;C@mJ2ct!a9U|$YC5JZXAY3@E#liKHTQL?feRkf+qsm zqL4WTcFf-CB^{704(qtR%S#f({VZ`89CleUPJk=f3ROsVi%kNnR^+s1ed74dg|gOl zPwWuC7x1Fv9Q6&>5Oa!G7UMm%iJI1glt$I>d|GRam5cLO%WG4Fny(qxUZs^qW@<&F z#PWttG~F}`#o00?SSy%RV^@e)Vy0M@h+d{eR?%3IP%Uo&lb>hpykPK?YP|M8c`3=` zik*@ex;XQBOzc@R7YWU?qG^=cxzY}L28yR10I<7ZYIL>sL16AGsgxW*Y8nj+U#AY~ z8OcFH9p0vzn1I}bl_sF_{46?kDxK9F|1q?r0Bntc$>)}zN(y0-m^)tS0K z^Rp*ILlB|2S_q~jdEpW5-FqT>&3_L>`Mlr9@5e*@+kr!34q+EoHbr4Dn5#KUQq$!w zO8c(OR$Auf1*dvxe~CReT_BdX`mo)IGlXf2)SM$aYb#^-3Iqdr7Mk|HP=bT7g#{3Y zXVqaoHyEg0BeVoP%V^Ce;9Q<%+FIs>miAJ#E2o=fgJs&MiyI?!#zhJRn6#oT=Gm3D zo5tY{tIq7v5t;z2;Vv6j4fDD|MRRGd09C4if-XDK4zhiXPPj`42ht7*g`ksS$IAc> z=R7eQ&Jv@xi=G1RKY9md3Cp$mk`r%T?R`q|c0f&McEk;lg8+c)0VxoCLxJ){|q~No^zeRpj%? z?ex|g^}gfva_Pdn|m+wX>ANW zvDUR;(csL}NnZh?p-@sP&$H$UIsRq;Y2_c_OBeV^DrCt6U!Z}vU`@{fz?WthViTi+ zkoV8IKRE61Bc4g+Q7^xd8gI$DSSM#HH_$XmAYT%!i!)U!l|7X`jR|Z!ONtawMjEP;O(<0mDl82YmZ@aX zl-s<5=@~1d!RCGrDW1taLY$j@Bqf*_TQ;XkUtwgNw%JNnib>oX=n+k}B4Hp^)(`16E00|b^ z00~VNm=!geF>GIeBE?>l7pRL(mQ~`P%&cX>C_C9k%ZBw9b8$)0? j4>kgnB#QDTZ_troES!8&M~A None: # Create the baseline .secrets file with metadata initial_data = { - "version": "1.0", + "version": "1.1", "created_at": datetime.now().isoformat(), "security_tier": tier, "secrets": {} @@ -129,6 +129,33 @@ def load_encryption_key(self) -> bytes: return ZeroEnvCrypto.derive_key(master_key, tier, salt) + def create_crypto(self) -> ZeroEnvCrypto: + """ + Build a ZeroEnvCrypto instance configured for the stored security tier and salt + + Returns: + ZeroEnvCrypto instance ready for encrypt/decrypt operations + """ + encryption_key = self.load_encryption_key() + return ZeroEnvCrypto(encryption_key) + + def get_project_info(self) -> dict: + """ + Get project configuration information for the info command + + Returns: + Dictionary with version, created_at, security_tier, secrets_count, + and directory path + """ + data = self.load_secrets_file() + return { + "version": data.get("version", "1.0"), + "created_at": data.get("created_at", "unknown"), + "security_tier": data.get("security_tier", "standard"), + "secrets_count": len(data.get("secrets", {})), + "directory": str(self.directory), + } + def load_secrets_file(self) -> dict: """ Load the secrets file diff --git a/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_storage.cpython-312-pytest-9.0.3.pyc index 881074b4cc7044fff35873c19e0b0377c6c9e2ef..3937c92f65be1bed8bc5c47f116ac84605d3fc89 100644 GIT binary patch delta 9837 zcmeHN4R9OBb;jZM?f~fc15*6gQT&C(ACQt|T9QTCqV+FIwy0R9ox%|BC_SFXfmMWnyjh~ zdFH5Sy^5*d>uiZ4N8B7 z@Ym!Uu_oH%N}JY3icc(qncj2atT}Z9Z#^~2Op}(;{KeN#)5=l%RvPwT2iPYYaK z9?x2OT{0;hXvKq#FDy%5Us&0WeZ595j=ju$hqS%aVSNiL@UgEnDoJwrrJc<5`-6#G0((0`kx2^U1cgE|Tcx$c1{R z$r3e_mU7otiF;M+GiHmL58=e-ePXQ95M`q_U5}Hv7$4=L{NZ|oA!d)-VU9-f&iM+5 z#7FJDjAX|~a;5+7JLhGE+p}o5=PlyN-*0N9N{1TD%}RmRu#x3a&!H9HjPOQHjNi|f zNX`qEh!sY5NJ}Ik>hLqP~SWBUk<+Z7(c|ib0YW6Un;to`ov>F z<*+|+Q0$hK-~)j{b(N8;Pvy6zJx-eWNG1jDL8X7^93O_&=pFR+AC)POFENo_#8ug3 zlYnoqUdist0$-auG{?cJ#0puuWKl%ypb}vd0`)FuKzd*Bg+j6t7HjGUhFU!CkP?I1 zOM+rSwVeQS&a9cJF2_p#=;7MpIh!`bTxxJ-C0Z{bp2MzY>IRfG*iRS2`v|KMzJTx| z!d(c@A*@4>(=F1;Wew;Jg?$0Zr%2wgUsfV5ckt!cnKZzn3;D^D`>W!)MYFlJQ@ORt z-1=eb4~62f+E*Hrf+xXxrUlO|o4&5Go@XO!MKUew8q=(2tX6MMiJ8r>H0sSMafXe1 z64}Fm4vc-%YA7f@-#OAbzCP*l40AVxg6B`1K9OMCPM;X_Cxv>DCUycOg!+k{Q$pJ; zi`R9nLM`by#kLI(s-oG(|STf%^IbhR2s>m7cNqG<1 zO}YPHD1Cr{K1TU(1co8^jg~IRdpXeOgMIar@IK3=?f(z&HzZxH8N6Ss^M36_XM$ae zyziV6*3PncUDqnql8#gCTI4-yB-7#yh{$`*Ru!O2B)g`X5HW72)lhLY4@|A(;fdOu9E+Y{eG6+ro{^C7XI7r_slKQ zDEAl-*+^wmzTKop=@t@@mzk_lE2%WPYGXDC;%!lj-pop-7zbfJN14M%c^bq&xwnFR zIBc_6u!S5S*hlk)OM9Ip(oA!Q(I=|NmCbec`qjyU(( zsdBKFtCmxvG zH3d7QKLi^hWMfDC0Z4iMJz}F6J{A;veEt0)aX<-5M`x-&>?W2Cm*mH%s^WRYvw3w>d3DJ=_pt4TtMvTt zk==>vHA&aq!yE*+`6ZdnLgDlA)A0n`emXujloZ?`B^uiiL2hiH653~3ysoEe)S`}4 zZ2RyrwUkuFII7Qp9M_dp0XiKU(p6I;Xu?ox^)wxPo*5Kja3v3(b&rWI9ThnEgXP;S z;CVwB9svE2b#j-<1gvN&%+E1PCdvG&X2uwzL^KvO#4JDoE6@T`;iy&5*=>?J2H9?u zrNrQ(91x?B05M7=OO%7`){2e2##hbiK#~n}(-apf$oSquy8Z_WIZR?x)Y#a}nI$$` z_2#1v^3}aORev+=FtHkqAV)W9;+4(12;{1kxC>UA?eY>2X*RDB%6)z7h{JUZfB%DYqimkGjy~+3TnCXu_guetZ$bQ0@=2d3tqQIzTs-~_>NC#A?6Bdf?W zeTCIDluf6nG&4PccK0BxLs(C~)mIW-b+wF!3wi%$rM{Mwr2dUi4`;KT{ncnS)p=Ds7$aMCTajc z9Iu_Ip}FpcE8C$4GSMsBr-Ti&EMC{O3i95zu6T`FH%;qw`O0=xoQKdAWePsoKgNgj zCkU?~+(M8L{uCh>p%}r4ZpwndA~+Fp5&jHYE0K6g4#F=}PzkM}T~K=3@B{N2a^XU& zc+agO-m>mi5ofvQRv~ZM$lNZnSya|iJ9@776rAgwx)*)>Tps=uTjcN_C3px@oo#{M zV5AD`)RR5@)xe=+lh+SzuvW7U^wfj8Y+U$l6(v^eJpe1I7L$uGLSN=2U)&80E>i9H!rBOCKS2*Z% zMj94aIeKE<-!IC?2VhNspMwM45Iy8p4&$;u3ovKKrKUWKW&PB@Q~5V6U%_^#>IOrw zPah406*tsfvMY7z!?~RKqbn=W7?0tUX2NX9w|h`*K|>CYK;@p2 zFhrs;DCvjCJVEV;v;kzBFE%V5^3sW8s8QHOu04O)aa1Bo&+=q zZw3LZZU_|#wstr^UY!u&Xb-PzYFRBLu>c}|^$RSX6s8H5>jIRV$iJVr)NuF^bH4acD8 zhB(GZUY46}rl<`(lbIxBuQTO@Sad>UeeaepP`87T0Hp{Q*)V@`(Li7zsDSSsl$DS_ z7!X^UT4=TzkdMJBlo~cjVz0j+V(AWCtPj8ozH;Fbn-LP1FvNMF)Od-8a`O)mD8 z9uD!KF6tk;lgyPbylS=4Bc4UCbIl@(OMMWc-x6WoQYZG>U8Whfjy}W3;TiUjbMl9q z@!e8n^~XF2?Ff#h+Vn}J&b^iNTLJo#GhTrNUWPZY&`hfqo@uH#HKks&==o;ZJq?52 zX5tw1+cVfrghw8PDYyRCQ%;@VrL!mXz3y)|2VkRYs2?)`9`RTQOm1V!Mi~k!q1OZig96-;^XEF9F|)5L=9Ioe~;mS-h@k6>2F-ORB6R*o<%0+f$<648jcCkaZM5D&!@`Z<`=& zFDKR)&XCdP%9bIW%^ICy+smf2nurtoPLBNh3(nN*38kax?b`rb-gR)czCxEXHY;``|wZxgDzSS%nJ!fk5Al|0hasABEoWn8iYCoH^NE;nxdyO^n9#Ckl`D`7yFo@H)4j~Saqw&YN@}KW3{Z(fVZr` zy2jf%IhGal_NcA9K)FR+7awGPOnNWww9%|TpCOkomhr2gTeu9NedOB3wwiYhzh`9X be|5j*K89(3*MRt|t;W3!v*MNkk@oq&=b8cp&>B^NYqrJ_*LIiR#@fl}wsk8}I#5U^Li#Td~)Gv3PT4 z&`&nGb^kz?F2xjpeQ+cuEE_Q?fF?u;8Y??@_TMOUJ@xj17F6XV*=2HAI* z)V6?>gAI`bvQ{!y38`unR2pa&E7;m9fSAUb#YO8T8|4);uA6LFroH)mo(Qwl8n~|Y zAQnh#(NPewZt~wk3&vxU&dvsL%jCXVP0bA;!}35xK8R2T5nDk7#Csst4iK?(@`GAEph^67IxwF**2x16 zOswl>fm!*fX%geb&8^Mu?DBB$zEl8d2RpnC#A*i-7bm~&N@KYMWKNc~)0-?lNpSLq zGpvjkC(r8EVZ1(B(NJdcs%ELpf4luyWx?J78@CszK17r87DsADW^qY;escbM@eRCQG7~fQIQDP9uOe|lHe{*O)g3;iO);SO)a{+Ib@P8n{<&Y zP^L=Q$iT>0*T4{nLJSP7z({lQni&#|YbGC^F^lo&TZlX-=wL(KjHM5r~VeCu={q;QquA$Hb@sRtW$+iQ~Bd diff --git a/tests/test_storage.py b/tests/test_storage.py index aad129b..c36adc9 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -55,7 +55,7 @@ def test_initialize(self, storage, master_key): # Verify secrets file content data = json.loads(storage.secrets_path.read_text()) - assert data['version'] == "1.0" + assert data['version'] == "1.1" assert data['secrets'] == {} def test_load_master_key(self, storage, master_key): @@ -284,3 +284,158 @@ def test_backward_compatibility_no_tier(self, temp_dir, master_key): storage.add_secret(crypto, "OLD_SECRET", "old_value") retrieved = storage.get_secret(crypto, "OLD_SECRET") assert retrieved == "old_value" + + +class TestCreateCrypto: + """Tests for create_crypto() convenience method""" + + @pytest.fixture + def temp_dir(self, tmp_path): + return str(tmp_path) + + @pytest.fixture + def master_key(self): + return generate_master_key() + + def test_create_crypto_standard_tier(self, temp_dir, master_key): + """create_crypto() returns a working ZeroEnvCrypto for standard tier""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='standard') + + crypto = storage.create_crypto() + assert isinstance(crypto, ZeroEnvCrypto) + + # Should be able to encrypt and decrypt + encrypted = crypto.encrypt("test_value") + assert crypto.decrypt(encrypted) == "test_value" + + def test_create_crypto_enhanced_tier(self, temp_dir, master_key): + """create_crypto() returns a correctly derived ZeroEnvCrypto for enhanced tier""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + crypto = storage.create_crypto() + assert isinstance(crypto, ZeroEnvCrypto) + + # Verify round-trip encryption + encrypted = crypto.encrypt("secure_value") + assert crypto.decrypt(encrypted) == "secure_value" + + def test_create_crypto_max_tier(self, temp_dir, master_key): + """create_crypto() returns a correctly derived ZeroEnvCrypto for max tier""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='max') + + crypto = storage.create_crypto() + assert isinstance(crypto, ZeroEnvCrypto) + + encrypted = crypto.encrypt("max_secret") + assert crypto.decrypt(encrypted) == "max_secret" + + def test_create_crypto_consistent(self, temp_dir, master_key): + """create_crypto() is deterministic - two calls produce compatible instances""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + crypto1 = storage.create_crypto() + encrypted = crypto1.encrypt("consistent_value") + + # A fresh instance should decrypt the same ciphertext + crypto2 = storage.create_crypto() + assert crypto2.decrypt(encrypted) == "consistent_value" + + def test_create_crypto_can_add_and_get_secrets(self, temp_dir, master_key): + """create_crypto() result can be used with add_secret / get_secret""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + crypto = storage.create_crypto() + storage.add_secret(crypto, "KEY", "value123") + assert storage.get_secret(crypto, "KEY") == "value123" + + +class TestGetProjectInfo: + """Tests for get_project_info() method""" + + @pytest.fixture + def temp_dir(self, tmp_path): + return str(tmp_path) + + @pytest.fixture + def master_key(self): + return generate_master_key() + + def test_get_project_info_keys(self, temp_dir, master_key): + """get_project_info() returns all expected keys""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + info = storage.get_project_info() + assert "version" in info + assert "created_at" in info + assert "security_tier" in info + assert "secrets_count" in info + assert "directory" in info + + def test_get_project_info_version(self, temp_dir, master_key): + """get_project_info() reports version 1.1 for newly created files""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + info = storage.get_project_info() + assert info["version"] == "1.1" + + def test_get_project_info_standard_tier(self, temp_dir, master_key): + """get_project_info() reports correct tier for standard""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='standard') + + info = storage.get_project_info() + assert info["security_tier"] == "standard" + + def test_get_project_info_enhanced_tier(self, temp_dir, master_key): + """get_project_info() reports correct tier for enhanced""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key, tier='enhanced') + + info = storage.get_project_info() + assert info["security_tier"] == "enhanced" + + def test_get_project_info_secrets_count(self, temp_dir, master_key): + """get_project_info() returns correct secrets count""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + crypto = storage.create_crypto() + storage.add_secret(crypto, "A", "1") + storage.add_secret(crypto, "B", "2") + + info = storage.get_project_info() + assert info["secrets_count"] == 2 + + def test_get_project_info_directory(self, temp_dir, master_key): + """get_project_info() returns the storage directory""" + storage = SecretsStorage(temp_dir) + storage.initialize(master_key) + + info = storage.get_project_info() + assert info["directory"] == temp_dir + + def test_get_project_info_backward_compatible(self, temp_dir, master_key): + """get_project_info() works with legacy v1.0 files""" + storage = SecretsStorage(temp_dir) + + # Write a v1.0 style file (no security_tier) + storage.key_path.write_text(ZeroEnvCrypto.key_to_string(master_key)) + old_data = { + "version": "1.0", + "created_at": "2023-01-01T00:00:00", + "secrets": {} + } + storage.save_secrets_file(old_data) + + info = storage.get_project_info() + assert info["version"] == "1.0" + assert info["security_tier"] == "standard" + assert info["secrets_count"] == 0 +