secure-keystore 3.0.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,88 @@
1
+ # ── Secrets and runtime data ─────────────────────────────────────────────────
2
+ #
3
+ # The one category in this repository that matters more than build output. A
4
+ # keystore directory holds encrypted values, but a plaintext keys file holds the
5
+ # real thing - and the whole point of the library is that neither ends up in a
6
+ # commit.
7
+ #
8
+ # Not anchored to the root: a keystore written while running an example under
9
+ # javascript/ or python/ must be ignored there too.
10
+ .keystore/
11
+ .keystore-*/
12
+ *.skb
13
+ *-backup-*.tar
14
+ backups/
15
+
16
+ # Plaintext input files, anchored to the root and to each implementation, so the
17
+ # deliberately committed samples under */examples/ stay tracked.
18
+ /keys.txt
19
+ /keys.env
20
+ /keys.json
21
+ /*/keys.txt
22
+ /*/keys.env
23
+ /*/keys.json
24
+
25
+ # ── JavaScript ───────────────────────────────────────────────────────────────
26
+ node_modules/
27
+ */node_modules/
28
+ javascript/dist/
29
+ npm-debug.log*
30
+ # npm auth. The publish token is read from the NPM_TOKEN environment variable.
31
+ .npmrc
32
+
33
+ # ── .NET ─────────────────────────────────────────────────────────────────────
34
+ # Scoped to dotnet/, because javascript/bin holds the CLI entry point and is
35
+ # meant to be committed.
36
+ dotnet/**/bin/
37
+ dotnet/**/obj/
38
+ dotnet/**/TestResults/
39
+ *.user
40
+
41
+ # ── Python ───────────────────────────────────────────────────────────────────
42
+ __pycache__/
43
+ *.py[cod]
44
+ python/build/
45
+ python/dist/
46
+ python/.venv/
47
+ python/*.egg-info/
48
+ # An editable install writes its metadata beside the package.
49
+ python/src/*.egg-info/
50
+ *.egg-link
51
+ .pytest_cache/
52
+ .ruff_cache/
53
+
54
+ # ── PHP ──────────────────────────────────────────────────────────────────────
55
+ # composer.lock is not committed: this is a library, and the lock file of a
56
+ # library binds nobody.
57
+ php/vendor/
58
+ php/composer.lock
59
+ php/.phpunit.cache/
60
+ .phpunit.result.cache
61
+
62
+ # ── Go ───────────────────────────────────────────────────────────────────────
63
+ go/secure-keystore
64
+ go/secure-keystore.exe
65
+ go/**/testdata/fuzz/
66
+ go/**/*.test
67
+ go/coverage.out
68
+
69
+ # ── Rust ─────────────────────────────────────────────────────────────────────
70
+ # Cargo.lock is not committed, for the same reason as composer.lock.
71
+ rust/target/
72
+ rust/Cargo.lock
73
+ rust/fuzz/target/
74
+ rust/fuzz/corpus/
75
+ rust/fuzz/artifacts/
76
+
77
+ # ── Test artifacts ───────────────────────────────────────────────────────────
78
+ # Tests write to the system temp directory, so this only catches the older
79
+ # suites and anything run by hand.
80
+ test-temp/
81
+ coverage/
82
+
83
+ # ── Editor and OS noise ──────────────────────────────────────────────────────
84
+ .DS_Store
85
+ Thumbs.db
86
+ .vscode/
87
+ .idea/
88
+ *.log
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Python implementation are recorded here. The
4
+ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ project uses [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ The **package version** and the **store format version** are separate numbers and
8
+ move at different speeds — see [SPEC.md §11](../SPEC.md#11-versioning). This is
9
+ package 3.0.0, writing store format 3.0.
10
+
11
+ ## [3.0.0] - 2026-10-13
12
+
13
+ First release of the Python implementation.
14
+
15
+ Written against [SPEC.md](../SPEC.md) rather than by reading another
16
+ implementation — which is the rule that let six of them agree — and tested against
17
+ the [shared conformance corpus](../conformance/), a set of fixtures the JavaScript
18
+ implementation produced.
19
+
20
+ **Not verified.** Written against the specification, but not yet run on a machine with a Python toolchain. See [ROADMAP.md §3](../ROADMAP.md#3-next--ci-across-all-six).
21
+
22
+ ### Added
23
+
24
+ - `Keystore`, the store: `secure`, `get`, `get_all`, `set`, `migrate`,
25
+ `delete_key` and `delete_store`, in this language's spelling. A user key
26
+ unlocks a per-store master key; the master key unlocks each value under its own
27
+ salt.
28
+ - **One password stretch per store open**, not one per value. The user key is
29
+ stretched with PBKDF2-HMAC-SHA256 at 600,000 rounds; value keys come from
30
+ HKDF-SHA256, because a master key is not a password and stretching it buys
31
+ nothing. See [SPEC.md §2.1](../SPEC.md#21-two-derivations-because-there-are-two-jobs).
32
+ - **Every value bound to its key name**, as GCM additional authenticated data and
33
+ as HKDF context, so a value file moved to another key's name fails to decrypt.
34
+ - Metadata without a user key: listing stores, listing keys, and store info. Key
35
+ names are not encrypted, which is what makes a store inspectable without a
36
+ password — and means a key name must not itself be a secret.
37
+ - Export and import as one portable `.skb` bundle, with every name in it reduced
38
+ to its base name before writing, because a bundle is an untrusted file that
39
+ arrived over a wire.
40
+ - Stateless value tokens: `encrypt_value` and `decrypt_value`, writing `skv2` and
41
+ reading both `skv2` and `skv1`. The iteration count is recorded in the token, so
42
+ a column encrypted cheaply and a single secret encrypted expensively both read
43
+ back.
44
+ - Reading format 2.0 stores, and `migrate` to carry one across to 3.0. A 2.0 store
45
+ keeps taking 2.0 writes until it is migrated, so a store is never half one
46
+ format and half the other.
47
+ - The three input formats — colon, dotenv and JSON — parseable from a string, with
48
+ no directory involved.
49
+ - Errors carrying the codes in [SPEC.md §10](../SPEC.md#10-errors), identical in
50
+ every implementation.
51
+ - A command line with the same verbs, flags and `--json` output as every other
52
+ implementation.
53
+ - **Silence by default.** The library prints nothing unless given a logger, which
54
+ is what lets it drop into an application that has opinions about its own
55
+ logging.
56
+
57
+ ### Fixed before release
58
+
59
+ - **The command line raised `UnicodeEncodeError` on any non-ASCII value.**
60
+ Python defaults stdout to the ANSI code page on Windows - cp1252 - so printing
61
+ a secret containing an emoji aborted the command rather than printing
62
+ anything. The library was correct throughout; it could store a secret it could
63
+ not show. stdout and stderr are now reconfigured to UTF-8. Found by
64
+ [`cross-test.mjs`](../conformance/cross-test.mjs).
65
+
66
+ ### Notes specific to Python
67
+
68
+ - `cryptography` for PBKDF2HMAC, HKDF and AESGCM — the one dependency.
69
+ - `AESGCM` accepts both the 12-byte nonce 3.0 uses and the 16-byte one 2.0 wrote, so reading a legacy store needs no special case.
70
+ - `AESGCM.encrypt` appends the tag to the ciphertext; the format stores them as separate hex fields, so the last 16 bytes are split off.
71
+ - Full type hints and a `py.typed` marker.
72
+ - The whole `master.json` document is kept, so unknown fields survive a rewrite.
73
+
74
+ [3.0.0]: https://github.com/eharain/secure-keystore/releases/tag/python%2Fv3.0.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tech Style Ltd
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,222 @@
1
+ Metadata-Version: 2.5
2
+ Name: secure-keystore
3
+ Version: 3.0.0
4
+ Summary: Encrypted secrets at rest for Python, with no service to run. Two-layer AES-256-GCM over PBKDF2-SHA256 and HKDF-SHA256, with a documented cross-language format.
5
+ Project-URL: Homepage, https://tech-style.co/
6
+ Project-URL: Repository, https://github.com/eharain/secure-keystore
7
+ Project-URL: Issues, https://github.com/eharain/secure-keystore/issues
8
+ Project-URL: Changelog, https://github.com/eharain/secure-keystore/blob/main/python/CHANGELOG.md
9
+ Author-email: Ejaz Hussain Arain <hello@tech-style.co>
10
+ Maintainer-email: Tech Style Ltd <hello@tech-style.co>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: aes-gcm,ci-cd,configuration,credentials,devops,dotenv,encryption,encryption-at-rest,hkdf,keystore,pbkdf2,secret-management,secrets,vault
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Security :: Cryptography
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: cryptography>=41.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # secure-keystore — Python
32
+
33
+ **Encrypted secrets at rest, with no service to run.** One dependency:
34
+ [`cryptography`](https://cryptography.io/).
35
+
36
+ [![PyPI](https://img.shields.io/badge/pypi-secure--keystore-3775A9.svg)](https://pypi.org/project/secure-keystore/)
37
+ [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
38
+ [![licence](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
39
+ [![format](https://img.shields.io/badge/format-3.0-6366f1.svg)](../SPEC.md)
40
+
41
+ ```bash
42
+ pip install secure-keystore
43
+ ```
44
+
45
+ ```python
46
+ import os
47
+ from secure_keystore import Keystore
48
+
49
+ store = Keystore()
50
+ store.secure("./keys.env", os.environ["USER_KEY"], "production")
51
+ api_key = store.get("api_key", os.environ["USER_KEY"], "production")
52
+ ```
53
+
54
+ This is one of [six implementations](../README.md#one-format-six-languages) of the
55
+ [secure-keystore format](../SPEC.md). A store written here opens in the
56
+ JavaScript, Go, .NET, PHP and Rust ones, and the other way round — which is not a
57
+ claim but [a test](tests/test_conformance.py) against a corpus the JavaScript
58
+ implementation produced, and against
59
+ [`cross-test.mjs`](../conformance/cross-test.mjs), where all six write stores and
60
+ all six read every one of them.
61
+
62
+ ---
63
+
64
+ ## Two ways in
65
+
66
+ **A store** — a directory of encrypted values that one user key opens.
67
+
68
+ **A token** — one string in, one string out, nothing on disk.
69
+
70
+ ```python
71
+ from secure_keystore import encrypt_value, decrypt_value
72
+
73
+ token = encrypt_value("sk_live_abc123", user_key)
74
+ back = decrypt_value(token, user_key)
75
+ ```
76
+
77
+ ---
78
+
79
+ ## API
80
+
81
+ ### `Keystore(keystore_dir=None, *, iterations=600_000, logger=None, create_dir=True)`
82
+
83
+ Binds one keystore directory and one set of options, validated once here rather
84
+ than on every call. The instance holds no secret: the user key is passed to each
85
+ call that needs one and is never retained.
86
+
87
+ | Argument | Default | |
88
+ |---|---|---|
89
+ | `keystore_dir` | `./.keystore` | Where stores live. |
90
+ | `iterations` | `600000` | PBKDF2 rounds for the *user key*, on new and migrated stores. Recorded in the store. Does not apply to value keys, which use HKDF. |
91
+ | `logger` | `None` | Called with one line of progress. **None means the package prints nothing** — the default, and what a server wants. |
92
+ | `create_dir` | `True` | Create the keystore directory if absent. |
93
+
94
+ ### Store operations
95
+
96
+ ```python
97
+ store.secure(input_file, user_key, store_name=None) -> SecureResult
98
+ store.get(key, user_key, store_name=None) -> str
99
+ store.get_all(user_key, store_name) -> dict[str, str]
100
+ store.set(key, value, user_key, store_name) -> SetResult
101
+ store.migrate(store_name, user_key) -> MigrateResult
102
+ store.delete_key(key, store_name) -> None
103
+ store.delete_store(store_name) -> None
104
+ ```
105
+
106
+ `get_all` costs **one** PBKDF2 derivation however many keys the store holds — see
107
+ [SPEC.md §2.1](../SPEC.md#21-two-derivations-because-there-are-two-jobs).
108
+
109
+ ### Metadata — no user key needed
110
+
111
+ ```python
112
+ store.list_stores() -> list[str]
113
+ store.list_keys(store_name) -> list[str]
114
+ store.get_store_info(store_name) -> StoreInfo
115
+ ```
116
+
117
+ Key names live in metadata in the clear, so **a key name must not itself be a
118
+ secret**.
119
+
120
+ ### Bundles and tokens
121
+
122
+ ```python
123
+ store.export_store(store_name, backup_path=None) -> str
124
+ store.import_store(backup_path, store_name=None) -> str
125
+
126
+ encrypt_value(plaintext, user_key, *, iterations=None) -> str | None
127
+ decrypt_value(token, user_key) -> str
128
+ is_encrypted_value(value) -> bool
129
+ token_version(value) -> str | None
130
+ ```
131
+
132
+ `iterations` lowers the cost for bulk work — a token pays one stretch **per
133
+ value**, unlike a store. The count is recorded in the token, so decryption
134
+ follows whatever was chosen.
135
+
136
+ ---
137
+
138
+ ## Errors
139
+
140
+ Every failure is a `KeystoreError` with a stable `.code`:
141
+
142
+ ```python
143
+ from secure_keystore import ErrorCodes, KeystoreError
144
+
145
+ try:
146
+ store.get("api_key", user_key, "production")
147
+ except KeystoreError as error:
148
+ if error.code == ErrorCodes.BAD_USER_KEY:
149
+ ... # wrong password, tampered bytes, or a mismatched context
150
+ if error.code == ErrorCodes.STORE_NOT_FOUND:
151
+ ... # not deployed yet
152
+ ```
153
+
154
+ The codes are identical in all six implementations —
155
+ [SPEC.md §10](../SPEC.md#10-errors). A wrong user key would otherwise surface as
156
+ `cryptography.exceptions.InvalidTag`, which names neither the store nor the
157
+ reason.
158
+
159
+ ---
160
+
161
+ ## Notes specific to Python
162
+
163
+ **`AESGCM` handles both nonce lengths.** `cryptography` accepts a 16-byte nonce as
164
+ well as the 12-byte one format 3.0 uses, so reading a format 2.0 store needs no
165
+ special case here — unlike .NET, where it needs a whole GCM implementation. Do not
166
+ let that make the 16-byte IV look fine; see
167
+ [ROADMAP.md §1.1](../ROADMAP.md#11-a-12-byte-iv).
168
+
169
+ **`AESGCM.encrypt` appends the tag** to the ciphertext; the format stores them as
170
+ separate hex fields, so the last 16 bytes are split off.
171
+
172
+ **Type hints throughout**, and a `py.typed` marker, so `mypy` sees the API.
173
+
174
+ **Ordering.** `entries` is a list and stays one; `dict` preserves insertion order
175
+ in the versions this supports, so a parsed input file keeps the document's key
176
+ order.
177
+
178
+ ---
179
+
180
+ ## The CLI
181
+
182
+ ```bash
183
+ secure-keystore get api_key "$USER_KEY" production
184
+ python -m secure_keystore --help # the same thing, without the entry point
185
+ ```
186
+
187
+ Same verbs, flags, error codes and `--json` output as every other
188
+ implementation — see the [root README](../README.md#the-cli).
189
+
190
+ ---
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ pip install -e ".[dev]"
196
+ pytest # includes the shared corpus
197
+ ```
198
+
199
+ ```
200
+ python/
201
+ ├── src/secure_keystore/
202
+ │ ├── __init__.py the public surface, and ALGORITHMS
203
+ │ ├── _format.py everything the format fixes
204
+ │ ├── errors.py KeystoreError and its codes
205
+ │ ├── _crypto.py PBKDF2, HKDF, AES-GCM — the only place a cipher is built
206
+ │ ├── token.py the stateless skv2 token
207
+ │ ├── input_file.py the three input formats, testable without a file
208
+ │ ├── bundle.py the .skb bundle
209
+ │ ├── keystore.py the store itself
210
+ │ └── cli.py
211
+ └── tests/
212
+ ```
213
+
214
+ ## Further reading
215
+
216
+ - **[../SPEC.md](../SPEC.md)** — the format, in enough detail to implement it
217
+ - **[../conformance/](../conformance/)** — how cross-language compatibility is tested
218
+ - **[CHANGELOG.md](CHANGELOG.md)**
219
+
220
+ ## Licence
221
+
222
+ MIT © [Tech Style Ltd](https://tech-style.co/)
@@ -0,0 +1,192 @@
1
+ # secure-keystore — Python
2
+
3
+ **Encrypted secrets at rest, with no service to run.** One dependency:
4
+ [`cryptography`](https://cryptography.io/).
5
+
6
+ [![PyPI](https://img.shields.io/badge/pypi-secure--keystore-3775A9.svg)](https://pypi.org/project/secure-keystore/)
7
+ [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
8
+ [![licence](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
9
+ [![format](https://img.shields.io/badge/format-3.0-6366f1.svg)](../SPEC.md)
10
+
11
+ ```bash
12
+ pip install secure-keystore
13
+ ```
14
+
15
+ ```python
16
+ import os
17
+ from secure_keystore import Keystore
18
+
19
+ store = Keystore()
20
+ store.secure("./keys.env", os.environ["USER_KEY"], "production")
21
+ api_key = store.get("api_key", os.environ["USER_KEY"], "production")
22
+ ```
23
+
24
+ This is one of [six implementations](../README.md#one-format-six-languages) of the
25
+ [secure-keystore format](../SPEC.md). A store written here opens in the
26
+ JavaScript, Go, .NET, PHP and Rust ones, and the other way round — which is not a
27
+ claim but [a test](tests/test_conformance.py) against a corpus the JavaScript
28
+ implementation produced, and against
29
+ [`cross-test.mjs`](../conformance/cross-test.mjs), where all six write stores and
30
+ all six read every one of them.
31
+
32
+ ---
33
+
34
+ ## Two ways in
35
+
36
+ **A store** — a directory of encrypted values that one user key opens.
37
+
38
+ **A token** — one string in, one string out, nothing on disk.
39
+
40
+ ```python
41
+ from secure_keystore import encrypt_value, decrypt_value
42
+
43
+ token = encrypt_value("sk_live_abc123", user_key)
44
+ back = decrypt_value(token, user_key)
45
+ ```
46
+
47
+ ---
48
+
49
+ ## API
50
+
51
+ ### `Keystore(keystore_dir=None, *, iterations=600_000, logger=None, create_dir=True)`
52
+
53
+ Binds one keystore directory and one set of options, validated once here rather
54
+ than on every call. The instance holds no secret: the user key is passed to each
55
+ call that needs one and is never retained.
56
+
57
+ | Argument | Default | |
58
+ |---|---|---|
59
+ | `keystore_dir` | `./.keystore` | Where stores live. |
60
+ | `iterations` | `600000` | PBKDF2 rounds for the *user key*, on new and migrated stores. Recorded in the store. Does not apply to value keys, which use HKDF. |
61
+ | `logger` | `None` | Called with one line of progress. **None means the package prints nothing** — the default, and what a server wants. |
62
+ | `create_dir` | `True` | Create the keystore directory if absent. |
63
+
64
+ ### Store operations
65
+
66
+ ```python
67
+ store.secure(input_file, user_key, store_name=None) -> SecureResult
68
+ store.get(key, user_key, store_name=None) -> str
69
+ store.get_all(user_key, store_name) -> dict[str, str]
70
+ store.set(key, value, user_key, store_name) -> SetResult
71
+ store.migrate(store_name, user_key) -> MigrateResult
72
+ store.delete_key(key, store_name) -> None
73
+ store.delete_store(store_name) -> None
74
+ ```
75
+
76
+ `get_all` costs **one** PBKDF2 derivation however many keys the store holds — see
77
+ [SPEC.md §2.1](../SPEC.md#21-two-derivations-because-there-are-two-jobs).
78
+
79
+ ### Metadata — no user key needed
80
+
81
+ ```python
82
+ store.list_stores() -> list[str]
83
+ store.list_keys(store_name) -> list[str]
84
+ store.get_store_info(store_name) -> StoreInfo
85
+ ```
86
+
87
+ Key names live in metadata in the clear, so **a key name must not itself be a
88
+ secret**.
89
+
90
+ ### Bundles and tokens
91
+
92
+ ```python
93
+ store.export_store(store_name, backup_path=None) -> str
94
+ store.import_store(backup_path, store_name=None) -> str
95
+
96
+ encrypt_value(plaintext, user_key, *, iterations=None) -> str | None
97
+ decrypt_value(token, user_key) -> str
98
+ is_encrypted_value(value) -> bool
99
+ token_version(value) -> str | None
100
+ ```
101
+
102
+ `iterations` lowers the cost for bulk work — a token pays one stretch **per
103
+ value**, unlike a store. The count is recorded in the token, so decryption
104
+ follows whatever was chosen.
105
+
106
+ ---
107
+
108
+ ## Errors
109
+
110
+ Every failure is a `KeystoreError` with a stable `.code`:
111
+
112
+ ```python
113
+ from secure_keystore import ErrorCodes, KeystoreError
114
+
115
+ try:
116
+ store.get("api_key", user_key, "production")
117
+ except KeystoreError as error:
118
+ if error.code == ErrorCodes.BAD_USER_KEY:
119
+ ... # wrong password, tampered bytes, or a mismatched context
120
+ if error.code == ErrorCodes.STORE_NOT_FOUND:
121
+ ... # not deployed yet
122
+ ```
123
+
124
+ The codes are identical in all six implementations —
125
+ [SPEC.md §10](../SPEC.md#10-errors). A wrong user key would otherwise surface as
126
+ `cryptography.exceptions.InvalidTag`, which names neither the store nor the
127
+ reason.
128
+
129
+ ---
130
+
131
+ ## Notes specific to Python
132
+
133
+ **`AESGCM` handles both nonce lengths.** `cryptography` accepts a 16-byte nonce as
134
+ well as the 12-byte one format 3.0 uses, so reading a format 2.0 store needs no
135
+ special case here — unlike .NET, where it needs a whole GCM implementation. Do not
136
+ let that make the 16-byte IV look fine; see
137
+ [ROADMAP.md §1.1](../ROADMAP.md#11-a-12-byte-iv).
138
+
139
+ **`AESGCM.encrypt` appends the tag** to the ciphertext; the format stores them as
140
+ separate hex fields, so the last 16 bytes are split off.
141
+
142
+ **Type hints throughout**, and a `py.typed` marker, so `mypy` sees the API.
143
+
144
+ **Ordering.** `entries` is a list and stays one; `dict` preserves insertion order
145
+ in the versions this supports, so a parsed input file keeps the document's key
146
+ order.
147
+
148
+ ---
149
+
150
+ ## The CLI
151
+
152
+ ```bash
153
+ secure-keystore get api_key "$USER_KEY" production
154
+ python -m secure_keystore --help # the same thing, without the entry point
155
+ ```
156
+
157
+ Same verbs, flags, error codes and `--json` output as every other
158
+ implementation — see the [root README](../README.md#the-cli).
159
+
160
+ ---
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ pip install -e ".[dev]"
166
+ pytest # includes the shared corpus
167
+ ```
168
+
169
+ ```
170
+ python/
171
+ ├── src/secure_keystore/
172
+ │ ├── __init__.py the public surface, and ALGORITHMS
173
+ │ ├── _format.py everything the format fixes
174
+ │ ├── errors.py KeystoreError and its codes
175
+ │ ├── _crypto.py PBKDF2, HKDF, AES-GCM — the only place a cipher is built
176
+ │ ├── token.py the stateless skv2 token
177
+ │ ├── input_file.py the three input formats, testable without a file
178
+ │ ├── bundle.py the .skb bundle
179
+ │ ├── keystore.py the store itself
180
+ │ └── cli.py
181
+ └── tests/
182
+ ```
183
+
184
+ ## Further reading
185
+
186
+ - **[../SPEC.md](../SPEC.md)** — the format, in enough detail to implement it
187
+ - **[../conformance/](../conformance/)** — how cross-language compatibility is tested
188
+ - **[CHANGELOG.md](CHANGELOG.md)**
189
+
190
+ ## Licence
191
+
192
+ MIT © [Tech Style Ltd](https://tech-style.co/)
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "secure-keystore"
7
+ version = "3.0.0"
8
+ description = "Encrypted secrets at rest for Python, with no service to run. Two-layer AES-256-GCM over PBKDF2-SHA256 and HKDF-SHA256, with a documented cross-language format."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Ejaz Hussain Arain", email = "hello@tech-style.co" }]
13
+ maintainers = [{ name = "Tech Style Ltd", email = "hello@tech-style.co" }]
14
+ keywords = [
15
+ "encryption", "secrets", "secret-management", "keystore", "credentials",
16
+ "vault", "aes-gcm", "pbkdf2", "hkdf", "dotenv", "configuration",
17
+ "encryption-at-rest", "devops", "ci-cd",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Security :: Cryptography",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = ["cryptography>=41.0"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://tech-style.co/"
36
+ Repository = "https://github.com/eharain/secure-keystore"
37
+ Issues = "https://github.com/eharain/secure-keystore/issues"
38
+ Changelog = "https://github.com/eharain/secure-keystore/blob/main/python/CHANGELOG.md"
39
+
40
+ [project.scripts]
41
+ secure-keystore = "secure_keystore.cli:main"
42
+
43
+ [project.optional-dependencies]
44
+ dev = ["pytest>=7.0"]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/secure_keystore"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py310"