credstore 0.3.7__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.
- credstore-0.3.7/.gitignore +232 -0
- credstore-0.3.7/PKG-INFO +190 -0
- credstore-0.3.7/README.md +165 -0
- credstore-0.3.7/config.json5 +10 -0
- credstore-0.3.7/credstore/__init__.py +75 -0
- credstore-0.3.7/credstore/__main__.py +892 -0
- credstore-0.3.7/credstore/_backend.py +196 -0
- credstore-0.3.7/credstore/_config.py +87 -0
- credstore-0.3.7/credstore/_enumerate.py +123 -0
- credstore-0.3.7/credstore/_resolver.py +106 -0
- credstore-0.3.7/credstore/_shell.py +261 -0
- credstore-0.3.7/credstore/_store.py +262 -0
- credstore-0.3.7/credstore/_tty.py +90 -0
- credstore-0.3.7/pyproject.toml +58 -0
- credstore-0.3.7/tests/__init__.py +0 -0
- credstore-0.3.7/tests/test_backend.py +335 -0
- credstore-0.3.7/tests/test_cli.py +824 -0
- credstore-0.3.7/tests/test_config.py +139 -0
- credstore-0.3.7/tests/test_resolver.py +108 -0
- credstore-0.3.7/tests/test_store.py +516 -0
- credstore-0.3.7/tests/test_tty.py +176 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Credential store encrypted backup
|
|
2
|
+
credentials.crypt
|
|
3
|
+
|
|
4
|
+
# WeChat session files (contain auth tokens)
|
|
5
|
+
wechat_*.json5
|
|
6
|
+
|
|
7
|
+
# Runtime logs
|
|
8
|
+
logs/
|
|
9
|
+
|
|
10
|
+
# Byte-compiled / optimized / DLL files
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[codz]
|
|
13
|
+
*$py.class
|
|
14
|
+
|
|
15
|
+
# C extensions
|
|
16
|
+
*.so
|
|
17
|
+
|
|
18
|
+
# Distribution / packaging
|
|
19
|
+
.Python
|
|
20
|
+
build/
|
|
21
|
+
develop-eggs/
|
|
22
|
+
dist/
|
|
23
|
+
downloads/
|
|
24
|
+
eggs/
|
|
25
|
+
.eggs/
|
|
26
|
+
lib/
|
|
27
|
+
lib64/
|
|
28
|
+
parts/
|
|
29
|
+
sdist/
|
|
30
|
+
var/
|
|
31
|
+
wheels/
|
|
32
|
+
share/python-wheels/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
.installed.cfg
|
|
35
|
+
*.egg
|
|
36
|
+
MANIFEST
|
|
37
|
+
|
|
38
|
+
# PyInstaller
|
|
39
|
+
# Usually these files are written by a python script from a template
|
|
40
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
41
|
+
*.manifest
|
|
42
|
+
*.spec
|
|
43
|
+
|
|
44
|
+
# Installer logs
|
|
45
|
+
pip-log.txt
|
|
46
|
+
pip-delete-this-directory.txt
|
|
47
|
+
|
|
48
|
+
# Unit test / coverage reports
|
|
49
|
+
htmlcov/
|
|
50
|
+
.tox/
|
|
51
|
+
.nox/
|
|
52
|
+
.coverage
|
|
53
|
+
.coverage.*
|
|
54
|
+
.cache
|
|
55
|
+
nosetests.xml
|
|
56
|
+
coverage.xml
|
|
57
|
+
*.cover
|
|
58
|
+
*.py.cover
|
|
59
|
+
.hypothesis/
|
|
60
|
+
.pytest_cache/
|
|
61
|
+
cover/
|
|
62
|
+
|
|
63
|
+
# Translations
|
|
64
|
+
*.mo
|
|
65
|
+
*.pot
|
|
66
|
+
|
|
67
|
+
# Django stuff:
|
|
68
|
+
*.log
|
|
69
|
+
local_settings.py
|
|
70
|
+
db.sqlite3
|
|
71
|
+
db.sqlite3-journal
|
|
72
|
+
|
|
73
|
+
# Flask stuff:
|
|
74
|
+
instance/
|
|
75
|
+
.webassets-cache
|
|
76
|
+
|
|
77
|
+
# Scrapy stuff:
|
|
78
|
+
.scrapy
|
|
79
|
+
|
|
80
|
+
# Sphinx documentation
|
|
81
|
+
docs/_build/
|
|
82
|
+
|
|
83
|
+
# PyBuilder
|
|
84
|
+
.pybuilder/
|
|
85
|
+
target/
|
|
86
|
+
|
|
87
|
+
# Jupyter Notebook
|
|
88
|
+
.ipynb_checkpoints
|
|
89
|
+
|
|
90
|
+
# IPython
|
|
91
|
+
profile_default/
|
|
92
|
+
ipython_config.py
|
|
93
|
+
|
|
94
|
+
# pyenv
|
|
95
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
96
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
97
|
+
# .python-version
|
|
98
|
+
|
|
99
|
+
# pipenv
|
|
100
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
101
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
102
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
103
|
+
# install all needed dependencies.
|
|
104
|
+
# Pipfile.lock
|
|
105
|
+
|
|
106
|
+
# UV
|
|
107
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
108
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
109
|
+
# commonly ignored for libraries.
|
|
110
|
+
# uv.lock
|
|
111
|
+
|
|
112
|
+
# poetry
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
114
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
115
|
+
# commonly ignored for libraries.
|
|
116
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
117
|
+
# poetry.lock
|
|
118
|
+
# poetry.toml
|
|
119
|
+
|
|
120
|
+
# pdm
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
122
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
123
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
124
|
+
# pdm.lock
|
|
125
|
+
# pdm.toml
|
|
126
|
+
.pdm-python
|
|
127
|
+
.pdm-build/
|
|
128
|
+
|
|
129
|
+
# pixi
|
|
130
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
131
|
+
# pixi.lock
|
|
132
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
133
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
134
|
+
.pixi
|
|
135
|
+
|
|
136
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
137
|
+
__pypackages__/
|
|
138
|
+
|
|
139
|
+
# Celery stuff
|
|
140
|
+
celerybeat-schedule
|
|
141
|
+
celerybeat.pid
|
|
142
|
+
|
|
143
|
+
# Redis
|
|
144
|
+
*.rdb
|
|
145
|
+
*.aof
|
|
146
|
+
*.pid
|
|
147
|
+
|
|
148
|
+
# RabbitMQ
|
|
149
|
+
mnesia/
|
|
150
|
+
rabbitmq/
|
|
151
|
+
rabbitmq-data/
|
|
152
|
+
|
|
153
|
+
# ActiveMQ
|
|
154
|
+
activemq-data/
|
|
155
|
+
|
|
156
|
+
# SageMath parsed files
|
|
157
|
+
*.sage.py
|
|
158
|
+
|
|
159
|
+
# Environments
|
|
160
|
+
.env
|
|
161
|
+
.envrc
|
|
162
|
+
.venv
|
|
163
|
+
env/
|
|
164
|
+
venv/
|
|
165
|
+
ENV/
|
|
166
|
+
env.bak/
|
|
167
|
+
venv.bak/
|
|
168
|
+
|
|
169
|
+
# Spyder project settings
|
|
170
|
+
.spyderproject
|
|
171
|
+
.spyproject
|
|
172
|
+
|
|
173
|
+
# Rope project settings
|
|
174
|
+
.ropeproject
|
|
175
|
+
|
|
176
|
+
# mkdocs documentation
|
|
177
|
+
/site
|
|
178
|
+
|
|
179
|
+
# mypy
|
|
180
|
+
.mypy_cache/
|
|
181
|
+
.dmypy.json
|
|
182
|
+
dmypy.json
|
|
183
|
+
|
|
184
|
+
# Pyre type checker
|
|
185
|
+
.pyre/
|
|
186
|
+
|
|
187
|
+
# pytype static type analyzer
|
|
188
|
+
.pytype/
|
|
189
|
+
|
|
190
|
+
# Cython debug symbols
|
|
191
|
+
cython_debug/
|
|
192
|
+
|
|
193
|
+
# PyCharm
|
|
194
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
195
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
196
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
197
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
198
|
+
# .idea/
|
|
199
|
+
|
|
200
|
+
# Abstra
|
|
201
|
+
# Abstra is an AI-powered process automation framework.
|
|
202
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
203
|
+
# Learn more at https://abstra.io/docs
|
|
204
|
+
.abstra/
|
|
205
|
+
|
|
206
|
+
# Visual Studio Code
|
|
207
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
208
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
209
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
210
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
211
|
+
# .vscode/
|
|
212
|
+
# Temporary file for partial code execution
|
|
213
|
+
tempCodeRunnerFile.py
|
|
214
|
+
|
|
215
|
+
# Ruff stuff:
|
|
216
|
+
.ruff_cache/
|
|
217
|
+
|
|
218
|
+
# PyPI configuration file
|
|
219
|
+
.pypirc
|
|
220
|
+
|
|
221
|
+
# Marimo
|
|
222
|
+
marimo/_static/
|
|
223
|
+
marimo/_lsp/
|
|
224
|
+
__marimo__/
|
|
225
|
+
|
|
226
|
+
# Streamlit
|
|
227
|
+
.streamlit/secrets.toml
|
|
228
|
+
|
|
229
|
+
# SQLite database files (may appear anywhere in the project)
|
|
230
|
+
*.db
|
|
231
|
+
*.db-shm
|
|
232
|
+
*.db-wal
|
credstore-0.3.7/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: credstore
|
|
3
|
+
Version: 0.3.7
|
|
4
|
+
Summary: Cross-platform credential storage — OS keyring with AES-encrypted file backup
|
|
5
|
+
Project-URL: Homepage, https://github.com/juzcn/slife
|
|
6
|
+
Project-URL: Repository, https://github.com/juzcn/slife
|
|
7
|
+
Project-URL: Documentation, https://github.com/juzcn/slife/blob/main/credstore/README.md
|
|
8
|
+
Author-email: juzcn <zhangjun@cueb.edu.cn>
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: keyring>=25.0.0
|
|
23
|
+
Requires-Dist: keyrings-cryptfile>=1.3.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# credstore
|
|
27
|
+
|
|
28
|
+
Cross-platform credential storage — OS keyring + AES-encrypted file backup.
|
|
29
|
+
|
|
30
|
+
A standalone secret manager that ships with [Slife](https://github.com/juzcn/slife)
|
|
31
|
+
but does **not** depend on it. Only two lightweight dependencies: `keyring` and
|
|
32
|
+
`keyrings-cryptfile`.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
### One-click (recommended)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install credstore
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or with uv:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install credstore
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Verify
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
credstore status
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
No configuration needed — the OS keyring is used automatically.
|
|
55
|
+
Run `credstore set-password` to enable encrypted backup.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## CLI
|
|
60
|
+
|
|
61
|
+
### Setup (first time)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
credstore set-password
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Creates `~/.credstore/credentials.crypt` and sets a master password (dev: `./credentials.crypt`). Path configurable via `CREDSTORE_FILE` env var.
|
|
68
|
+
|
|
69
|
+
### Command Reference
|
|
70
|
+
|
|
71
|
+
| Command | Master key | Description |
|
|
72
|
+
|---------|:----------:|-------------|
|
|
73
|
+
| `set-password` | sets it | Create/change master key, init cryptfile |
|
|
74
|
+
| `set KEY` | enters it | Atomic dual-write (cryptfile + keyring). Rolls back on keyring failure |
|
|
75
|
+
| `get KEY` | no | Keyring only, masked output (`sk-5f…b722`) |
|
|
76
|
+
| `get KEY -p` | enters it | Dual-query keyring + cryptfile, plaintext. Fails on mismatch |
|
|
77
|
+
| `delete KEY` | enters it | Remove from keyring + cryptfile |
|
|
78
|
+
| `list` | enters it | List all stored credential keys |
|
|
79
|
+
| `inject KEY...` | no | Persist to system env: registry (Windows) or shell profile (Unix) |
|
|
80
|
+
| `uninject KEY...` | no | Remove from system env: registry (Windows) or shell profile (Unix) |
|
|
81
|
+
| `reset-keyring` | enters it | Restore keyring from cryptfile backup |
|
|
82
|
+
| `reset-backup` | enters it | Sync system keyring → cryptfile |
|
|
83
|
+
| `status` | no | Show backend status |
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
credstore set-password # first-time setup
|
|
87
|
+
credstore set DEEPSEEK_API_KEY # store (masked, atomic dual-write)
|
|
88
|
+
credstore get DEEPSEEK_API_KEY # retrieve, masked output
|
|
89
|
+
credstore get DEEPSEEK_API_KEY -p # retrieve plaintext (dual-query)
|
|
90
|
+
credstore list # list all stored keys
|
|
91
|
+
credstore delete OLD_KEY # delete from keyring + cryptfile
|
|
92
|
+
|
|
93
|
+
# System environment injection (persistent)
|
|
94
|
+
credstore inject DEEPSEEK_API_KEY # Windows → registry, Unix → shell profile
|
|
95
|
+
Invoke-Expression (credstore inject KEY) # PowerShell: activate in current shell
|
|
96
|
+
eval "$(credstore inject KEY)" # Bash/Zsh: activate in current shell
|
|
97
|
+
credstore uninject DEEPSEEK_API_KEY # remove from registry/profile
|
|
98
|
+
|
|
99
|
+
credstore reset-keyring # restore keyring from cryptfile backup
|
|
100
|
+
credstore reset-backup # sync keyring → cryptfile
|
|
101
|
+
credstore status # show backend status
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Environment Variable Injection
|
|
105
|
+
|
|
106
|
+
`inject` reads a secret from the keyring and persists it to the system
|
|
107
|
+
environment. On Windows, it writes directly to the registry (`HKCU\Environment`);
|
|
108
|
+
on Unix, it appends to the shell profile. New shells load it automatically.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Persist + activate in current shell
|
|
112
|
+
credstore inject DEEPSEEK_API_KEY # persist to registry/profile
|
|
113
|
+
Invoke-Expression (credstore inject KEY) # PowerShell: activate now
|
|
114
|
+
eval "$(credstore inject KEY)" # Bash/Zsh: activate now
|
|
115
|
+
|
|
116
|
+
# Remove
|
|
117
|
+
credstore uninject DEEPSEEK_API_KEY # remove from registry/profile
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Secret never appears on the terminal — `inject` detects TTY and prints only
|
|
121
|
+
a hint. The actual export command flows through a pipe when wrapped in
|
|
122
|
+
`Invoke-Expression` / `eval`.
|
|
123
|
+
|
|
124
|
+
No master password required (keyring only).
|
|
125
|
+
|
|
126
|
+
### Disaster Recovery
|
|
127
|
+
|
|
128
|
+
When the OS keyring loses data (e.g. after a Windows password change):
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
credstore reset-keyring # restore all from cryptfile backup
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Design: Memory Safety
|
|
135
|
+
|
|
136
|
+
Secrets are immutable Python `str` objects — they cannot be zeroed in place.
|
|
137
|
+
credstore mitigates memory leaks through three design rules:
|
|
138
|
+
|
|
139
|
+
| Rule | Mechanism |
|
|
140
|
+
|------|-----------|
|
|
141
|
+
| **Never batch-load** | `list` collects only key names. Sync comparison fetches one value at a time and immediately `del`-s it |
|
|
142
|
+
| **Prefer existence checks** | `exists_credential()` / `list_credential_keys()` never retrieve secret content |
|
|
143
|
+
| **Explicit cleanup** | Every CLI handler `del`-s secret references after last use. `set`, `get`, `reset`, `set-password` all clean up on every exit path — including error branches |
|
|
144
|
+
|
|
145
|
+
### Operations that read/transport secrets
|
|
146
|
+
|
|
147
|
+
| Operation | How memory is cleaned |
|
|
148
|
+
|-----------|----------------------|
|
|
149
|
+
| `get` | Caller is responsible for `del`-ing the returned value |
|
|
150
|
+
| `set` | `del secret` + `del master_pw` after dual-write (all code paths) |
|
|
151
|
+
| `list` | Values fetched one-at-a-time, compared, `del`-ed immediately |
|
|
152
|
+
| `inject` | Value read → persisted → `del`-ed. TTY: no secret on stdout. Pipe: secret through pipe only |
|
|
153
|
+
| `uninject` | No secrets involved — registry/profile cleanup only |
|
|
154
|
+
| `reset-keyring` | Each value `del`-ed after writing to keyring |
|
|
155
|
+
| `reset-backup` | Batch load unavoidable, `del entries` + `del master_pw` after sync |
|
|
156
|
+
| `set-password` | `old_data` dict `del`-ed after re-encryption; all password strings cleaned up |
|
|
157
|
+
|
|
158
|
+
The `masked_input()` terminal helper echoes `*` for each keystroke — paste works but the
|
|
159
|
+
actual value is never displayed or logged.
|
|
160
|
+
|
|
161
|
+
## Python API
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import credstore
|
|
165
|
+
|
|
166
|
+
# Read / check / write / delete (system keyring only, no prompt)
|
|
167
|
+
credstore.get_credential("myapp/api_key") # → str | None
|
|
168
|
+
credstore.exists_credential("myapp/api_key") # → bool (NEVER returns secret)
|
|
169
|
+
credstore.list_credential_keys() # → list[str] (NEVER returns values)
|
|
170
|
+
credstore.set_credential("myapp/api_key", "sk-…")
|
|
171
|
+
credstore.delete_credential("myapp/api_key") # → bool
|
|
172
|
+
|
|
173
|
+
# Shell formatting (for env var injection)
|
|
174
|
+
credstore.format_export("MY_KEY", "secret", "bash") # → "export MY_KEY='secret'"
|
|
175
|
+
credstore.format_unset("MY_KEY", "bash") # → "unset MY_KEY"
|
|
176
|
+
|
|
177
|
+
# keyring: URI resolution
|
|
178
|
+
credstore.is_keyring_uri("keyring:myapp/k") # → True
|
|
179
|
+
credstore.resolve_uri("keyring:myapp/k") # → the secret value
|
|
180
|
+
|
|
181
|
+
# Backend info
|
|
182
|
+
credstore.check_backend() # → {"available": True, "backend": "…", …}
|
|
183
|
+
credstore.is_cryptfile_ready() # → True if master key is set
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Memory rule: callers of `get_credential()` and `resolve_uri()` MUST `del` the
|
|
187
|
+
returned value after use. Prefer `exists_credential()` or `list_credential_keys()`
|
|
188
|
+
when you only need to know whether a credential exists.
|
|
189
|
+
|
|
190
|
+
The Python API talks to the system keyring only — no master password, no prompt. Dual-write (keyring + cryptfile) is handled by the CLI.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# credstore
|
|
2
|
+
|
|
3
|
+
Cross-platform credential storage — OS keyring + AES-encrypted file backup.
|
|
4
|
+
|
|
5
|
+
A standalone secret manager that ships with [Slife](https://github.com/juzcn/slife)
|
|
6
|
+
but does **not** depend on it. Only two lightweight dependencies: `keyring` and
|
|
7
|
+
`keyrings-cryptfile`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
### One-click (recommended)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install credstore
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or with uv:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv tool install credstore
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Verify
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
credstore status
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No configuration needed — the OS keyring is used automatically.
|
|
30
|
+
Run `credstore set-password` to enable encrypted backup.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## CLI
|
|
35
|
+
|
|
36
|
+
### Setup (first time)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
credstore set-password
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Creates `~/.credstore/credentials.crypt` and sets a master password (dev: `./credentials.crypt`). Path configurable via `CREDSTORE_FILE` env var.
|
|
43
|
+
|
|
44
|
+
### Command Reference
|
|
45
|
+
|
|
46
|
+
| Command | Master key | Description |
|
|
47
|
+
|---------|:----------:|-------------|
|
|
48
|
+
| `set-password` | sets it | Create/change master key, init cryptfile |
|
|
49
|
+
| `set KEY` | enters it | Atomic dual-write (cryptfile + keyring). Rolls back on keyring failure |
|
|
50
|
+
| `get KEY` | no | Keyring only, masked output (`sk-5f…b722`) |
|
|
51
|
+
| `get KEY -p` | enters it | Dual-query keyring + cryptfile, plaintext. Fails on mismatch |
|
|
52
|
+
| `delete KEY` | enters it | Remove from keyring + cryptfile |
|
|
53
|
+
| `list` | enters it | List all stored credential keys |
|
|
54
|
+
| `inject KEY...` | no | Persist to system env: registry (Windows) or shell profile (Unix) |
|
|
55
|
+
| `uninject KEY...` | no | Remove from system env: registry (Windows) or shell profile (Unix) |
|
|
56
|
+
| `reset-keyring` | enters it | Restore keyring from cryptfile backup |
|
|
57
|
+
| `reset-backup` | enters it | Sync system keyring → cryptfile |
|
|
58
|
+
| `status` | no | Show backend status |
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
credstore set-password # first-time setup
|
|
62
|
+
credstore set DEEPSEEK_API_KEY # store (masked, atomic dual-write)
|
|
63
|
+
credstore get DEEPSEEK_API_KEY # retrieve, masked output
|
|
64
|
+
credstore get DEEPSEEK_API_KEY -p # retrieve plaintext (dual-query)
|
|
65
|
+
credstore list # list all stored keys
|
|
66
|
+
credstore delete OLD_KEY # delete from keyring + cryptfile
|
|
67
|
+
|
|
68
|
+
# System environment injection (persistent)
|
|
69
|
+
credstore inject DEEPSEEK_API_KEY # Windows → registry, Unix → shell profile
|
|
70
|
+
Invoke-Expression (credstore inject KEY) # PowerShell: activate in current shell
|
|
71
|
+
eval "$(credstore inject KEY)" # Bash/Zsh: activate in current shell
|
|
72
|
+
credstore uninject DEEPSEEK_API_KEY # remove from registry/profile
|
|
73
|
+
|
|
74
|
+
credstore reset-keyring # restore keyring from cryptfile backup
|
|
75
|
+
credstore reset-backup # sync keyring → cryptfile
|
|
76
|
+
credstore status # show backend status
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Environment Variable Injection
|
|
80
|
+
|
|
81
|
+
`inject` reads a secret from the keyring and persists it to the system
|
|
82
|
+
environment. On Windows, it writes directly to the registry (`HKCU\Environment`);
|
|
83
|
+
on Unix, it appends to the shell profile. New shells load it automatically.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Persist + activate in current shell
|
|
87
|
+
credstore inject DEEPSEEK_API_KEY # persist to registry/profile
|
|
88
|
+
Invoke-Expression (credstore inject KEY) # PowerShell: activate now
|
|
89
|
+
eval "$(credstore inject KEY)" # Bash/Zsh: activate now
|
|
90
|
+
|
|
91
|
+
# Remove
|
|
92
|
+
credstore uninject DEEPSEEK_API_KEY # remove from registry/profile
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Secret never appears on the terminal — `inject` detects TTY and prints only
|
|
96
|
+
a hint. The actual export command flows through a pipe when wrapped in
|
|
97
|
+
`Invoke-Expression` / `eval`.
|
|
98
|
+
|
|
99
|
+
No master password required (keyring only).
|
|
100
|
+
|
|
101
|
+
### Disaster Recovery
|
|
102
|
+
|
|
103
|
+
When the OS keyring loses data (e.g. after a Windows password change):
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
credstore reset-keyring # restore all from cryptfile backup
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Design: Memory Safety
|
|
110
|
+
|
|
111
|
+
Secrets are immutable Python `str` objects — they cannot be zeroed in place.
|
|
112
|
+
credstore mitigates memory leaks through three design rules:
|
|
113
|
+
|
|
114
|
+
| Rule | Mechanism |
|
|
115
|
+
|------|-----------|
|
|
116
|
+
| **Never batch-load** | `list` collects only key names. Sync comparison fetches one value at a time and immediately `del`-s it |
|
|
117
|
+
| **Prefer existence checks** | `exists_credential()` / `list_credential_keys()` never retrieve secret content |
|
|
118
|
+
| **Explicit cleanup** | Every CLI handler `del`-s secret references after last use. `set`, `get`, `reset`, `set-password` all clean up on every exit path — including error branches |
|
|
119
|
+
|
|
120
|
+
### Operations that read/transport secrets
|
|
121
|
+
|
|
122
|
+
| Operation | How memory is cleaned |
|
|
123
|
+
|-----------|----------------------|
|
|
124
|
+
| `get` | Caller is responsible for `del`-ing the returned value |
|
|
125
|
+
| `set` | `del secret` + `del master_pw` after dual-write (all code paths) |
|
|
126
|
+
| `list` | Values fetched one-at-a-time, compared, `del`-ed immediately |
|
|
127
|
+
| `inject` | Value read → persisted → `del`-ed. TTY: no secret on stdout. Pipe: secret through pipe only |
|
|
128
|
+
| `uninject` | No secrets involved — registry/profile cleanup only |
|
|
129
|
+
| `reset-keyring` | Each value `del`-ed after writing to keyring |
|
|
130
|
+
| `reset-backup` | Batch load unavoidable, `del entries` + `del master_pw` after sync |
|
|
131
|
+
| `set-password` | `old_data` dict `del`-ed after re-encryption; all password strings cleaned up |
|
|
132
|
+
|
|
133
|
+
The `masked_input()` terminal helper echoes `*` for each keystroke — paste works but the
|
|
134
|
+
actual value is never displayed or logged.
|
|
135
|
+
|
|
136
|
+
## Python API
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import credstore
|
|
140
|
+
|
|
141
|
+
# Read / check / write / delete (system keyring only, no prompt)
|
|
142
|
+
credstore.get_credential("myapp/api_key") # → str | None
|
|
143
|
+
credstore.exists_credential("myapp/api_key") # → bool (NEVER returns secret)
|
|
144
|
+
credstore.list_credential_keys() # → list[str] (NEVER returns values)
|
|
145
|
+
credstore.set_credential("myapp/api_key", "sk-…")
|
|
146
|
+
credstore.delete_credential("myapp/api_key") # → bool
|
|
147
|
+
|
|
148
|
+
# Shell formatting (for env var injection)
|
|
149
|
+
credstore.format_export("MY_KEY", "secret", "bash") # → "export MY_KEY='secret'"
|
|
150
|
+
credstore.format_unset("MY_KEY", "bash") # → "unset MY_KEY"
|
|
151
|
+
|
|
152
|
+
# keyring: URI resolution
|
|
153
|
+
credstore.is_keyring_uri("keyring:myapp/k") # → True
|
|
154
|
+
credstore.resolve_uri("keyring:myapp/k") # → the secret value
|
|
155
|
+
|
|
156
|
+
# Backend info
|
|
157
|
+
credstore.check_backend() # → {"available": True, "backend": "…", …}
|
|
158
|
+
credstore.is_cryptfile_ready() # → True if master key is set
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Memory rule: callers of `get_credential()` and `resolve_uri()` MUST `del` the
|
|
162
|
+
returned value after use. Prefer `exists_credential()` or `list_credential_keys()`
|
|
163
|
+
when you only need to know whether a credential exists.
|
|
164
|
+
|
|
165
|
+
The Python API talks to the system keyring only — no master password, no prompt. Dual-write (keyring + cryptfile) is handled by the CLI.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
// credstore configuration.
|
|
3
|
+
// Lookup order: ./credstore.json5 → ~/.credstore/config.json5
|
|
4
|
+
// Env var CREDSTORE_FILE overrides both.
|
|
5
|
+
//
|
|
6
|
+
// If no config is found, defaults to ~/.credstore/credentials.crypt
|
|
7
|
+
|
|
8
|
+
// Custom path for the encrypted credential file:
|
|
9
|
+
// cryptfile_path: "~/.credstore/credentials.crypt",
|
|
10
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""credstore — secure credential storage via OS keyring.
|
|
2
|
+
|
|
3
|
+
Cross-platform API for retrieving secrets stored in the OS keyring
|
|
4
|
+
with keyrings.cryptfile encrypted backup. Secrets are *stored* only
|
|
5
|
+
via the CLI (``credstore set``) which reads from masked stdin — never
|
|
6
|
+
through Python function arguments.
|
|
7
|
+
|
|
8
|
+
Modules::
|
|
9
|
+
|
|
10
|
+
_store.py CredentialStore + module-level API
|
|
11
|
+
_shell.py Shell formatting helpers
|
|
12
|
+
_backend.py Dual-write backends (system keyring + cryptfile)
|
|
13
|
+
_enumerate.py Platform-specific credential enumeration
|
|
14
|
+
_config.py Config file loading
|
|
15
|
+
_resolver.py keyring: URI resolution
|
|
16
|
+
_tty.py Masked terminal input
|
|
17
|
+
__main__.py CLI (entry point)
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
import credstore
|
|
22
|
+
|
|
23
|
+
# Retrieve a secret
|
|
24
|
+
value = credstore.get_credential("myapp/api_key")
|
|
25
|
+
|
|
26
|
+
# Resolve keyring: URIs in config values
|
|
27
|
+
resolved = credstore.resolve_uri("keyring:myapp/api_key")
|
|
28
|
+
|
|
29
|
+
# Delete
|
|
30
|
+
credstore.delete_credential("myapp/api_key")
|
|
31
|
+
|
|
32
|
+
# Shell formatting
|
|
33
|
+
credstore.format_export("KEY", "value", "bash") # → export statement
|
|
34
|
+
|
|
35
|
+
# Check backend
|
|
36
|
+
info = credstore.check_backend()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from credstore._shell import format_export, format_unset
|
|
40
|
+
from credstore._store import (
|
|
41
|
+
get_credential,
|
|
42
|
+
exists_credential,
|
|
43
|
+
list_credential_keys,
|
|
44
|
+
set_credential,
|
|
45
|
+
delete_credential,
|
|
46
|
+
get_backend_name,
|
|
47
|
+
check_backend,
|
|
48
|
+
init_store,
|
|
49
|
+
)
|
|
50
|
+
from credstore._resolver import (
|
|
51
|
+
resolve_uri,
|
|
52
|
+
is_keyring_uri,
|
|
53
|
+
parse_keyring_uri,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Read / write / delete
|
|
58
|
+
"get_credential",
|
|
59
|
+
"exists_credential",
|
|
60
|
+
"list_credential_keys",
|
|
61
|
+
"set_credential",
|
|
62
|
+
"delete_credential",
|
|
63
|
+
# URI resolution
|
|
64
|
+
"is_keyring_uri",
|
|
65
|
+
"parse_keyring_uri",
|
|
66
|
+
"resolve_uri",
|
|
67
|
+
# Shell formatting
|
|
68
|
+
"format_export",
|
|
69
|
+
"format_unset",
|
|
70
|
+
# Diagnostics
|
|
71
|
+
"get_backend_name",
|
|
72
|
+
"check_backend",
|
|
73
|
+
"init_store",
|
|
74
|
+
]
|
|
75
|
+
__version__ = "0.3.6"
|