dotseal 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,34 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: Python ${{ matrix.python-version }}
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ python -m pip install --upgrade pip
31
+ python -m pip install -e ".[test]"
32
+
33
+ - name: Run tests
34
+ run: python -m pytest
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Environment variables
13
+ .env
@@ -0,0 +1 @@
1
+ 3.13
dotseal-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dotseal 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.
dotseal-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,288 @@
1
+ Metadata-Version: 2.4
2
+ Name: dotseal
3
+ Version: 0.1.0
4
+ Summary: Git-friendly encrypted .env files with cleartext keys and sealed values (SOPS-inspired structural encryption).
5
+ Project-URL: Homepage, https://github.com/Jastchi/dotseal
6
+ Project-URL: Repository, https://github.com/Jastchi/dotseal
7
+ Author: dotseal contributors
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: aes-gcm,configuration,dotenv,dotseal,encryption,env,secrets,sops
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Security :: Cryptography
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Requires-Python: >=3.8
27
+ Requires-Dist: cryptography>=42.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest<9,>=8.0.0; (python_version < '3.10') and extra == 'dev'
30
+ Requires-Dist: pytest>=9.0.0; (python_version >= '3.10') and extra == 'dev'
31
+ Requires-Dist: ruff>=0.15; (python_version >= '3.9') and extra == 'dev'
32
+ Requires-Dist: ty>=0.0.47; (python_version >= '3.9') and extra == 'dev'
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest<9,>=8.0.0; (python_version < '3.10') and extra == 'test'
35
+ Requires-Dist: pytest>=9.0.0; (python_version >= '3.10') and extra == 'test'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # dotseal
39
+
40
+ Git-friendly encrypted `.env` files with cleartext keys and sealed values — an offline-first environment-variable manager for Python, inspired by [Mozilla SOPS](https://github.com/getsops/sops) but built natively for the Python ecosystem.
41
+
42
+ `dotseal` performs **structural encryption**: it leaves your `.env` **keys in cleartext** and encrypts only the **values**. The result is a `.env.enc` file you can safely commit, review in pull requests, and merge — because the diff still shows *which* variables changed, just not their secret contents.
43
+
44
+ ```diff
45
+ DATABASE_URL=ENC[AES_GCM,data:Zm9vYmFy...]
46
+ - DEBUG=ENC[AES_GCM,data:TXVzaWM=]
47
+ + DEBUG=ENC[AES_GCM,data:b3RoZXI=]
48
+ API_KEY=ENC[AES_GCM,data:c2VjcmV0...]
49
+ ```
50
+
51
+ - **No OS dependencies.** Pure Python on top of [`cryptography`](https://cryptography.io). No `age`, `gpg`, `sops`, `openssl` CLI, or Go binaries required.
52
+ - **Authenticated encryption.** AES-256-GCM (AEAD) with a fresh nonce per value.
53
+ - **Tamper-evident & swap-proof.** Each value is bound to its variable name as Additional Authenticated Data (AAD), so ciphertext can't be moved between keys.
54
+ - **Runtime loader.** Decrypt straight into `os.environ` — no cleartext file ever touches disk.
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pip install dotseal
62
+ ```
63
+
64
+ Requires Python 3.8+.
65
+
66
+ ---
67
+
68
+ ## Quickstart
69
+
70
+ ```bash
71
+ # 1. Generate a master key (saved to .dotseal.key and gitignored)
72
+ dotseal init
73
+
74
+ # 2. Write a normal .env file
75
+ cat > .env <<'EOF'
76
+ DATABASE_URL=postgres://user:pass@localhost:5432/db
77
+ DEBUG=True
78
+ API_KEY=super-secret
79
+ EOF
80
+
81
+ # 3. Encrypt it → .env.enc (commit this; never commit .env or the key)
82
+ dotseal encrypt
83
+
84
+ # 4. Decrypt when you need it back
85
+ dotseal decrypt
86
+ ```
87
+
88
+ ### What gets committed?
89
+
90
+ | File | Commit it? | Contents |
91
+ | --------------------- | ---------- | ----------------------------------------- |
92
+ | `.env.enc` | ✅ Yes | Keys in cleartext, values encrypted |
93
+ | `.env` | ❌ No | Full cleartext secrets |
94
+ | `.dotseal.key` | ❌ **Never**| The master key (auto-added to `.gitignore`) |
95
+
96
+ ---
97
+
98
+ ## CLI Reference
99
+
100
+ ### `dotseal init`
101
+ Generates a new cryptographically secure master key, writes it to `.dotseal.key` (mode `0600`), and adds it to `.gitignore` (creating one if needed). Prints the key **fingerprint** (not the key) so you can verify which key encrypted a file. Use `--force` to replace an existing key (this makes existing `.env.enc` files undecryptable).
102
+
103
+ ### `dotseal encrypt [input] [output]`
104
+ Encrypts the values of a cleartext env file. Defaults: `.env` → `.env.enc`. Idempotent — values that are already encrypted are left untouched.
105
+
106
+ ### `dotseal decrypt [input] [output]`
107
+ Decrypts values back to cleartext. Defaults: `.env.enc` → `.env`. The output is written with owner-only (`0600`) permissions since it contains secrets.
108
+
109
+ ### `dotseal edit [file]`
110
+ SOPS-style editing. Decrypts `.env.enc` to a temporary file (mode `0600`), opens it in `$EDITOR` (falling back to `nano`), and re-encrypts on save. The temp file is securely overwritten and deleted afterward. If the file doesn't exist yet, you get a fresh template to start from.
111
+
112
+ ### Common options
113
+ All commands except `init` accept:
114
+
115
+ - `-k, --key <base64>` — provide the master key directly (overrides env var and key file).
116
+ - `--key-file <path>` — use a specific key file instead of auto-discovery.
117
+
118
+ ---
119
+
120
+ ## Key Management
121
+
122
+ The master key is resolved in this order (first match wins):
123
+
124
+ 1. An explicit `--key` argument (CLI) or `master_key=` argument (loader).
125
+ 2. The `DOTSEAL_MASTER_KEY` environment variable.
126
+ 3. A local `.dotseal.key` file (searched for in the current directory and upward through parent directories).
127
+
128
+ The key is a base64-encoded 32-byte (AES-256) value. Generate one programmatically with:
129
+
130
+ ```python
131
+ from dotseal import generate_master_key
132
+ print(generate_master_key())
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Runtime Loader (no cleartext on disk)
138
+
139
+ `load_env` is a **drop-in replacement for `python-dotenv`'s `load_dotenv`** — it just reads an encrypted `.env.enc` instead of a cleartext `.env`. Call it once at startup and your secrets are available as ordinary environment variables through the `os` module:
140
+
141
+ ```python
142
+ import os
143
+ from dotseal import load_env
144
+
145
+ # Resolves the key from DOTSEAL_MASTER_KEY or .dotseal.key
146
+ load_env() # reads ".env.enc" by default
147
+
148
+ os.getenv("DATABASE_URL") # now available, like any env var
149
+ ```
150
+
151
+ Signature:
152
+
153
+ ```python
154
+ def load_env(
155
+ dotenv_path: str = ".env.enc",
156
+ *,
157
+ master_key: str | None = None,
158
+ override: bool = False,
159
+ encoding: str = "utf-8",
160
+ ) -> bool:
161
+ ...
162
+ ```
163
+
164
+ - `override=False` (default): existing process env vars win (12-factor friendly).
165
+ - `override=True`: decrypted values overwrite anything already in `os.environ`.
166
+ - Returns `True` if at least one variable was set (matching `load_dotenv`). Want the values as a `dict` instead? Use `decrypt_to_dict` (below).
167
+
168
+ Other programmatic helpers:
169
+
170
+ ```python
171
+ from dotseal import encrypt_text, decrypt_text, decrypt_to_dict, load_key_bytes
172
+
173
+ key = load_key_bytes("BASE64KEY==")
174
+ enc = encrypt_text("FOO=bar\n", key) # -> ".env.enc" text
175
+ cleartext = decrypt_text(enc, key) # -> ".env" text
176
+ mapping = decrypt_to_dict(enc, key) # -> {"FOO": "bar"}
177
+ ```
178
+
179
+ ---
180
+
181
+ ## File Format
182
+
183
+ ```env
184
+ # Generated by dotseal. DO NOT EDIT VALUES MANUALLY.
185
+ DATABASE_URL=ENC[AES_GCM,data:<base64(nonce ‖ ciphertext ‖ tag)>]
186
+ DEBUG=ENC[AES_GCM,data:...]
187
+ # dotseal: v=1 alg=AES_GCM key_fp=7ef08b59e6a945e4
188
+ ```
189
+
190
+ - Each value's payload is `base64(12-byte nonce ‖ ciphertext ‖ GCM tag)`.
191
+ - The variable name is bound as AAD, so values cannot be swapped between keys.
192
+ - The trailing `# dotseal:` metadata line records the algorithm and a **key fingerprint** (a one-way hash of the key). On decrypt, the fingerprint is checked first so a wrong key fails fast with a clear message instead of a cryptic crypto error.
193
+ - Comments and blank lines are preserved. Values containing spaces, `#`, or newlines are safely quoted/escaped on decryption.
194
+
195
+ ---
196
+
197
+ ## CI/CD Integration
198
+
199
+ The pattern is always the same: provide the master key via the `DOTSEAL_MASTER_KEY` environment variable (from your platform's secret store), commit only `.env.enc`, and either decrypt to a file or load at runtime.
200
+
201
+ ### GitHub Actions
202
+
203
+ Store the key as a repository/environment **secret** named `DOTSEAL_MASTER_KEY`.
204
+
205
+ ```yaml
206
+ jobs:
207
+ deploy:
208
+ runs-on: ubuntu-latest
209
+ env:
210
+ DOTSEAL_MASTER_KEY: ${{ secrets.DOTSEAL_MASTER_KEY }}
211
+ steps:
212
+ - uses: actions/checkout@v4
213
+ - uses: actions/setup-python@v5
214
+ with:
215
+ python-version: "3.12"
216
+ - run: pip install dotseal
217
+
218
+ # Option A: decrypt to a real .env for tools that expect a file
219
+ - run: dotseal decrypt .env.enc .env
220
+
221
+ # Option B: load at runtime inside your app (no cleartext file)
222
+ - run: python -c "from dotseal import load_env; load_env(); import app"
223
+ ```
224
+
225
+ ### Docker
226
+
227
+ Bake only the encrypted file into the image and pass the key at runtime:
228
+
229
+ ```dockerfile
230
+ FROM python:3.12-slim
231
+ WORKDIR /app
232
+ RUN pip install dotseal
233
+ COPY .env.enc .
234
+ COPY . .
235
+ # App calls load_env() on startup.
236
+ CMD ["python", "main.py"]
237
+ ```
238
+
239
+ ```bash
240
+ docker run -e DOTSEAL_MASTER_KEY="$(cat .dotseal.key)" my-image
241
+ ```
242
+
243
+ ```python
244
+ # main.py
245
+ from dotseal import load_env
246
+ load_env() # picks up DOTSEAL_MASTER_KEY from the container env
247
+ ```
248
+
249
+ ### Kubernetes
250
+
251
+ Store the master key in a `Secret` and expose it as `DOTSEAL_MASTER_KEY`:
252
+
253
+ ```yaml
254
+ env:
255
+ - name: DOTSEAL_MASTER_KEY
256
+ valueFrom:
257
+ secretKeyRef:
258
+ name: dotseal
259
+ key: master-key
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Security Notes & Limitations
265
+
266
+ - **AES-256-GCM** provides confidentiality *and* integrity. Tampered ciphertext or a wrong key is rejected rather than silently producing garbage.
267
+ - **AAD binding** prevents an attacker who can edit the committed `.env.enc` from relocating a high-privilege secret onto a low-privilege variable name.
268
+ - **Key fingerprint** is a domain-separated SHA-256 hash truncated to 8 bytes; it reveals nothing about the key itself.
269
+ - **Memory hygiene is best-effort.** dotseal overwrites the mutable key buffers it controls, but Python's immutable `str`/`bytes` and garbage collector mean secrets can still linger in memory. Do not rely on this for protection against an attacker with live process access.
270
+ - **The master key is the whole ballgame.** Anyone with the key can decrypt everything. Rotate it by re-encrypting with `dotseal init --force` followed by `encrypt`, and store it only in trusted secret managers.
271
+ - This tool is a single-key symmetric scheme. It does **not** implement multi-recipient/asymmetric key sharing (a SOPS + `age`/KMS feature).
272
+
273
+ ---
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ uv venv && uv pip install -e ".[dev]"
279
+ uv run pytest
280
+ ```
281
+
282
+ CI runs the full test suite on Python 3.8 through 3.14 (see `.github/workflows/test.yml`).
283
+
284
+ The test suite covers crypto round-trips, edge-case values (empty strings, `!!@#$%=`, unicode, multi-line, large), structural parsing, the runtime loader (asserting no side-effect files are written), and the full CLI lifecycle including `edit`.
285
+
286
+ ## License
287
+
288
+ MIT
@@ -0,0 +1,251 @@
1
+ # dotseal
2
+
3
+ Git-friendly encrypted `.env` files with cleartext keys and sealed values — an offline-first environment-variable manager for Python, inspired by [Mozilla SOPS](https://github.com/getsops/sops) but built natively for the Python ecosystem.
4
+
5
+ `dotseal` performs **structural encryption**: it leaves your `.env` **keys in cleartext** and encrypts only the **values**. The result is a `.env.enc` file you can safely commit, review in pull requests, and merge — because the diff still shows *which* variables changed, just not their secret contents.
6
+
7
+ ```diff
8
+ DATABASE_URL=ENC[AES_GCM,data:Zm9vYmFy...]
9
+ - DEBUG=ENC[AES_GCM,data:TXVzaWM=]
10
+ + DEBUG=ENC[AES_GCM,data:b3RoZXI=]
11
+ API_KEY=ENC[AES_GCM,data:c2VjcmV0...]
12
+ ```
13
+
14
+ - **No OS dependencies.** Pure Python on top of [`cryptography`](https://cryptography.io). No `age`, `gpg`, `sops`, `openssl` CLI, or Go binaries required.
15
+ - **Authenticated encryption.** AES-256-GCM (AEAD) with a fresh nonce per value.
16
+ - **Tamper-evident & swap-proof.** Each value is bound to its variable name as Additional Authenticated Data (AAD), so ciphertext can't be moved between keys.
17
+ - **Runtime loader.** Decrypt straight into `os.environ` — no cleartext file ever touches disk.
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install dotseal
25
+ ```
26
+
27
+ Requires Python 3.8+.
28
+
29
+ ---
30
+
31
+ ## Quickstart
32
+
33
+ ```bash
34
+ # 1. Generate a master key (saved to .dotseal.key and gitignored)
35
+ dotseal init
36
+
37
+ # 2. Write a normal .env file
38
+ cat > .env <<'EOF'
39
+ DATABASE_URL=postgres://user:pass@localhost:5432/db
40
+ DEBUG=True
41
+ API_KEY=super-secret
42
+ EOF
43
+
44
+ # 3. Encrypt it → .env.enc (commit this; never commit .env or the key)
45
+ dotseal encrypt
46
+
47
+ # 4. Decrypt when you need it back
48
+ dotseal decrypt
49
+ ```
50
+
51
+ ### What gets committed?
52
+
53
+ | File | Commit it? | Contents |
54
+ | --------------------- | ---------- | ----------------------------------------- |
55
+ | `.env.enc` | ✅ Yes | Keys in cleartext, values encrypted |
56
+ | `.env` | ❌ No | Full cleartext secrets |
57
+ | `.dotseal.key` | ❌ **Never**| The master key (auto-added to `.gitignore`) |
58
+
59
+ ---
60
+
61
+ ## CLI Reference
62
+
63
+ ### `dotseal init`
64
+ Generates a new cryptographically secure master key, writes it to `.dotseal.key` (mode `0600`), and adds it to `.gitignore` (creating one if needed). Prints the key **fingerprint** (not the key) so you can verify which key encrypted a file. Use `--force` to replace an existing key (this makes existing `.env.enc` files undecryptable).
65
+
66
+ ### `dotseal encrypt [input] [output]`
67
+ Encrypts the values of a cleartext env file. Defaults: `.env` → `.env.enc`. Idempotent — values that are already encrypted are left untouched.
68
+
69
+ ### `dotseal decrypt [input] [output]`
70
+ Decrypts values back to cleartext. Defaults: `.env.enc` → `.env`. The output is written with owner-only (`0600`) permissions since it contains secrets.
71
+
72
+ ### `dotseal edit [file]`
73
+ SOPS-style editing. Decrypts `.env.enc` to a temporary file (mode `0600`), opens it in `$EDITOR` (falling back to `nano`), and re-encrypts on save. The temp file is securely overwritten and deleted afterward. If the file doesn't exist yet, you get a fresh template to start from.
74
+
75
+ ### Common options
76
+ All commands except `init` accept:
77
+
78
+ - `-k, --key <base64>` — provide the master key directly (overrides env var and key file).
79
+ - `--key-file <path>` — use a specific key file instead of auto-discovery.
80
+
81
+ ---
82
+
83
+ ## Key Management
84
+
85
+ The master key is resolved in this order (first match wins):
86
+
87
+ 1. An explicit `--key` argument (CLI) or `master_key=` argument (loader).
88
+ 2. The `DOTSEAL_MASTER_KEY` environment variable.
89
+ 3. A local `.dotseal.key` file (searched for in the current directory and upward through parent directories).
90
+
91
+ The key is a base64-encoded 32-byte (AES-256) value. Generate one programmatically with:
92
+
93
+ ```python
94
+ from dotseal import generate_master_key
95
+ print(generate_master_key())
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Runtime Loader (no cleartext on disk)
101
+
102
+ `load_env` is a **drop-in replacement for `python-dotenv`'s `load_dotenv`** — it just reads an encrypted `.env.enc` instead of a cleartext `.env`. Call it once at startup and your secrets are available as ordinary environment variables through the `os` module:
103
+
104
+ ```python
105
+ import os
106
+ from dotseal import load_env
107
+
108
+ # Resolves the key from DOTSEAL_MASTER_KEY or .dotseal.key
109
+ load_env() # reads ".env.enc" by default
110
+
111
+ os.getenv("DATABASE_URL") # now available, like any env var
112
+ ```
113
+
114
+ Signature:
115
+
116
+ ```python
117
+ def load_env(
118
+ dotenv_path: str = ".env.enc",
119
+ *,
120
+ master_key: str | None = None,
121
+ override: bool = False,
122
+ encoding: str = "utf-8",
123
+ ) -> bool:
124
+ ...
125
+ ```
126
+
127
+ - `override=False` (default): existing process env vars win (12-factor friendly).
128
+ - `override=True`: decrypted values overwrite anything already in `os.environ`.
129
+ - Returns `True` if at least one variable was set (matching `load_dotenv`). Want the values as a `dict` instead? Use `decrypt_to_dict` (below).
130
+
131
+ Other programmatic helpers:
132
+
133
+ ```python
134
+ from dotseal import encrypt_text, decrypt_text, decrypt_to_dict, load_key_bytes
135
+
136
+ key = load_key_bytes("BASE64KEY==")
137
+ enc = encrypt_text("FOO=bar\n", key) # -> ".env.enc" text
138
+ cleartext = decrypt_text(enc, key) # -> ".env" text
139
+ mapping = decrypt_to_dict(enc, key) # -> {"FOO": "bar"}
140
+ ```
141
+
142
+ ---
143
+
144
+ ## File Format
145
+
146
+ ```env
147
+ # Generated by dotseal. DO NOT EDIT VALUES MANUALLY.
148
+ DATABASE_URL=ENC[AES_GCM,data:<base64(nonce ‖ ciphertext ‖ tag)>]
149
+ DEBUG=ENC[AES_GCM,data:...]
150
+ # dotseal: v=1 alg=AES_GCM key_fp=7ef08b59e6a945e4
151
+ ```
152
+
153
+ - Each value's payload is `base64(12-byte nonce ‖ ciphertext ‖ GCM tag)`.
154
+ - The variable name is bound as AAD, so values cannot be swapped between keys.
155
+ - The trailing `# dotseal:` metadata line records the algorithm and a **key fingerprint** (a one-way hash of the key). On decrypt, the fingerprint is checked first so a wrong key fails fast with a clear message instead of a cryptic crypto error.
156
+ - Comments and blank lines are preserved. Values containing spaces, `#`, or newlines are safely quoted/escaped on decryption.
157
+
158
+ ---
159
+
160
+ ## CI/CD Integration
161
+
162
+ The pattern is always the same: provide the master key via the `DOTSEAL_MASTER_KEY` environment variable (from your platform's secret store), commit only `.env.enc`, and either decrypt to a file or load at runtime.
163
+
164
+ ### GitHub Actions
165
+
166
+ Store the key as a repository/environment **secret** named `DOTSEAL_MASTER_KEY`.
167
+
168
+ ```yaml
169
+ jobs:
170
+ deploy:
171
+ runs-on: ubuntu-latest
172
+ env:
173
+ DOTSEAL_MASTER_KEY: ${{ secrets.DOTSEAL_MASTER_KEY }}
174
+ steps:
175
+ - uses: actions/checkout@v4
176
+ - uses: actions/setup-python@v5
177
+ with:
178
+ python-version: "3.12"
179
+ - run: pip install dotseal
180
+
181
+ # Option A: decrypt to a real .env for tools that expect a file
182
+ - run: dotseal decrypt .env.enc .env
183
+
184
+ # Option B: load at runtime inside your app (no cleartext file)
185
+ - run: python -c "from dotseal import load_env; load_env(); import app"
186
+ ```
187
+
188
+ ### Docker
189
+
190
+ Bake only the encrypted file into the image and pass the key at runtime:
191
+
192
+ ```dockerfile
193
+ FROM python:3.12-slim
194
+ WORKDIR /app
195
+ RUN pip install dotseal
196
+ COPY .env.enc .
197
+ COPY . .
198
+ # App calls load_env() on startup.
199
+ CMD ["python", "main.py"]
200
+ ```
201
+
202
+ ```bash
203
+ docker run -e DOTSEAL_MASTER_KEY="$(cat .dotseal.key)" my-image
204
+ ```
205
+
206
+ ```python
207
+ # main.py
208
+ from dotseal import load_env
209
+ load_env() # picks up DOTSEAL_MASTER_KEY from the container env
210
+ ```
211
+
212
+ ### Kubernetes
213
+
214
+ Store the master key in a `Secret` and expose it as `DOTSEAL_MASTER_KEY`:
215
+
216
+ ```yaml
217
+ env:
218
+ - name: DOTSEAL_MASTER_KEY
219
+ valueFrom:
220
+ secretKeyRef:
221
+ name: dotseal
222
+ key: master-key
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Security Notes & Limitations
228
+
229
+ - **AES-256-GCM** provides confidentiality *and* integrity. Tampered ciphertext or a wrong key is rejected rather than silently producing garbage.
230
+ - **AAD binding** prevents an attacker who can edit the committed `.env.enc` from relocating a high-privilege secret onto a low-privilege variable name.
231
+ - **Key fingerprint** is a domain-separated SHA-256 hash truncated to 8 bytes; it reveals nothing about the key itself.
232
+ - **Memory hygiene is best-effort.** dotseal overwrites the mutable key buffers it controls, but Python's immutable `str`/`bytes` and garbage collector mean secrets can still linger in memory. Do not rely on this for protection against an attacker with live process access.
233
+ - **The master key is the whole ballgame.** Anyone with the key can decrypt everything. Rotate it by re-encrypting with `dotseal init --force` followed by `encrypt`, and store it only in trusted secret managers.
234
+ - This tool is a single-key symmetric scheme. It does **not** implement multi-recipient/asymmetric key sharing (a SOPS + `age`/KMS feature).
235
+
236
+ ---
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ uv venv && uv pip install -e ".[dev]"
242
+ uv run pytest
243
+ ```
244
+
245
+ CI runs the full test suite on Python 3.8 through 3.14 (see `.github/workflows/test.yml`).
246
+
247
+ The test suite covers crypto round-trips, edge-case values (empty strings, `!!@#$%=`, unicode, multi-line, large), structural parsing, the runtime loader (asserting no side-effect files are written), and the full CLI lifecycle including `edit`.
248
+
249
+ ## License
250
+
251
+ MIT
@@ -0,0 +1,56 @@
1
+ """dotseal: Git-friendly encrypted env var manager with cleartext keys and sealed values.
2
+
3
+ Public API
4
+ ----------
5
+ * :func:`load_env` -- runtime loader (decrypt into ``os.environ``); drop-in for
6
+ ``python-dotenv``'s ``load_dotenv``.
7
+ * :func:`encrypt_text` / :func:`decrypt_text` -- whole-file transforms.
8
+ * :func:`decrypt_to_dict` -- decrypt into a mapping, in memory.
9
+ * :func:`generate_master_key` / :func:`resolve_master_key` -- key helpers.
10
+ * The exception hierarchy rooted at :class:`DotsealError`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .core import (
16
+ ENV_VAR_NAME,
17
+ KEY_FILE_NAME,
18
+ decrypt_text,
19
+ decrypt_to_dict,
20
+ encrypt_text,
21
+ resolve_master_key,
22
+ )
23
+ from .crypto import generate_master_key, key_fingerprint, load_key_bytes
24
+ from .exceptions import (
25
+ DecryptionError,
26
+ EncryptionError,
27
+ InvalidMasterKeyError,
28
+ KeyFingerprintMismatchError,
29
+ MasterKeyNotFoundError,
30
+ ParseError,
31
+ DotsealError,
32
+ )
33
+ from .loader import load_env
34
+
35
+ __version__ = "0.1.0"
36
+
37
+ __all__ = [
38
+ "__version__",
39
+ "load_env",
40
+ "encrypt_text",
41
+ "decrypt_text",
42
+ "decrypt_to_dict",
43
+ "generate_master_key",
44
+ "resolve_master_key",
45
+ "load_key_bytes",
46
+ "key_fingerprint",
47
+ "ENV_VAR_NAME",
48
+ "KEY_FILE_NAME",
49
+ "DotsealError",
50
+ "MasterKeyNotFoundError",
51
+ "InvalidMasterKeyError",
52
+ "KeyFingerprintMismatchError",
53
+ "DecryptionError",
54
+ "EncryptionError",
55
+ "ParseError",
56
+ ]