datavalv-client 0.5.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.
- datavalv_client-0.5.0/.gitignore +51 -0
- datavalv_client-0.5.0/LICENSE +21 -0
- datavalv_client-0.5.0/PKG-INFO +442 -0
- datavalv_client-0.5.0/README.md +417 -0
- datavalv_client-0.5.0/RELEASING.md +163 -0
- datavalv_client-0.5.0/pyproject.toml +73 -0
- datavalv_client-0.5.0/src/datavalv_client/__init__.py +45 -0
- datavalv_client-0.5.0/src/datavalv_client/_format_constants.py +29 -0
- datavalv_client-0.5.0/src/datavalv_client/cli.py +537 -0
- datavalv_client-0.5.0/src/datavalv_client/client.py +533 -0
- datavalv_client-0.5.0/src/datavalv_client/conformance.py +102 -0
- datavalv_client-0.5.0/src/datavalv_client/crypto.py +183 -0
- datavalv_client-0.5.0/src/datavalv_client/errors.py +296 -0
- datavalv_client-0.5.0/src/datavalv_client/format.py +41 -0
- datavalv_client-0.5.0/src/datavalv_client/pins.py +175 -0
- datavalv_client-0.5.0/src/datavalv_client/tokens.py +89 -0
- datavalv_client-0.5.0/tests/conftest.py +247 -0
- datavalv_client-0.5.0/tests/test_cli.py +657 -0
- datavalv_client-0.5.0/tests/test_client.py +641 -0
- datavalv_client-0.5.0/tests/test_conformance_cli.py +85 -0
- datavalv_client-0.5.0/tests/test_conformance_matrix.py +17 -0
- datavalv_client-0.5.0/tests/test_constants_generation.py +35 -0
- datavalv_client-0.5.0/tests/test_crypto.py +172 -0
- datavalv_client-0.5.0/tests/test_format.py +46 -0
- datavalv_client-0.5.0/tests/test_invite_key.py +60 -0
- datavalv_client-0.5.0/tests/test_packaging.py +229 -0
- datavalv_client-0.5.0/tests/test_pins.py +225 -0
- datavalv_client-0.5.0/tests/test_public_sample.py +249 -0
- datavalv_client-0.5.0/tests/test_readme.py +303 -0
- datavalv_client-0.5.0/tests/test_smoke.py +5 -0
- datavalv_client-0.5.0/tests/test_vectors.py +34 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
|
|
5
|
+
venv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
|
|
13
|
+
# Node / web
|
|
14
|
+
node_modules/
|
|
15
|
+
web/dist/
|
|
16
|
+
|
|
17
|
+
# CDK
|
|
18
|
+
cdk.out/
|
|
19
|
+
.cdk.staging/
|
|
20
|
+
|
|
21
|
+
# Secrets & keys — never commit signing keys
|
|
22
|
+
*.pem
|
|
23
|
+
*.key
|
|
24
|
+
.env
|
|
25
|
+
.env.*
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
|
|
30
|
+
# npm lockfile is kept; build output is not. `crypto/dist` and `client-ts/dist`
|
|
31
|
+
# are covered by the `dist/` rule above; the tarballs are what `npm pack` leaves
|
|
32
|
+
# at the repository root when somebody follows client-ts/README.md step 1.
|
|
33
|
+
*.tgz
|
|
34
|
+
crypto/test-results/
|
|
35
|
+
crypto/playwright-report/
|
|
36
|
+
|
|
37
|
+
.venv/
|
|
38
|
+
|
|
39
|
+
# uv-managed environment
|
|
40
|
+
infra/cdk.out/
|
|
41
|
+
|
|
42
|
+
# IDE
|
|
43
|
+
.idea/
|
|
44
|
+
.vscode/
|
|
45
|
+
*.swp
|
|
46
|
+
|
|
47
|
+
# The first-backup runbook has the operator save a scratch push_backup.py in
|
|
48
|
+
# client/. It carries their client id, key id and the path to a signing key, so
|
|
49
|
+
# it must not be committed -- and it is reproduced in the runbook itself, so
|
|
50
|
+
# there is nothing to preserve.
|
|
51
|
+
client/push_backup.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hypercode AB
|
|
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,442 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: datavalv-client
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Client library for pushing end-to-end encrypted backups to Datavalv
|
|
5
|
+
Project-URL: Homepage, https://datavalv.se
|
|
6
|
+
Project-URL: Documentation, https://datavalv.se/connect-system
|
|
7
|
+
Project-URL: Source, https://github.com/Hypercode-SE/datavalv-clients
|
|
8
|
+
Author: Hypercode AB
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: backup,datavalv,encryption,end-to-end
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Requires-Dist: cryptography>=44
|
|
22
|
+
Requires-Dist: httpx>=0.28
|
|
23
|
+
Requires-Dist: pyjwt[crypto]>=2.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# datavalv-client
|
|
27
|
+
|
|
28
|
+
Datavalv receives backups pushed by a server and hands them back to people in a
|
|
29
|
+
browser. The copy is encrypted on the machine that made it, under a key
|
|
30
|
+
Datavalv never holds, so neither Datavalv nor AWS can read it.
|
|
31
|
+
|
|
32
|
+
This package is the pushing half: a `datavalv` command for a scheduled job, and
|
|
33
|
+
a `DatavalvClient` for a program that would rather do it itself. It is the
|
|
34
|
+
procedure on <https://datavalv.se/connect-system> with the mechanical parts
|
|
35
|
+
done for you — the keypair, a fresh single-use JWT for every request, and the
|
|
36
|
+
four HTTP calls one backup costs, in order, with the checksum header the third
|
|
37
|
+
one requires.
|
|
38
|
+
|
|
39
|
+
One step is not a command, and never will be. Step 3 says why.
|
|
40
|
+
|
|
41
|
+
Python 3.13 or newer, on a POSIX system.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Connecting a server
|
|
46
|
+
|
|
47
|
+
Six steps. The first four are once per server; the last two are what you do per
|
|
48
|
+
customer and per night.
|
|
49
|
+
|
|
50
|
+
### 1. Install it
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv pip install datavalv-client
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
or `pip install datavalv-client` if the server has no `uv`. That is the whole
|
|
57
|
+
step for most people.
|
|
58
|
+
|
|
59
|
+
The source is public if you would rather read it before you run it, or build it
|
|
60
|
+
yourself:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/Hypercode-SE/datavalv-clients.git
|
|
64
|
+
cd datavalv-clients
|
|
65
|
+
uv pip install ./client
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For a server with no clone on it, build the wheel where you do have one:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv build client # writes dist/datavalv_client-0.5.0-py3-none-any.whl
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
copy that one file over, and install it there:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install datavalv_client-0.5.0-py3-none-any.whl
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It brings `cryptography`, `httpx` and `PyJWT` with it, and installs one command,
|
|
81
|
+
`datavalv`.
|
|
82
|
+
|
|
83
|
+
### 2. Make a signing keypair
|
|
84
|
+
|
|
85
|
+
Run this **on the machine that will push the backups**. A signing key that has
|
|
86
|
+
been copied from somewhere else has been in an `scp` buffer, a backup of a
|
|
87
|
+
laptop, and whatever else copied it.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
datavalv keygen
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
It writes `datavalv-signing.pem` in the current directory — created at mode
|
|
94
|
+
`600`, not created and then chmodded, because a file that exists readable for a
|
|
95
|
+
moment has been readable — and prints the **public** half to stdout, which is
|
|
96
|
+
the block you paste in step 3. Everything else it says goes to stderr.
|
|
97
|
+
|
|
98
|
+
The private half is never printed, never sent anywhere, and Datavalv stores only
|
|
99
|
+
the public one: there is no secret to fetch back later, and nothing we hold
|
|
100
|
+
proves anything in your place.
|
|
101
|
+
|
|
102
|
+
`--out PATH` puts the key somewhere else. `keygen` refuses to overwrite an
|
|
103
|
+
existing key file, because the old one may still be registered and pushing
|
|
104
|
+
backups; `--force` means you know that, and the old key should be retired in the
|
|
105
|
+
browser afterwards.
|
|
106
|
+
|
|
107
|
+
### 3. Register the server in the browser
|
|
108
|
+
|
|
109
|
+
Sign in at <https://datavalv.se>, open **Connected servers** in the account menu
|
|
110
|
+
(`/applications`), give the server a name and paste the whole public block from
|
|
111
|
+
step 2. Two values come back. Keep both:
|
|
112
|
+
|
|
113
|
+
| value | what it identifies |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `clientId` | this server |
|
|
116
|
+
| `kid` | this key |
|
|
117
|
+
|
|
118
|
+
**The first time that account signs in, a recovery code comes first.** Every new
|
|
119
|
+
account is shown one, once, and the screen asks for two of its four groups back
|
|
120
|
+
before it lets you on — an account that will only ever own a server included.
|
|
121
|
+
Write it down and keep it off the machine, then go on to **Connected servers**.
|
|
122
|
+
It is what ties the account's own key to a new password if the old one is lost.
|
|
123
|
+
|
|
124
|
+
**Why this is not a command.** `POST /v1/apps` sits on the Cognito authorizer,
|
|
125
|
+
so it takes a signed-in person — and a command line that asked for your Datavalv
|
|
126
|
+
password would be asking for the one password this product is built never to
|
|
127
|
+
see. That password never reaches Datavalv at all: the browser turns it into a
|
|
128
|
+
key that unwraps a vault and sends only a value derived from it, which is why a
|
|
129
|
+
stolen copy of our database is unreadable. A CLI prompt would be the one place
|
|
130
|
+
it existed in the clear. So this client makes the keypair and prints the public
|
|
131
|
+
half, and a human pastes it.
|
|
132
|
+
|
|
133
|
+
The same screen is where a key is rotated — add the new one, deploy it, retire
|
|
134
|
+
the old one, with both working in between — and where a server is retired
|
|
135
|
+
altogether.
|
|
136
|
+
|
|
137
|
+
### 4. Configure the four settings
|
|
138
|
+
|
|
139
|
+
| variable | flag | value |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `DATAVALV_API` | `--api` | `https://api.datavalv.se` (staging is `https://api-staging.datavalv.se`, and a registration in one is unknown to the other) |
|
|
142
|
+
| `DATAVALV_CLIENT_ID` | `--client-id` | the `clientId` from step 3 |
|
|
143
|
+
| `DATAVALV_KEY_ID` | `--key-id` | the `kid` from step 3 |
|
|
144
|
+
| `DATAVALV_PRIVATE_KEY` | `--private-key` | the **path** to the key from step 2 |
|
|
145
|
+
|
|
146
|
+
A flag wins over the variable; a scheduled job should use the variables.
|
|
147
|
+
|
|
148
|
+
`DATAVALV_PRIVATE_KEY` is a path on purpose. A private key in an environment
|
|
149
|
+
variable is in `ps`, in the shell history of whatever set it, and in every crash
|
|
150
|
+
dump of the process that read it. The client refuses key text there rather than
|
|
151
|
+
being helpfully permissive about it, and does not echo the value back when it
|
|
152
|
+
does — the natural error message would print your private key into whatever
|
|
153
|
+
collects your cron output.
|
|
154
|
+
|
|
155
|
+
If the key file is readable by anyone but its owner you get a warning, not a
|
|
156
|
+
refusal: a permission bit is worth telling you about, and not worth turning into
|
|
157
|
+
a night with no backup.
|
|
158
|
+
|
|
159
|
+
### 5. Create a vault for the customer
|
|
160
|
+
|
|
161
|
+
One vault per customer, organisation or server whose data you push. One call,
|
|
162
|
+
once.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
datavalv vault create --external-id kund-4711 --owner-email anna@example.se --name "Kund 4711" --retention 7
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`--external-id` is your own identifier for them and what every later call uses.
|
|
169
|
+
`--retention` is how many copies are kept (1 if you do not say). `--locale
|
|
170
|
+
sv|en` picks the language of the invitation the owner receives — supplied, never
|
|
171
|
+
detected, because no browser is involved in this call and Swedish is the
|
|
172
|
+
default guess.
|
|
173
|
+
|
|
174
|
+
The vault's id is printed on stdout; the rest is on stderr. The owner is
|
|
175
|
+
emailed, and the vault waits.
|
|
176
|
+
|
|
177
|
+
**The address is the whole of the connection.** There is no invitation link and
|
|
178
|
+
no code: the vault appears only to an account whose *confirmed* email address is
|
|
179
|
+
identical to the one you sent. A mistyped address still returns `201`, still
|
|
180
|
+
sends a mail, and is reported as an error to nobody. Read it back to the
|
|
181
|
+
customer before you run the command.
|
|
182
|
+
|
|
183
|
+
**The invitation names your system only once we have confirmed who registered
|
|
184
|
+
it.** Anybody can register a server under any name, and that mail goes out from
|
|
185
|
+
our domain — so until we have checked, it says only that a vault is waiting, and
|
|
186
|
+
carries neither your server's name nor the vault's. Get in touch once the server
|
|
187
|
+
is registered and we will do it; nothing else changes.
|
|
188
|
+
|
|
189
|
+
**Creations are counted per day.** A server may create twice as many vaults in a
|
|
190
|
+
day as it may hold — ten, at the default allowance of five — and deleting a vault
|
|
191
|
+
gives nothing back. Past that the call answers `429 quota_exceeded`, which is
|
|
192
|
+
exit code 5: tomorrow it works.
|
|
193
|
+
|
|
194
|
+
### 6. Check the vault's key
|
|
195
|
+
|
|
196
|
+
Every backup is encrypted to the vault's public key, and step 1 of a push gets
|
|
197
|
+
that key **from us**. We are the one party this design does not trust with your
|
|
198
|
+
plaintext, so that key is the one thing here you should not take our word for: a
|
|
199
|
+
service that answered with a key of its own could read every backup sent
|
|
200
|
+
afterwards, and nothing in the protocol would show it.
|
|
201
|
+
|
|
202
|
+
So this client remembers. The first key it is given for a vault is written down,
|
|
203
|
+
as `ssh` does with `known_hosts`, and **a different one is refused before
|
|
204
|
+
anything is encrypted** — exit code 7. A vault's key is made once, in its first
|
|
205
|
+
member's browser, and never changes.
|
|
206
|
+
|
|
207
|
+
That leaves the first time, and a person closes it. Once the owner has started
|
|
208
|
+
the vault:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
datavalv vault key --external-id kund-4711
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
prints the fingerprint of the key we offer, on stdout, and records nothing. The
|
|
215
|
+
vault's owner sees a fingerprint under their backups at datavalv.se, **worked out
|
|
216
|
+
in their browser from the key they hold** — ask them to read it out. If the two
|
|
217
|
+
are the same, push. Better still, record theirs, which makes no request and
|
|
218
|
+
never passes through us at all:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
datavalv vault key --external-id kund-4711 --trust de6272b8151979389437054b06df8a817a6ad62ab87faac4bbacb3360c1385d0
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
A substituted key is then refused on the very first push. Skip both and the
|
|
225
|
+
first push trusts the key it is offered, prints its fingerprint once, and pins
|
|
226
|
+
it — better than nothing, and the check above is what makes it more than that.
|
|
227
|
+
|
|
228
|
+
The fingerprint is SHA-256 over the key's DER, so nothing of ours is needed to
|
|
229
|
+
compute it:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
openssl pkey -pubin -in tenant-public.pem -outform DER | openssl dgst -sha256
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Pins live in `~/.local/state/datavalv/known_vaults.json` (under
|
|
236
|
+
`$XDG_STATE_HOME` if that is set), mode 600; `DATAVALV_KNOWN_VAULTS` names
|
|
237
|
+
another path. The Python and TypeScript clients share the file. **A machine with
|
|
238
|
+
no disk to remember on** — a container that starts empty every night — sees every
|
|
239
|
+
key for the first time, which protects nothing: mount that path, or pass the
|
|
240
|
+
fingerprint in from configuration (see *The library*).
|
|
241
|
+
|
|
242
|
+
**If a push exits 7**, nothing was sent. If the vault was deleted and created
|
|
243
|
+
again it is a new vault with a new key: get the new fingerprint from its owner,
|
|
244
|
+
and if it is the one offered,
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
datavalv vault key --external-id kund-4711 --forget
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
and push again. If it is not, do not push, and tell us.
|
|
251
|
+
|
|
252
|
+
### 7. Push a backup
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
datavalv push /var/backups/kund-4711.sql.gz --external-id kund-4711 --label "nightly"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
That is the four calls: fetch the vault's public key, register the copy, upload
|
|
259
|
+
the ciphertext, confirm it. All four or none — a copy counts only once the last
|
|
260
|
+
one has returned, and until then the previous copy is untouched.
|
|
261
|
+
|
|
262
|
+
The backup id goes to stdout and everything else to stderr, so
|
|
263
|
+
`BACKUP=$(datavalv push ...)` holds an id and nothing else. A dash reads stdin,
|
|
264
|
+
for a dump you would rather not write to disk:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pg_dump kund4711 | gzip | datavalv push - --external-id kund-4711
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The filename is encrypted and sent along, so the member who can open the backup
|
|
271
|
+
can see what it was called. `--name-as NAME` sends a different one and
|
|
272
|
+
`--no-name` sends none at all; reading from stdin sends none, because there is
|
|
273
|
+
no name to send and inventing one would put a guess in the vault. A path is
|
|
274
|
+
never sent — only its last component.
|
|
275
|
+
|
|
276
|
+
`--idempotency-key` is worth setting from a schedule that retries: without it, a
|
|
277
|
+
retry of a run that timed out stores the same night twice and spends the quota
|
|
278
|
+
twice.
|
|
279
|
+
|
|
280
|
+
**A `409` before the owner has started the vault is expected.** The vault's key
|
|
281
|
+
is made in the first member's browser, so until somebody signs in there is
|
|
282
|
+
genuinely nothing to encrypt against. `datavalv push` exits **5** for that, and
|
|
283
|
+
5 means *nothing is wrong, run it again later*. A schedule that treats it as a
|
|
284
|
+
failure pages somebody about a server that is working perfectly. The run after
|
|
285
|
+
the owner signs in goes through on its own, with nobody touching the server.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Exit codes
|
|
290
|
+
|
|
291
|
+
`datavalv` returns these so a scheduled job can branch on them without reading
|
|
292
|
+
prose:
|
|
293
|
+
|
|
294
|
+
| code | meaning |
|
|
295
|
+
|---|---|
|
|
296
|
+
| 0 | done |
|
|
297
|
+
| 1 | failed for a reason not covered below |
|
|
298
|
+
| 2 | the command line was wrong |
|
|
299
|
+
| 3 | a setting, or a file named on the command line, is missing or unusable |
|
|
300
|
+
| 4 | the signing key, client id or key id was refused |
|
|
301
|
+
| 5 | nothing is wrong; run it again later (vault not started yet, network, quota) |
|
|
302
|
+
| 6 | the service refused the request and a person must look at it |
|
|
303
|
+
| 7 | the key offered for the vault is not the one on record; nothing was sent |
|
|
304
|
+
|
|
305
|
+
5 is the one worth a rule in your scheduler: run it again unchanged, and the run
|
|
306
|
+
after the vault is started goes through. 3, 4, 6 and 7 need a person — 7 most
|
|
307
|
+
of all, and soon: see step 6. 2 means the command line itself was wrong, so
|
|
308
|
+
nobody was going to be paged about it anyway.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## What the crypto does
|
|
313
|
+
|
|
314
|
+
Per backup, on your server, before anything is sent:
|
|
315
|
+
|
|
316
|
+
- A fresh **AES-256-GCM** key is drawn for this one copy and the file is
|
|
317
|
+
encrypted under it. The whole 21-byte format header — magic bytes, version
|
|
318
|
+
and algorithm ids, the IV and its length — is the additional authenticated
|
|
319
|
+
data, so a header edited in transit makes the copy fail to open rather than
|
|
320
|
+
decrypt into something else.
|
|
321
|
+
- That key is wrapped to the **vault's RSA-3072 public key**, RSA-OAEP with
|
|
322
|
+
SHA-256, and the wrapped copy travels with the backup. The vault's private
|
|
323
|
+
key exists only inside members' browsers, wrapped under keys derived from a
|
|
324
|
+
member's password and their recovery code.
|
|
325
|
+
- A **SHA-256 digest is taken over the ciphertext**, never over the plaintext.
|
|
326
|
+
It is signed into the upload URL, so storage refuses a body that disagrees
|
|
327
|
+
with it, and Datavalv checks it again before the copy is allowed to count.
|
|
328
|
+
- The filename, when you send one, is encrypted under the same key with its own
|
|
329
|
+
IV — never the payload's, which would reuse a nonce under one AES-GCM key.
|
|
330
|
+
|
|
331
|
+
The format is frozen and shared with the browser implementation:
|
|
332
|
+
`format/v1.json` in this repository is the single source of both, and a
|
|
333
|
+
conformance matrix runs the two against each other on live code.
|
|
334
|
+
|
|
335
|
+
Nothing here can open a backup you have pushed — not for want of code, since the
|
|
336
|
+
format's decryption side is in `datavalv_client.crypto` and the conformance
|
|
337
|
+
suite runs on it, but because a source application is never handed the key it
|
|
338
|
+
would need. That is what makes a compromised server leak the backups it is
|
|
339
|
+
about to make rather than the ones it has made.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## The library
|
|
344
|
+
|
|
345
|
+
The CLI is a thin layer over this. Everything it does is available directly:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
from pathlib import Path
|
|
349
|
+
|
|
350
|
+
from datavalv_client import DatavalvClient
|
|
351
|
+
|
|
352
|
+
with DatavalvClient(
|
|
353
|
+
api_base="https://api.datavalv.se",
|
|
354
|
+
client_id=CLIENT_ID,
|
|
355
|
+
key_id=KEY_ID,
|
|
356
|
+
private_key=Path("/etc/datavalv/datavalv-signing.pem").read_text(),
|
|
357
|
+
) as client:
|
|
358
|
+
vault = client.create_vault("kund-4711", "anna@example.se", retention_count=7)
|
|
359
|
+
|
|
360
|
+
with open("/var/backups/kund-4711.sql.gz", "rb") as dump:
|
|
361
|
+
pushed = client.push_backup(
|
|
362
|
+
"kund-4711", dump, name="kund-4711.sql.gz", label="nightly"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
print(pushed.backup_id, pushed.size, pushed.sha256)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`private_key` is PEM **text** or an already-loaded `cryptography` key object —
|
|
369
|
+
never a path, which is the opposite of the environment variable's rule and for
|
|
370
|
+
the same reason: a client that accepted both would make the two spellings
|
|
371
|
+
interchangeable, and that is how a key ends up in an environment variable.
|
|
372
|
+
|
|
373
|
+
`push_backup` reads the whole file into memory. The size and the digest of the
|
|
374
|
+
ciphertext have to be known before an upload URL can be asked for, so there is
|
|
375
|
+
no streaming arrangement that would not encrypt the file twice.
|
|
376
|
+
|
|
377
|
+
Failures are `DatavalvError` subclasses, each naming the step it happened at,
|
|
378
|
+
and each saying whether running the same thing again could work:
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from datavalv_client import DatavalvError
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
client.push_backup("kund-4711", data)
|
|
385
|
+
except DatavalvError as exc:
|
|
386
|
+
if exc.retryable: # VaultNotInitialized, QuotaExceeded, NetworkError
|
|
387
|
+
log.info("not yet: %s", exc)
|
|
388
|
+
else: # AuthError, ConfigurationError, ApiError, UploadError,
|
|
389
|
+
raise # VaultKeyChanged -- never retry that one; see step 6
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**The vault's key is pinned here too**, with no argument needed: `pins` defaults
|
|
393
|
+
to the same file the CLI uses, and `push_backup` raises `VaultKeyChanged` — with
|
|
394
|
+
`.pinned` and `.offered` — before encrypting anything if the key is not the one
|
|
395
|
+
on record. `pushed.key_first_seen` is true on the push that recorded it, and
|
|
396
|
+
`pushed.key_fingerprint` is what to compare with the owner's. On a machine with
|
|
397
|
+
no disk to remember on, pass the fingerprint in from configuration instead, so
|
|
398
|
+
that a key which is not that one is refused from the first push:
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
from datavalv_client import DatavalvClient, MemoryPins
|
|
402
|
+
|
|
403
|
+
pins = MemoryPins({("https://api.datavalv.se", CLIENT_ID, "kund-4711"): FINGERPRINT})
|
|
404
|
+
client = DatavalvClient(
|
|
405
|
+
api_base="https://api.datavalv.se",
|
|
406
|
+
client_id=CLIENT_ID,
|
|
407
|
+
key_id=KEY_ID,
|
|
408
|
+
private_key=PRIVATE_KEY_PEM,
|
|
409
|
+
pins=pins,
|
|
410
|
+
)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`client.list_backups("kund-4711")` says what the service holds for a vault: ids,
|
|
414
|
+
sizes, digests, timestamps, labels and the encrypted filename. No key material —
|
|
415
|
+
the member listing is a different model in the API for exactly that reason.
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## What is not here
|
|
420
|
+
|
|
421
|
+
- **Restoring is the browser's.** A backup is decrypted inside a member's
|
|
422
|
+
browser with a key this package is never given. Signing in at datavalv.se is
|
|
423
|
+
the whole of the restore path.
|
|
424
|
+
- **Registering a server is the browser's**, for the reason in step 3.
|
|
425
|
+
- **Changing retention and deleting a vault** are API routes
|
|
426
|
+
(`PATCH` and `DELETE /v1/tenants/{externalId}`) that this client does not wrap
|
|
427
|
+
yet. Retention is set when the vault is created.
|
|
428
|
+
|
|
429
|
+
The protocol underneath, if you would rather implement it yourself, is
|
|
430
|
+
<https://datavalv.se/developers> — and the four calls with their fields are on
|
|
431
|
+
<https://datavalv.se/connect-system>, which this README follows step for step.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Developing and releasing
|
|
436
|
+
|
|
437
|
+
The package lives in `client/` of the Datavalv monorepo. Its tests run with
|
|
438
|
+
`cd client && uv run pytest`, and `make check` from the repository root runs
|
|
439
|
+
those plus the format governance the crypto depends on.
|
|
440
|
+
|
|
441
|
+
`RELEASING.md` is the publish procedure. It has not been run: nothing has been
|
|
442
|
+
uploaded to PyPI under this name by this project.
|