kv-secrets 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kv_secrets-0.1.0/.gitignore +31 -0
- kv_secrets-0.1.0/LICENSE +21 -0
- kv_secrets-0.1.0/PKG-INFO +33 -0
- kv_secrets-0.1.0/README.md +162 -0
- kv_secrets-0.1.0/kv/DESIGN.md +139 -0
- kv_secrets-0.1.0/kv/README.md +175 -0
- kv_secrets-0.1.0/kv/__init__.py +2 -0
- kv_secrets-0.1.0/kv/__main__.py +16 -0
- kv_secrets-0.1.0/kv/auth.py +93 -0
- kv_secrets-0.1.0/kv/cli.py +666 -0
- kv_secrets-0.1.0/kv/cli_remote.py +448 -0
- kv_secrets-0.1.0/kv/config.py +133 -0
- kv_secrets-0.1.0/kv/crypto.py +88 -0
- kv_secrets-0.1.0/kv/env.py +91 -0
- kv_secrets-0.1.0/kv/remote.py +135 -0
- kv_secrets-0.1.0/kv/store.py +159 -0
- kv_secrets-0.1.0/kv/sync.py +125 -0
- kv_secrets-0.1.0/kv_mcp/__init__.py +5 -0
- kv_secrets-0.1.0/kv_mcp/__main__.py +62 -0
- kv_secrets-0.1.0/kv_mcp/protocol.py +69 -0
- kv_secrets-0.1.0/kv_mcp/server.py +169 -0
- kv_secrets-0.1.0/kv_mcp/tools.py +345 -0
- kv_secrets-0.1.0/pyproject.toml +49 -0
- kv_secrets-0.1.0/tests/test_e2e.py +851 -0
- kv_secrets-0.1.0/tests/test_mcp.py +1097 -0
- kv_secrets-0.1.0/tests/test_stress.py +89 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Test artifacts
|
|
10
|
+
*.db
|
|
11
|
+
*.db-shm
|
|
12
|
+
*.db-wal
|
|
13
|
+
*.log
|
|
14
|
+
|
|
15
|
+
# Secrets — NEVER commit these
|
|
16
|
+
.env
|
|
17
|
+
.secrets/
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
.cursor/
|
|
23
|
+
|
|
24
|
+
# Server — private (not open source)
|
|
25
|
+
kv_server/
|
|
26
|
+
|
|
27
|
+
# Reviewer workspace — internal only
|
|
28
|
+
reviewer/
|
|
29
|
+
|
|
30
|
+
# Deploy artifacts — private
|
|
31
|
+
deploy/
|
kv_secrets-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kv-secrets contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kv-secrets
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Encrypted secrets management for developers and AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/sathi/kv-secrets
|
|
6
|
+
Project-URL: Documentation, https://github.com/sathi/kv-secrets#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/sathi/kv-secrets/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,devtools,encryption,mcp,secrets
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: cryptography>=41.0.0
|
|
25
|
+
Provides-Extra: server
|
|
26
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == 'server'
|
|
27
|
+
Requires-Dist: asyncpg>=0.29.0; extra == 'server'
|
|
28
|
+
Requires-Dist: bcrypt>=4.0.0; extra == 'server'
|
|
29
|
+
Requires-Dist: fastapi>=0.110.0; extra == 'server'
|
|
30
|
+
Requires-Dist: python-jose[cryptography]>=3.3.0; extra == 'server'
|
|
31
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'server'
|
|
32
|
+
Requires-Dist: stripe>=8.0.0; extra == 'server'
|
|
33
|
+
Requires-Dist: uvicorn>=0.27.0; extra == 'server'
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# kv
|
|
2
|
+
|
|
3
|
+
Encrypted secrets management for developers and AI coding agents.
|
|
4
|
+
|
|
5
|
+
**kv** encrypts your API keys, database URLs, and tokens with ChaCha20-Poly1305 — then lets your AI editor (Cursor, Claude Code, VS Code) use them safely through MCP.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install kv-secrets
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Why kv?
|
|
12
|
+
|
|
13
|
+
Your AI coding agent needs your API keys to run and test code. But pasting secrets into chat is dangerous — they end up in logs, training data, and prompt history.
|
|
14
|
+
|
|
15
|
+
**kv keeps secrets encrypted on disk and injects them only at runtime.** Your AI agent never sees the plaintext values.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Initialize in your project
|
|
21
|
+
kv init
|
|
22
|
+
|
|
23
|
+
# Store secrets
|
|
24
|
+
kv set API_KEY sk-live-abc123
|
|
25
|
+
kv set DATABASE_URL postgres://user:pass@host/db
|
|
26
|
+
|
|
27
|
+
# Run commands with secrets injected
|
|
28
|
+
kv run -- python app.py
|
|
29
|
+
kv run -- npm start
|
|
30
|
+
|
|
31
|
+
# List keys (values stay hidden)
|
|
32
|
+
kv ls
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## MCP Integration (AI Editors)
|
|
36
|
+
|
|
37
|
+
kv includes an MCP server so Cursor, Claude Code, and VS Code Copilot can manage secrets without ever seeing them.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Auto-configure your editor (one command)
|
|
41
|
+
kv setup cursor
|
|
42
|
+
kv setup claude-code
|
|
43
|
+
kv setup vscode
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That's it. Your AI agent now has access to these tools:
|
|
47
|
+
|
|
48
|
+
| Tool | Profile | What it does |
|
|
49
|
+
|------|---------|-------------|
|
|
50
|
+
| `kv_status` | safe | Check if kv is initialized |
|
|
51
|
+
| `kv_envs` | safe | List environments (dev, staging, prod) |
|
|
52
|
+
| `kv_list` | safe | List secret names (no values) |
|
|
53
|
+
| `kv_run` | safe | Run commands with secrets injected |
|
|
54
|
+
| `kv_set` | mutate | Store a secret (opt-in) |
|
|
55
|
+
| `kv_rm` | mutate | Remove a secret (opt-in) |
|
|
56
|
+
| `kv_get` | reveal | Read a secret value (opt-in) |
|
|
57
|
+
|
|
58
|
+
**Security profiles** control what your AI can do:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Default: safe only (list + run)
|
|
62
|
+
kv setup cursor
|
|
63
|
+
|
|
64
|
+
# Allow storing secrets
|
|
65
|
+
kv setup cursor --allow-mutate
|
|
66
|
+
|
|
67
|
+
# Allow reading values (use with caution)
|
|
68
|
+
kv setup cursor --allow-reveal
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## How It Works
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
You: "run the tests"
|
|
75
|
+
|
|
76
|
+
AI Agent kv
|
|
77
|
+
| |
|
|
78
|
+
|-- kv_run ["pytest"] --------->|
|
|
79
|
+
| |-- decrypt secrets
|
|
80
|
+
| |-- inject into env
|
|
81
|
+
| |-- subprocess.run(pytest)
|
|
82
|
+
| |-- return exit code only
|
|
83
|
+
|<-- "exit code: 0" -----------|
|
|
84
|
+
|
|
85
|
+
Secret values never appear in the chat.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Encryption
|
|
89
|
+
|
|
90
|
+
- **Algorithm:** ChaCha20-Poly1305 (AEAD)
|
|
91
|
+
- **Key derivation:** BLAKE2b with environment name as context
|
|
92
|
+
- **Storage:** Binary `.enc` files — safe to commit to git
|
|
93
|
+
- **Master key:** Stored in `.secrets/key` — add to `.gitignore`
|
|
94
|
+
|
|
95
|
+
## Multi-Environment
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Switch environments
|
|
99
|
+
kv env staging
|
|
100
|
+
kv env prod
|
|
101
|
+
|
|
102
|
+
# Set per-environment secrets
|
|
103
|
+
kv set API_KEY sk-live-prod --env prod
|
|
104
|
+
kv set API_KEY sk-test-dev --env dev
|
|
105
|
+
|
|
106
|
+
# Run in specific environment
|
|
107
|
+
kv run --env prod -- python deploy.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## All Commands
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
kv init Initialize kv in current project
|
|
114
|
+
kv set KEY VAL Store an encrypted secret
|
|
115
|
+
kv get KEY Decrypt and print a secret
|
|
116
|
+
kv ls List secret names
|
|
117
|
+
kv rm KEY Remove a secret
|
|
118
|
+
kv run -- CMD Run command with secrets in env
|
|
119
|
+
kv envs List environments
|
|
120
|
+
kv env NAME Switch default environment
|
|
121
|
+
kv export Export as .env format
|
|
122
|
+
kv import FILE Import from .env file
|
|
123
|
+
kv status Show project info
|
|
124
|
+
kv setup EDITOR Configure MCP for your editor
|
|
125
|
+
kv version Print version
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Key Sharing
|
|
129
|
+
|
|
130
|
+
Share the master key with teammates out-of-band (Signal, 1Password, etc.):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Export key as portable string
|
|
134
|
+
kv export-key
|
|
135
|
+
# kvkey_dGhpcyBpcyBhIHRlc3Qga2V5...
|
|
136
|
+
|
|
137
|
+
# Teammate imports it
|
|
138
|
+
kv import-key kvkey_dGhpcyBpcyBhIHRlc3Qga2V5...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `.enc` files are safe to commit — without the key, they're just noise.
|
|
142
|
+
|
|
143
|
+
## Security Model
|
|
144
|
+
|
|
145
|
+
| What | Where | Safe to share? |
|
|
146
|
+
|------|-------|---------------|
|
|
147
|
+
| `.enc` files | Project dir | Yes (commit to git) |
|
|
148
|
+
| Master key | `.secrets/key` | No (share via secure channel) |
|
|
149
|
+
| Plaintext | Never on disk | N/A |
|
|
150
|
+
|
|
151
|
+
- Zero-knowledge design — even the cloud sync server (coming soon) never sees plaintext
|
|
152
|
+
- Encrypted blobs are opaque to anyone without the key
|
|
153
|
+
- `kv run` uses `stdout=DEVNULL` — your AI agent only sees exit codes, never output
|
|
154
|
+
|
|
155
|
+
## Requirements
|
|
156
|
+
|
|
157
|
+
- Python 3.10+
|
|
158
|
+
- `cryptography` (installed automatically)
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# kv — Design Document
|
|
2
|
+
|
|
3
|
+
## What
|
|
4
|
+
Encrypted secrets management CLI for developers. Local-first, CLI-first, designed to grow into a paid team product.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
- Dev teams share `.env` files through Slack/email (insecure)
|
|
8
|
+
- dotenvx = encryption only, no team features
|
|
9
|
+
- Doppler = $21/user, cloud-only
|
|
10
|
+
- Vault = overkill for small teams
|
|
11
|
+
- **Gap**: Simple CLI + real encryption + multi-environment + future team sync
|
|
12
|
+
|
|
13
|
+
## Name Choice: `kv`
|
|
14
|
+
- Two letters, universally understood (key-value)
|
|
15
|
+
- Reads naturally: `kv set`, `kv get`, `kv run`
|
|
16
|
+
- Rejected: `stash` (git collision), `seal` (vague), `crypt` (scary), `vault` (taken)
|
|
17
|
+
|
|
18
|
+
## Dependency Choice
|
|
19
|
+
- **`cryptography`** (single dependency) — Python stdlib has no symmetric cipher
|
|
20
|
+
- ChaCha20-Poly1305 AEAD — same as WireGuard, TLS 1.3
|
|
21
|
+
- Industry-standard, audited, constant-time
|
|
22
|
+
|
|
23
|
+
## Encryption Architecture
|
|
24
|
+
- **Master key**: 32 random bytes, stored as base64url in `.secrets/key`
|
|
25
|
+
- **Per-env keys**: Derived via BLAKE2b keyed hash (stdlib) — no per-env key storage
|
|
26
|
+
- **Cipher**: ChaCha20-Poly1305 with 12-byte random nonce + environment name as AAD
|
|
27
|
+
- **Storage**: Entire secrets dict encrypted as single JSON blob (no key name leakage)
|
|
28
|
+
|
|
29
|
+
## Storage Format
|
|
30
|
+
```
|
|
31
|
+
.secrets/
|
|
32
|
+
key # Master key. NEVER committed.
|
|
33
|
+
config.json # Project metadata
|
|
34
|
+
dev.enc # KV\x00 + version(1) + nonce(12) + ciphertext+tag
|
|
35
|
+
staging.enc
|
|
36
|
+
.gitignore # Auto-generated
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## CLI Interface
|
|
40
|
+
```
|
|
41
|
+
kv init Initialize project
|
|
42
|
+
kv set KEY=VALUE [-e ENV] Set a secret
|
|
43
|
+
kv set KEY [-e ENV] Set interactively (hidden input)
|
|
44
|
+
kv get KEY [-e ENV] Decrypt and print
|
|
45
|
+
kv ls [-e ENV] [--reveal] List keys
|
|
46
|
+
kv run COMMAND [-e ENV] Run with secrets injected
|
|
47
|
+
kv export [-e ENV] [-o FILE] Export as .env
|
|
48
|
+
kv import FILE [-e ENV] Import .env file
|
|
49
|
+
kv rm KEY [-e ENV] Remove a secret
|
|
50
|
+
kv envs List environments
|
|
51
|
+
kv env create NAME Create environment
|
|
52
|
+
kv env copy SRC DST Copy between environments
|
|
53
|
+
kv status Project status
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Key Decisions
|
|
57
|
+
1. **Single blob per env** — no key name leakage, atomic reads/writes
|
|
58
|
+
2. **BLAKE2b for key derivation** — master key is high-entropy, no slow KDF needed
|
|
59
|
+
3. **`find_project_root()` walks up dirs** — works from subdirectories (like git)
|
|
60
|
+
4. **Atomic writes** — tmp + `os.replace()` prevents corruption
|
|
61
|
+
5. **`.enc` safe to commit, `key` is not** — enables future git-based team sharing
|
|
62
|
+
|
|
63
|
+
## Monetization
|
|
64
|
+
- **Free**: Local CLI (12 commands, full encryption, multi-environment)
|
|
65
|
+
- **$15/team/month**: Cloud sync, team management, CI/CD tokens, basic RBAC
|
|
66
|
+
- **$99/team/month**: Rotation automation, advanced RBAC, audit logs
|
|
67
|
+
- Undercuts Doppler ($21/user x 5 = $105/mo) with per-team pricing
|
|
68
|
+
|
|
69
|
+
## Package Structure
|
|
70
|
+
```
|
|
71
|
+
kv/ # CLI package
|
|
72
|
+
__init__.py # Version
|
|
73
|
+
__main__.py # Entry point, Windows fixes
|
|
74
|
+
cli.py # argparse (20 commands), dispatch, ANSI output
|
|
75
|
+
cli_remote.py # Remote command handlers (login, push/pull, team, token)
|
|
76
|
+
crypto.py # ChaCha20 encrypt/decrypt, BLAKE2b derivation, kvkey_ export/import
|
|
77
|
+
store.py # .enc file format, secret CRUD, raw blob read/write, atomic writes
|
|
78
|
+
env.py # Secret injection, .env import/export
|
|
79
|
+
config.py # Project init, config.json, find_project_root, sync state
|
|
80
|
+
auth.py # Session management (~/.kv/session.json), auth headers
|
|
81
|
+
remote.py # HTTP client (urllib.request), all API calls
|
|
82
|
+
sync.py # Push/pull orchestration, hash computation, conflict detection
|
|
83
|
+
|
|
84
|
+
kv_server/ # API server package
|
|
85
|
+
__init__.py # Version
|
|
86
|
+
__main__.py # uvicorn entry (python -m kv_server)
|
|
87
|
+
app.py # FastAPI app factory, CORS, lifespan
|
|
88
|
+
config.py # Settings from env vars
|
|
89
|
+
database.py # SQLAlchemy models (5 tables), engine, session
|
|
90
|
+
models.py # Pydantic request/response schemas
|
|
91
|
+
auth.py # JWT, bcrypt, API token validation
|
|
92
|
+
billing.py # Stripe integration (checkout, portal, webhooks)
|
|
93
|
+
middleware.py # Rate limiting placeholder
|
|
94
|
+
routes/
|
|
95
|
+
__init__.py # Route registration
|
|
96
|
+
auth_routes.py # /auth/register, /auth/login, /auth/refresh
|
|
97
|
+
sync_routes.py # /sync/push, /sync/pull, /sync/status
|
|
98
|
+
team_routes.py # /team/create, /team/invite, /team/members, /team/revoke
|
|
99
|
+
token_routes.py # /tokens/create, /tokens/list, /tokens/revoke
|
|
100
|
+
billing_routes.py # /billing/status, /billing/checkout, /billing/portal, /billing/webhook
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Build Status — Local CLI (v0.1)
|
|
104
|
+
- [x] Package entry (__init__, __main__)
|
|
105
|
+
- [x] Encryption engine (crypto.py) — ChaCha20-Poly1305, BLAKE2b derivation, key I/O
|
|
106
|
+
- [x] Project config (config.py) — init, find_project_root, env registry
|
|
107
|
+
- [x] Secret store (store.py) — .enc format, CRUD, atomic writes, tamper detection
|
|
108
|
+
- [x] CLI commands (cli.py) — 12 local commands, ANSI output
|
|
109
|
+
- [x] Env injection + import/export (env.py) — subprocess injection, .env parsing
|
|
110
|
+
- [x] Full test suite — 20/20 local tests passing
|
|
111
|
+
- [x] README.md
|
|
112
|
+
|
|
113
|
+
## Build Status — Paid Tier (v0.2)
|
|
114
|
+
- [x] Server foundation (kv_server/) — FastAPI, SQLAlchemy, 5 DB tables, JWT auth
|
|
115
|
+
- [x] CLI auth (kv/auth.py) — login/signup/logout, session at ~/.kv/session.json
|
|
116
|
+
- [x] HTTP client (kv/remote.py) — stdlib urllib.request, all API calls
|
|
117
|
+
- [x] Push/pull sync (kv/sync.py) — encrypted blob transfer, hash-based conflict detection
|
|
118
|
+
- [x] Team management — create, invite, members, revoke, kvkey_ key sharing
|
|
119
|
+
- [x] CI/CD tokens — scoped API tokens (pull/push/admin), env restriction, expiry
|
|
120
|
+
- [x] Stripe billing — checkout, portal, webhook handler, trial period
|
|
121
|
+
- [x] CLI commands (cli.py) — expanded to 20 commands total
|
|
122
|
+
- [x] End-to-end test suite — 20/20 integration tests passing
|
|
123
|
+
|
|
124
|
+
## Paid Tier Architecture
|
|
125
|
+
- **Zero-knowledge**: Server stores raw .enc blobs, never sees plaintext
|
|
126
|
+
- **Auth**: JWT (1h access + 30d refresh) for CLI, API tokens (kvt_...) for CI/CD
|
|
127
|
+
- **Key sharing**: `kvkey_` prefix + base64url master key, shared out-of-band
|
|
128
|
+
- **Sync**: Client reads .enc blob -> base64 -> POST /sync/push, reverse for pull
|
|
129
|
+
- **Conflict resolution**: Last-write-wins with version numbers + blob hashes
|
|
130
|
+
- **Billing**: $15/team/month, 14-day trial, Stripe Checkout + webhooks
|
|
131
|
+
- **Server deps**: fastapi, uvicorn, sqlalchemy[asyncio], aiosqlite, python-jose, bcrypt, stripe
|
|
132
|
+
|
|
133
|
+
## Gotchas Found During Build
|
|
134
|
+
- `argparse.REMAINDER` field must not collide with subparser `dest` field name — renamed to `cmd`
|
|
135
|
+
- `subprocess.run` needs list args (not joined string) to preserve quoting for `python -c "..."`
|
|
136
|
+
- Optional flags (`-e`, `-q`) MUST come before REMAINDER positional in parser definition
|
|
137
|
+
- passlib + bcrypt compatibility broken on Python 3.12 — use `bcrypt` directly instead
|
|
138
|
+
- SQLite returns naive datetimes — normalize to UTC before comparing with aware datetimes
|
|
139
|
+
- Pydantic `EmailStr` requires `email-validator` package — use plain `str` to avoid extra dep
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# kv
|
|
2
|
+
|
|
3
|
+
Encrypted secrets management for developers. Set, get, inject — no plaintext `.env` files.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install cryptography
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
python -m kv init
|
|
11
|
+
python -m kv set DATABASE_URL=postgres://localhost/mydb
|
|
12
|
+
python -m kv set API_KEY # prompts for value (hidden)
|
|
13
|
+
python -m kv run python app.py # secrets injected as env vars
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
Teams share `.env` files through Slack and email. That's plaintext secrets in chat logs, email servers, and clipboard history. Existing solutions are either encryption-only (dotenvx), cloud-only and expensive (Doppler at $21/user), or overkill (HashiCorp Vault).
|
|
19
|
+
|
|
20
|
+
**kv** encrypts secrets locally with ChaCha20-Poly1305 (same cipher as WireGuard and TLS 1.3), supports multiple environments, and injects secrets into any command — all from a two-letter CLI.
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
kv init Initialize project
|
|
26
|
+
kv set KEY=VALUE [-e ENV] Set a secret
|
|
27
|
+
kv set KEY [-e ENV] Set interactively (hidden input)
|
|
28
|
+
kv get KEY [-e ENV] Decrypt and print a secret
|
|
29
|
+
kv ls [-e ENV] [--reveal] List secrets (values hidden by default)
|
|
30
|
+
kv rm KEY [-e ENV] [-f] Remove a secret
|
|
31
|
+
kv run [-e ENV] [-q] COMMAND... Run command with secrets as env vars
|
|
32
|
+
kv export [-e ENV] [-o FILE] Export as .env format
|
|
33
|
+
kv import FILE [-e ENV] Import from .env file
|
|
34
|
+
kv envs List all environments
|
|
35
|
+
kv env create NAME Create a new environment
|
|
36
|
+
kv env copy SRC DST Copy secrets between environments
|
|
37
|
+
kv status Project overview
|
|
38
|
+
kv --version Print version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Quick start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Initialize — creates .secrets/ with master key and config
|
|
45
|
+
python -m kv init
|
|
46
|
+
|
|
47
|
+
# Store secrets
|
|
48
|
+
python -m kv set DATABASE_URL=postgres://localhost:5432/mydb
|
|
49
|
+
python -m kv set STRIPE_KEY=sk_test_abc123
|
|
50
|
+
python -m kv set SESSION_SECRET # hidden prompt, nothing on screen
|
|
51
|
+
|
|
52
|
+
# Retrieve
|
|
53
|
+
python -m kv get DATABASE_URL # prints raw value (pipe-friendly)
|
|
54
|
+
python -m kv ls # list keys, values masked
|
|
55
|
+
python -m kv ls --reveal # list keys with decrypted values
|
|
56
|
+
|
|
57
|
+
# Inject into any command
|
|
58
|
+
python -m kv run python app.py
|
|
59
|
+
python -m kv run node server.js
|
|
60
|
+
python -m kv run -e staging python migrate.py
|
|
61
|
+
|
|
62
|
+
# Multiple environments
|
|
63
|
+
python -m kv set -e staging DATABASE_URL=postgres://staging-host/db
|
|
64
|
+
python -m kv set -e prod DATABASE_URL=postgres://prod-host/db
|
|
65
|
+
python -m kv envs # dev, staging, prod
|
|
66
|
+
python -m kv env copy dev staging # clone secrets across envs
|
|
67
|
+
|
|
68
|
+
# Import/export
|
|
69
|
+
python -m kv export -o .env # decrypt to .env file
|
|
70
|
+
python -m kv import legacy.env -e prod # encrypt from .env file
|
|
71
|
+
|
|
72
|
+
# Remove
|
|
73
|
+
python -m kv rm API_KEY # confirmation prompt
|
|
74
|
+
python -m kv rm API_KEY -f # skip confirmation
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
### Encryption
|
|
80
|
+
|
|
81
|
+
- **Cipher**: ChaCha20-Poly1305 AEAD (authenticated encryption with associated data)
|
|
82
|
+
- **Master key**: 32 random bytes generated at `kv init`, stored in `.secrets/key`
|
|
83
|
+
- **Per-environment keys**: Derived from master key via BLAKE2b keyed hash — deterministic, no per-env key storage needed
|
|
84
|
+
- **Nonce**: 12 random bytes per write, prepended to ciphertext
|
|
85
|
+
- **AAD**: Environment name is bound as additional authenticated data — tampering or swapping `.enc` files between environments is detected
|
|
86
|
+
- **Payload**: All secrets for an environment are encrypted as a single JSON blob — key names are never visible without the master key
|
|
87
|
+
|
|
88
|
+
### Storage
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
your-project/
|
|
92
|
+
.secrets/
|
|
93
|
+
key Master key (base64url). NEVER commit this.
|
|
94
|
+
config.json Project metadata (environments, cipher, version)
|
|
95
|
+
dev.enc Encrypted secrets for dev
|
|
96
|
+
staging.enc Encrypted secrets for staging
|
|
97
|
+
prod.enc Encrypted secrets for prod
|
|
98
|
+
.gitignore Auto-generated: ignores key, allows *.enc
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The `.gitignore` inside `.secrets/` is auto-configured:
|
|
102
|
+
- `key` is ignored (your master key never enters git)
|
|
103
|
+
- `*.enc` files are allowed (encrypted blobs are safe to commit)
|
|
104
|
+
- `config.json` is allowed (no sensitive data)
|
|
105
|
+
|
|
106
|
+
This means `.enc` files can live in your repo. Anyone without the `key` file sees binary gibberish. Share the key through a secure channel once — after that, secrets travel with the code.
|
|
107
|
+
|
|
108
|
+
### Binary format
|
|
109
|
+
|
|
110
|
+
Each `.enc` file:
|
|
111
|
+
```
|
|
112
|
+
Bytes 0-2: KV\x00 Magic bytes
|
|
113
|
+
Byte 3: 0x01 Version
|
|
114
|
+
Bytes 4-15: nonce 12-byte random nonce
|
|
115
|
+
Bytes 16+: ciphertext ChaCha20-Poly1305 encrypted payload + 16-byte auth tag
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Decrypted payload (JSON):
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"_meta": {"updated": "2026-02-18T14:35:00+00:00", "count": 3},
|
|
122
|
+
"secrets": {
|
|
123
|
+
"DATABASE_URL": "postgres://localhost/mydb",
|
|
124
|
+
"API_KEY": "sk-test123",
|
|
125
|
+
"SESSION_SECRET": "a1b2c3d4"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Security model
|
|
131
|
+
|
|
132
|
+
**What's protected:**
|
|
133
|
+
- Secret values are encrypted at rest with a 256-bit key
|
|
134
|
+
- Secret key names are encrypted (single-blob-per-env design)
|
|
135
|
+
- Environment isolation — per-env derived keys, AAD binding
|
|
136
|
+
- Tamper detection — Poly1305 authentication tag catches any modification
|
|
137
|
+
- Atomic writes — tmp file + `os.replace()` prevents partial writes on crash
|
|
138
|
+
|
|
139
|
+
**What's NOT protected (yet):**
|
|
140
|
+
- The master key in `.secrets/key` is plaintext on disk (protected by file permissions, `.gitignore`)
|
|
141
|
+
- No access control — anyone with the key can read/write all environments
|
|
142
|
+
- No audit log — no record of who changed what
|
|
143
|
+
- No key rotation — changing the master key requires re-encrypting everything manually
|
|
144
|
+
|
|
145
|
+
These are the features that make up the paid team tier (cloud sync, RBAC, audit logs, rotation).
|
|
146
|
+
|
|
147
|
+
## Architecture
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
kv/
|
|
151
|
+
__init__.py Package version
|
|
152
|
+
__main__.py Entry point (python -m kv), Windows terminal fixes
|
|
153
|
+
crypto.py ChaCha20-Poly1305 encrypt/decrypt, BLAKE2b key derivation
|
|
154
|
+
store.py SecretStore class, .enc binary format, atomic CRUD
|
|
155
|
+
config.py Project init, find_project_root(), environment registry
|
|
156
|
+
cli.py argparse commands, ANSI-colored output
|
|
157
|
+
env.py Subprocess injection, .env import/export
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Single dependency**: `cryptography` (for ChaCha20-Poly1305). Everything else is stdlib.
|
|
161
|
+
|
|
162
|
+
## Platform
|
|
163
|
+
|
|
164
|
+
Works on Windows, macOS, and Linux. Tested primarily on Windows with PowerShell.
|
|
165
|
+
|
|
166
|
+
Run from any subdirectory — `kv` walks up the directory tree to find `.secrets/` (like `git` finds `.git/`).
|
|
167
|
+
|
|
168
|
+
```powershell
|
|
169
|
+
# From project root
|
|
170
|
+
python -m kv set FOO=bar
|
|
171
|
+
|
|
172
|
+
# From a subdirectory — still works
|
|
173
|
+
cd src/
|
|
174
|
+
python -m kv get FOO # finds .secrets/ in parent
|
|
175
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Entry point for python -m kv."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Windows terminal setup
|
|
7
|
+
if os.name == "nt":
|
|
8
|
+
os.system("") # enable ANSI escape codes
|
|
9
|
+
for stream in (sys.stdout, sys.stderr, sys.stdin):
|
|
10
|
+
if hasattr(stream, "reconfigure"):
|
|
11
|
+
stream.reconfigure(encoding="utf-8")
|
|
12
|
+
|
|
13
|
+
from .cli import main
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Authentication for kv CLI.
|
|
2
|
+
|
|
3
|
+
Manages user sessions, login/signup flows, and token storage.
|
|
4
|
+
Session stored at ~/.kv/session.json (per-user, not per-project).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SESSION_DIR = ".kv"
|
|
12
|
+
SESSION_FILE = "session.json"
|
|
13
|
+
|
|
14
|
+
# Default server URL — overridden by KV_API_URL env var
|
|
15
|
+
DEFAULT_API_URL = "http://127.0.0.1:8000"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_user_config_dir():
|
|
19
|
+
"""Get ~/.kv/ directory, create if needed."""
|
|
20
|
+
home = os.path.expanduser("~")
|
|
21
|
+
d = os.path.join(home, SESSION_DIR)
|
|
22
|
+
os.makedirs(d, exist_ok=True)
|
|
23
|
+
# Restrict permissions on Unix (owner-only access)
|
|
24
|
+
if os.name != "nt":
|
|
25
|
+
os.chmod(d, 0o700)
|
|
26
|
+
return d
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def session_path():
|
|
30
|
+
"""Full path to session.json."""
|
|
31
|
+
return os.path.join(get_user_config_dir(), SESSION_FILE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_session():
|
|
35
|
+
"""Load the current session, or None if not logged in."""
|
|
36
|
+
path = session_path()
|
|
37
|
+
if not os.path.isfile(path):
|
|
38
|
+
return None
|
|
39
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
40
|
+
return json.load(f)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_session(session):
|
|
44
|
+
"""Write session to disk."""
|
|
45
|
+
path = session_path()
|
|
46
|
+
tmp = path + ".tmp"
|
|
47
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
48
|
+
json.dump(session, f, indent=2)
|
|
49
|
+
f.write("\n")
|
|
50
|
+
os.replace(tmp, path)
|
|
51
|
+
# Restrict permissions on Unix (owner-only read/write)
|
|
52
|
+
if os.name != "nt":
|
|
53
|
+
os.chmod(path, 0o600)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def delete_session():
|
|
57
|
+
"""Remove session file (logout)."""
|
|
58
|
+
path = session_path()
|
|
59
|
+
if os.path.isfile(path):
|
|
60
|
+
os.remove(path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_api_url():
|
|
64
|
+
"""Get the API server URL."""
|
|
65
|
+
session = load_session()
|
|
66
|
+
if session and session.get("api_url"):
|
|
67
|
+
return session["api_url"]
|
|
68
|
+
return os.environ.get("KV_API_URL", DEFAULT_API_URL)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_auth_headers():
|
|
72
|
+
"""Get Authorization headers from session or KV_TOKEN env var.
|
|
73
|
+
|
|
74
|
+
Returns dict with Authorization header, or empty dict.
|
|
75
|
+
"""
|
|
76
|
+
# CI/CD token takes priority
|
|
77
|
+
env_token = os.environ.get("KV_TOKEN")
|
|
78
|
+
if env_token:
|
|
79
|
+
return {"Authorization": f"Token {env_token}"}
|
|
80
|
+
|
|
81
|
+
session = load_session()
|
|
82
|
+
if session and session.get("token"):
|
|
83
|
+
return {"Authorization": f"Bearer {session['token']}"}
|
|
84
|
+
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def require_session():
|
|
89
|
+
"""Get current session or raise with helpful message."""
|
|
90
|
+
session = load_session()
|
|
91
|
+
if not session:
|
|
92
|
+
raise RuntimeError("not logged in — run 'python -m kv login' first")
|
|
93
|
+
return session
|