keystash 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.
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest, windows-latest]
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ exclude:
16
+ - os: macos-latest
17
+ python-version: "3.9"
18
+ - os: windows-latest
19
+ python-version: "3.9"
20
+ runs-on: ${{ matrix.os }}
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+ - name: Install
27
+ run: pip install -e ".[dev]"
28
+ - name: Test
29
+ run: pytest -q
30
+ - name: Lint
31
+ if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
32
+ run: ruff check src tests
@@ -0,0 +1,44 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes with PyPI Trusted Publishing (OIDC) — no API token stored.
4
+ # One-time setup on pypi.org: Account settings → Publishing → add a
5
+ # "pending publisher" with:
6
+ # owner: thu-lawyer, repository: keystash, workflow: publish.yml,
7
+ # environment: pypi
8
+ # Then run this workflow from the Actions tab (or publish a GitHub release).
9
+
10
+ on:
11
+ workflow_dispatch:
12
+ release:
13
+ types: [published]
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Build
24
+ run: |
25
+ pip install build
26
+ python -m build
27
+ - uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write
38
+ steps:
39
+ - uses: actions/download-artifact@v4
40
+ with:
41
+ name: dist
42
+ path: dist/
43
+ - name: Publish
44
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+
9
+ # Environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Tooling
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .mypy_cache/
17
+ .coverage
18
+ htmlcov/
19
+
20
+ # Editors / OS
21
+ .idea/
22
+ .vscode/
23
+ .DS_Store
24
+
25
+ # NEVER commit a real vault
26
+ *.keystash
27
+ *.kstash
28
+ vault.enc*
keystash-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thu-lawyer
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,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: keystash
3
+ Version: 0.1.0
4
+ Summary: Local-first encrypted vault for API keys, tokens and passwords — one file, fuzzy search, env injection, expiry tracking.
5
+ Project-URL: Homepage, https://github.com/thu-lawyer/keystash
6
+ Project-URL: Repository, https://github.com/thu-lawyer/keystash
7
+ Project-URL: Issues, https://github.com/thu-lawyer/keystash/issues
8
+ Project-URL: Changelog, https://github.com/thu-lawyer/keystash/releases
9
+ Author: thu-lawyer
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-key,cli,developer-tools,password,password-manager,secrets,token,vault
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Security
27
+ Classifier: Topic :: Security :: Cryptography
28
+ Classifier: Topic :: Utilities
29
+ Requires-Python: >=3.9
30
+ Requires-Dist: cryptography>=41.0
31
+ Requires-Dist: rich>=13.0
32
+ Requires-Dist: typer>=0.12
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.4; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # keystash 🔑
39
+
40
+ **Local-first encrypted vault for API keys, tokens and passwords — one file, fuzzy search, env injection, expiry tracking.**
41
+
42
+ Your LLM API keys, cloud tokens and passwords are scattered across `.env` files, shell
43
+ histories and notes apps. `keystash` puts them in **one encrypted file** that you own:
44
+ no server, no account, no subscription. Sync that file with iCloud / Dropbox / SeaDrive /
45
+ Syncthing — it's ciphertext, so syncing it is safe.
46
+
47
+ ```bash
48
+ pip install keystash
49
+ ```
50
+
51
+ ## Why keystash
52
+
53
+ | | keystash | pass / gopass | Bitwarden / 1Password | Infisical / Vault |
54
+ |---|---|---|---|---|
55
+ | Setup | `pip install` + one password | GPG key ceremony | Account + app | Self-host a server |
56
+ | Storage | one encrypted file you own | many GPG files | vendor cloud | server |
57
+ | Offline | ✅ always | ✅ | partial | ❌ |
58
+ | Dev workflow (env injection, `run`) | ✅ built-in | ❌ | ❌ | ✅ (heavy) |
59
+ | Token expiry tracking | ✅ built-in | ❌ | ❌ | enterprise |
60
+
61
+ ## Quick start
62
+
63
+ ```bash
64
+ # 1. Create your vault (encrypted with AES-128-CBC + HMAC, PBKDF2-HMAC-SHA256 600k iters)
65
+ keystash init
66
+
67
+ # 2. Store a secret
68
+ keystash add openai --secret sk-... --tags llm,prod --expires 2027-01-31 --env-var OPENAI_API_KEY
69
+
70
+ # 3. Retrieve it
71
+ keystash get openai # masked preview
72
+ keystash get openai -c # → clipboard, auto-clears after 30 s
73
+ keystash get openai -q # raw secret for scripts: export K=$(keystash get openai -q)
74
+
75
+ # 4. Find things (fuzzy)
76
+ keystash ls # everything, expiry warnings included
77
+ keystash ls oprod # fuzzy: matches openai-prod
78
+ keystash ls --tag llm --json
79
+
80
+ # 5. Inject secrets into any command — nothing touches your shell or disk
81
+ keystash run -n openai -n anthropic -- python train.py
82
+ keystash run --tag llm -- python train.py
83
+ eval "$(keystash env --tag llm)" # or export them explicitly
84
+
85
+ # 6. Migrate off plaintext .env files
86
+ keystash import .env # then delete the .env file
87
+ ```
88
+
89
+ ## Commands
90
+
91
+ | Command | Purpose |
92
+ |---|---|
93
+ | `init` | Create the vault |
94
+ | `add NAME` | Store a secret (`-s` value, `-g LEN` to generate, `-t` tags, `--expires YYYY-MM-DD`, `--env-var`) |
95
+ | `get NAME` | Show (`-r` reveal), copy (`-c`), raw output (`-q`) |
96
+ | `ls [QUERY]` | List / fuzzy search (`--tag`, `--json`) |
97
+ | `edit NAME` | Update any field in place |
98
+ | `rm NAME` | Delete an entry |
99
+ | `gen [LEN]` | Generate a strong secret (`--save NAME` to store it) |
100
+ | `env NAME…` / `--tag` | Print `export` lines for shell eval |
101
+ | `run -n NAME… -- CMD` | Run a command with secrets injected as env vars (`--tag` selects by tag) |
102
+ | `import FILE` | Bulk-import `.env` or JSON |
103
+ | `export` | Export metadata (or secrets with `--with-secrets`) as JSON / dotenv |
104
+ | `status` | Vault health + expired / expiring-soon report |
105
+
106
+ ## Multi-machine sync
107
+
108
+ The vault is a single encrypted file. Point `KEYSTASH_VAULT` at any synced folder:
109
+
110
+ ```bash
111
+ export KEYSTASH_VAULT="~/CloudStorage/SeaDrive/vault.json" # or iCloud, Dropbox, …
112
+ ```
113
+
114
+ Each write re-encrypts with a fresh random salt, so last-writer-wins applies —
115
+ prefer one writer per vault at a time, like any sync file.
116
+
117
+ ## Security model
118
+
119
+ - **Cipher:** Fernet (AES-128-CBC + HMAC-SHA256, encrypt-then-MAC) via `cryptography`.
120
+ - **Key derivation:** PBKDF2-HMAC-SHA256, 600 000 iterations, per-save 128-bit random salt.
121
+ - **File mode:** `0600`; nothing is ever written to disk in plaintext.
122
+ - **Clipboard:** copied secrets are auto-cleared after 30 s (best effort, detached process).
123
+ - **No network.** No telemetry. The CLI is fully offline.
124
+ - Secrets live in process memory only while a command runs; Python cannot guarantee
125
+ zeroization after exit — the same is true of any CLI in a GC'd language.
126
+
127
+ ## Environment variables
128
+
129
+ | Variable | Purpose |
130
+ |---|---|
131
+ | `KEYSTASH_VAULT` | Vault file path (also `--vault`) |
132
+ | `KEYSTASH_PASSWORD` | Master password (for scripts/CI; prefer the interactive prompt) |
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ git clone https://github.com/thu-lawyer/keystash && cd keystash
138
+ uv pip install -e ".[dev]"
139
+ pytest
140
+ ```
141
+
142
+ ## License
143
+
144
+ [MIT](LICENSE)
@@ -0,0 +1,107 @@
1
+ # keystash 🔑
2
+
3
+ **Local-first encrypted vault for API keys, tokens and passwords — one file, fuzzy search, env injection, expiry tracking.**
4
+
5
+ Your LLM API keys, cloud tokens and passwords are scattered across `.env` files, shell
6
+ histories and notes apps. `keystash` puts them in **one encrypted file** that you own:
7
+ no server, no account, no subscription. Sync that file with iCloud / Dropbox / SeaDrive /
8
+ Syncthing — it's ciphertext, so syncing it is safe.
9
+
10
+ ```bash
11
+ pip install keystash
12
+ ```
13
+
14
+ ## Why keystash
15
+
16
+ | | keystash | pass / gopass | Bitwarden / 1Password | Infisical / Vault |
17
+ |---|---|---|---|---|
18
+ | Setup | `pip install` + one password | GPG key ceremony | Account + app | Self-host a server |
19
+ | Storage | one encrypted file you own | many GPG files | vendor cloud | server |
20
+ | Offline | ✅ always | ✅ | partial | ❌ |
21
+ | Dev workflow (env injection, `run`) | ✅ built-in | ❌ | ❌ | ✅ (heavy) |
22
+ | Token expiry tracking | ✅ built-in | ❌ | ❌ | enterprise |
23
+
24
+ ## Quick start
25
+
26
+ ```bash
27
+ # 1. Create your vault (encrypted with AES-128-CBC + HMAC, PBKDF2-HMAC-SHA256 600k iters)
28
+ keystash init
29
+
30
+ # 2. Store a secret
31
+ keystash add openai --secret sk-... --tags llm,prod --expires 2027-01-31 --env-var OPENAI_API_KEY
32
+
33
+ # 3. Retrieve it
34
+ keystash get openai # masked preview
35
+ keystash get openai -c # → clipboard, auto-clears after 30 s
36
+ keystash get openai -q # raw secret for scripts: export K=$(keystash get openai -q)
37
+
38
+ # 4. Find things (fuzzy)
39
+ keystash ls # everything, expiry warnings included
40
+ keystash ls oprod # fuzzy: matches openai-prod
41
+ keystash ls --tag llm --json
42
+
43
+ # 5. Inject secrets into any command — nothing touches your shell or disk
44
+ keystash run -n openai -n anthropic -- python train.py
45
+ keystash run --tag llm -- python train.py
46
+ eval "$(keystash env --tag llm)" # or export them explicitly
47
+
48
+ # 6. Migrate off plaintext .env files
49
+ keystash import .env # then delete the .env file
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ | Command | Purpose |
55
+ |---|---|
56
+ | `init` | Create the vault |
57
+ | `add NAME` | Store a secret (`-s` value, `-g LEN` to generate, `-t` tags, `--expires YYYY-MM-DD`, `--env-var`) |
58
+ | `get NAME` | Show (`-r` reveal), copy (`-c`), raw output (`-q`) |
59
+ | `ls [QUERY]` | List / fuzzy search (`--tag`, `--json`) |
60
+ | `edit NAME` | Update any field in place |
61
+ | `rm NAME` | Delete an entry |
62
+ | `gen [LEN]` | Generate a strong secret (`--save NAME` to store it) |
63
+ | `env NAME…` / `--tag` | Print `export` lines for shell eval |
64
+ | `run -n NAME… -- CMD` | Run a command with secrets injected as env vars (`--tag` selects by tag) |
65
+ | `import FILE` | Bulk-import `.env` or JSON |
66
+ | `export` | Export metadata (or secrets with `--with-secrets`) as JSON / dotenv |
67
+ | `status` | Vault health + expired / expiring-soon report |
68
+
69
+ ## Multi-machine sync
70
+
71
+ The vault is a single encrypted file. Point `KEYSTASH_VAULT` at any synced folder:
72
+
73
+ ```bash
74
+ export KEYSTASH_VAULT="~/CloudStorage/SeaDrive/vault.json" # or iCloud, Dropbox, …
75
+ ```
76
+
77
+ Each write re-encrypts with a fresh random salt, so last-writer-wins applies —
78
+ prefer one writer per vault at a time, like any sync file.
79
+
80
+ ## Security model
81
+
82
+ - **Cipher:** Fernet (AES-128-CBC + HMAC-SHA256, encrypt-then-MAC) via `cryptography`.
83
+ - **Key derivation:** PBKDF2-HMAC-SHA256, 600 000 iterations, per-save 128-bit random salt.
84
+ - **File mode:** `0600`; nothing is ever written to disk in plaintext.
85
+ - **Clipboard:** copied secrets are auto-cleared after 30 s (best effort, detached process).
86
+ - **No network.** No telemetry. The CLI is fully offline.
87
+ - Secrets live in process memory only while a command runs; Python cannot guarantee
88
+ zeroization after exit — the same is true of any CLI in a GC'd language.
89
+
90
+ ## Environment variables
91
+
92
+ | Variable | Purpose |
93
+ |---|---|
94
+ | `KEYSTASH_VAULT` | Vault file path (also `--vault`) |
95
+ | `KEYSTASH_PASSWORD` | Master password (for scripts/CI; prefer the interactive prompt) |
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ git clone https://github.com/thu-lawyer/keystash && cd keystash
101
+ uv pip install -e ".[dev]"
102
+ pytest
103
+ ```
104
+
105
+ ## License
106
+
107
+ [MIT](LICENSE)
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "keystash"
7
+ version = "0.1.0"
8
+ description = "Local-first encrypted vault for API keys, tokens and passwords — one file, fuzzy search, env injection, expiry tracking."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "thu-lawyer" }]
13
+ keywords = [
14
+ "password",
15
+ "password-manager",
16
+ "api-key",
17
+ "secrets",
18
+ "vault",
19
+ "cli",
20
+ "token",
21
+ "developer-tools",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Environment :: Console",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: MacOS",
29
+ "Operating System :: POSIX :: Linux",
30
+ "Operating System :: Microsoft :: Windows",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: Security",
38
+ "Topic :: Security :: Cryptography",
39
+ "Topic :: Utilities",
40
+ ]
41
+ dependencies = [
42
+ "typer>=0.12",
43
+ "rich>=13.0",
44
+ "cryptography>=41.0",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/thu-lawyer/keystash"
49
+ Repository = "https://github.com/thu-lawyer/keystash"
50
+ Issues = "https://github.com/thu-lawyer/keystash/issues"
51
+ Changelog = "https://github.com/thu-lawyer/keystash/releases"
52
+
53
+ [project.scripts]
54
+ keystash = "keystash.cli:app"
55
+
56
+ [project.optional-dependencies]
57
+ dev = ["pytest>=8.0", "ruff>=0.4"]
58
+
59
+ [tool.hatch.build.targets.wheel]
60
+ packages = ["src/keystash"]
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+
65
+ [tool.ruff]
66
+ line-length = 100
67
+ target-version = "py39"
68
+
69
+ [tool.ruff.lint]
70
+ # Keep Optional[...]/List[...] annotations: typer evaluates them at runtime,
71
+ # where `X | None` would fail on Python 3.9 — so the UP rules must stay off.
72
+ select = ["E4", "E7", "E9", "F", "I", "B", "DTZ"]
73
+ ignore = [
74
+ "B008", # typer's Option()/Argument() in defaults is the framework idiom
75
+ "DTZ011", # local-date semantics are intentional for expiry display
76
+ ]
@@ -0,0 +1,3 @@
1
+ """keystash — local-first encrypted vault for API keys, tokens and passwords."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow `python -m keystash`."""
2
+
3
+ from .cli import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()