Skip to content

Commit bfdf6f7

Browse files
committed
bip encrypted_backup
1 parent 2e3dd3f commit bfdf6f7

File tree

9 files changed

+1333
-0
lines changed

9 files changed

+1333
-0
lines changed

bip-encrypted-backup.md

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
```
2+
BIP: ?
3+
Title: Compact encryption scheme for non-seed wallet data
4+
Author: Pyth <pyth@pythcoiner.dev>
5+
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-xxxx
6+
Status: Draft
7+
Type: Specification
8+
Created: 2025-08-22
9+
License: BSD-2-Clause
10+
Post-History: https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607/31
11+
https://groups.google.com/g/bitcoindev/c/5NgJbpVDgEc
12+
```
13+
14+
## Introduction
15+
16+
### Abstract
17+
18+
This BIP defines a compact encryption scheme for **wallet descriptors** (BIP-0380),
19+
**wallet policies** (BIP-0388), **labels** (BIP-0329), and **wallet backup metadata** (json).
20+
The payload must not contain any private key material.
21+
22+
Users can store encrypted backups on untrusted media or cloud services without leaking
23+
addresses, script structures, or cosigner counts. The encryption key derives from the
24+
lexicographically-sorted public keys in the descriptor, allowing any keyholder to decrypt
25+
without additional secrets.
26+
27+
Though designed for descriptors and policies, the scheme works equally well for labels
28+
and backup metadata.
29+
30+
### Copyright
31+
32+
This BIP is licensed under the BSD 2-Clause License.
33+
Redistribution and use in source and binary forms, with or without modification, are
34+
permitted provided that the above copyright notice and this permission notice appear
35+
in all copies.
36+
37+
### Motivation
38+
39+
Losing the **wallet descriptor** (or **wallet policy**) is just as catastrophic as
40+
losing the seed itself. The seed lets you sign, but the descriptor maps you to your coins.
41+
For multisig or miniscript wallets, keys alone won't help—without the descriptor, you
42+
can't reconstruct the script.
43+
44+
Offline storage of descriptors has two practical obstacles:
45+
46+
1. **Descriptors are hard to store offline.**
47+
Descriptors can be much longer than a 12/24-word seed. Paper and steel backups
48+
become impractical or error-prone.
49+
50+
2. **Online redundancy carries privacy risk.**
51+
USB drives, phones, and cloud storage solve the length problem but expose your
52+
wallet structure. Plaintext descriptors leak your pubkeys and script details.
53+
Cloud encryption doesn't help against subpoenas or provider breaches, and each
54+
copy increases attack surface.
55+
56+
These constraints lead to an acute need for an **encrypted**, and
57+
ideally compact backup format that:
58+
59+
* can be **safely stored in multiple places**, including untrusted on-line services,
60+
* can be **decrypted only by intended holders** of specified public keys,
61+
62+
See the original [Delving post](https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607/31)
63+
for more background.
64+
65+
### Expected properties
66+
67+
* **Encrypted**: safe to store with untrusted cloud providers or backup services
68+
* **Access controlled**: only designated cosigners can decrypt
69+
* **Easy to implement**: it should not require any sophisticated tools/libraries.
70+
* **Vendor-neutral**: works with any hardware signer
71+
72+
### Scope
73+
74+
This proposal targets wallet descriptors (BIP-0380) and policies (BIP-0388), but the
75+
scheme also works for labels (BIP-0329) and other wallet metadata like
76+
[wallet backup](https://github.com/pythcoiner/wallet_backup).
77+
78+
Private key material MUST be removed before encrypting any payload.
79+
80+
## Specification
81+
82+
Note: in the followings sections, the operator ⊕ refers to the bitwise XOR operation.
83+
84+
### Secret generation
85+
86+
- Let $p_1, p_2, \dots, p_n$, be the public keys in the descriptor/wallet policy, in increasing lexicographical order
87+
- Let $s$ = sha256("BEB_BACKUP_DECRYPTION_SECRET" | $p_1$ | $p_2$ | ... | $p_n$)
88+
- Let $s_i$ = sha256("BEB_BACKUP_INDIVIDUAL_SECRET" | $p_i$)
89+
- Let $c_i$ = $s$ ⊕ $s_i$
90+
91+
**Note:** To prevent attackers from decrypting the backup using publicly known
92+
keys, explicitly exclude any public keys with x coordinate
93+
`50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0` (the BIP341 NUMS
94+
point, used as a taproot internal key in some applications). Additionally, exclude any
95+
other publicly known keys. In some cases, it may be possible to exclude certain keys
96+
from this process for customs applications or user needs, it is recommended to document
97+
such decision.
98+
99+
### Key Normalization
100+
101+
Before computing the encryption secret, all public keys in the descriptor/wallet policy MUST be normalized to **33-byte compressed public key format** (SEC format with 0x02 or 0x03 prefix).
102+
103+
The normalization process depends on the key type:
104+
105+
#### Extended Public Keys (xpubs)
106+
107+
For extended public keys (including those with origin information and/or multipaths):
108+
- Extract the root extended public key
109+
- Use its **compressed public key** (33 bytes)
110+
- Ignore derivation paths, origin information, and multipath specifiers
111+
112+
#### Compressed Public Keys
113+
114+
Already in the correct format—use as-is (33 bytes).
115+
116+
#### X-only Public Keys
117+
118+
For 32-byte x-only public keys:
119+
- Prepend 0x02 (assuming even y-coordinate)
120+
- Result is 33 bytes
121+
122+
#### Uncompressed Public Keys
123+
124+
For 65-byte uncompressed public keys (0x04 prefix):
125+
- Compress to SEC format using the y-coordinate parity
126+
- If y is even: prefix with 0x02
127+
- If y is odd: prefix with 0x03
128+
- Result is 33 bytes (prefix + x-coordinate)
129+
130+
See [keys_types.json](./bip-encrypted-backup/test_vectors/keys_types.json) for normalization test vectors.
131+
132+
### AES-GCM Encryption
133+
134+
* let $nonce$ = random()
135+
* let $ciphertext$ = aes_gcm_256_encrypt($payload$, $secret$, $nonce$)
136+
137+
### AES-GCM Decryption
138+
139+
In order to decrypt the payload of a backup, the owner of a certain public key p
140+
computes:
141+
142+
* let $s_i$ = sha256("BEB_BACKUP_INDIVIDUAL_SECRET" ‖ $p$)
143+
* for each `individual_secret_i` generate `reconstructed_secret_i` =
144+
`individual_secret_i``si`
145+
* for each `reconstructed_secret_i` process $payload$ =
146+
aes_gcm_256_decrypt($ciphertext$, $secret$, $nonce$)
147+
148+
Decryption will succeed if and only if **p** was one of the keys in the
149+
descriptor/wallet policy.
150+
151+
### Encoding
152+
153+
The encrypted backup must be encoded as follows:
154+
155+
`MAGIC` `VERSION` `DERIVATION_PATHS` `INDIVIDUAL_SECRETS` `ENCRYPTION`
156+
`ENCRYPTED_PAYLOAD`
157+
158+
#### Magic
159+
160+
`MAGIC`: 3 bytes which are ASCII/UTF-8 representation of **BEB** (`0x42, 0x45,
161+
0x42`).
162+
163+
#### Version
164+
165+
`VERSION`: 1 byte unsigned integer representing the format version. The current
166+
specification defines version `0x01`.
167+
168+
#### Derivation Paths
169+
170+
Note: the derivation-path vector should not contain duplicates.
171+
Derivation paths are optional; they can be useful to simplify the recovery process
172+
if one has used a non-common derivation path to derive his key.
173+
174+
`DERIVATION_PATH` follows this format:
175+
176+
`COUNT`
177+
`CHILD_COUNT` `CHILD` `...` `CHILD`
178+
`...`
179+
`CHILD_COUNT` `CHILD` `...` `CHILD`
180+
181+
`COUNT`: 1-byte unsigned integer (0–255) indicating how many derivation paths are
182+
included.
183+
`CHILD_COUNT`: 1-byte unsigned integer (1–255) indicating how many children are in
184+
the current path.
185+
`CHILD`: 4-byte big-endian unsigned integer representing a child index per BIP-32.
186+
187+
#### Individual Secrets
188+
189+
At least one individual secret must be supplied.
190+
191+
The `INDIVIDUAL_SECRETS` section follows this format:
192+
193+
`COUNT`
194+
`INDIVIDUAL_SECRET`
195+
`INDIVIDUAL_SECRET`
196+
197+
`COUNT`: 1-byte unsigned integer (1–255) indicating how many secrets are included.
198+
`INDIVIDUAL_SECRET`: 32-byte serialization of the derived individual secret.
199+
200+
Note: the individual secrets vector should not contain duplicates. Implementations
201+
MAY deduplicate secrets during encoding or parsing.
202+
203+
#### Encryption
204+
205+
`ENCRYPTION`: 1-byte unsigned integer identifying the encryption algorithm.
206+
207+
| Value | Definition |
208+
|:-------|:---------------------------------------|
209+
| 0x00 | Undefined |
210+
| 0x01 | AES-GCM-256 |
211+
212+
#### Payload Size Limits
213+
214+
AES-GCM-256 (per RFC5116) supports plaintext up to 2^36 - 31 bytes.
215+
Implementations MAY impose stricter limits based on platform constraints
216+
(e.g., limiting to 2^32 - 1 bytes on 32-bit architectures).
217+
218+
Implementations MUST reject empty payloads.
219+
220+
#### Ciphertext
221+
222+
`CIPHERTEXT` is the encrypted data resulting encryption of `PAYLOAD` with algorithm
223+
defined in `ENCRYPTION` where `PAYLOAD` is encoded following this format:
224+
225+
`CONTENT` `PLAINTEXT`
226+
227+
#### Content
228+
229+
`CONTENT` is a variable length field defining the type of `PLAINTEXT` being encrypted,
230+
it follows this format:
231+
232+
`LENGTH` `VARIANT`
233+
234+
`LENGTH`: 1-byte unsigned integer representing the length of `VARIANT` content.
235+
`VARIANT`: there is 3 variants:
236+
- if `LENGTH` == 0, it represent undefined content, no `VARIANT` follow.
237+
- if `LENGTH` == 2, `VARIANT` is 2-byte big-endian unsigned integer representing
238+
the related BIP number that defines the exact content category.
239+
- if 2 < `LENGTH` < 0xFF, `VARIANT` is `LENGTH` additional bytes carrying opaque,
240+
vendor-specific data.
241+
242+
Note: `LENGTH` = 0xFF is reserved for future extensions. Parsers MUST reject
243+
payloads with `LENGTH` = 0xFF by returning an error.
244+
245+
#### Encrypted Payload
246+
247+
`ENCRYPTED_PAYLOAD` follows this format:
248+
249+
`NONCE` `LENGTH` `CIPHERTEXT`
250+
251+
252+
`NONCE`: 12-byte nonce for AES-GCM-256.
253+
`LENGTH`: [compact
254+
size](https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer)
255+
integer representing ciphertext length.
256+
`CIPHERTEXT`: variable-length ciphertext.
257+
258+
Note: `CIPHERTEXT` is followed by the end of the `ENCRYPTED_PAYLOAD` section.
259+
Compliant parsers MUST stop reading after consuming `LENGTH` bytes of ciphertext;
260+
additional trailing bytes are reserved for vendor-specific extensions and MUST
261+
be ignored.
262+
263+
## Rationale
264+
265+
- Why derivation paths are optional: When standard derivation paths are used, they are
266+
easily discoverable, making them straightforward to brute-force. Omitting them
267+
enhances privacy by reducing the information shared publicly about the descriptor
268+
scheme.
269+
270+
- Why avoid including fingerprints in plaintext encoding: Including fingerprints leaks
271+
direct information about the descriptor participants, which compromises privacy.
272+
273+
274+
### Future Extensions
275+
276+
The version field enables possible future enhancements:
277+
278+
- Additional encryption algorithms
279+
- Support for threshold-based decryption
280+
- Hiding number of participants
281+
- bech32m export
282+
283+
### Implementation
284+
285+
- Rust [implementation](https://github.com/pythcoiner/bitcoin-encrypted-backup)
286+
287+
### Test Vectors
288+
289+
[key_types.json](./bip-encrypted-backup/test_vectors/keys_types.json) contains test
290+
vectors for key serialisations.
291+
[content_type.json](./bip-encrypted-backup/test_vectors/content_type.json) contains test
292+
vectors for contents types serialisations.
293+
[derivation_path.json](./bip-encrypted-backup/test_vectors/derivation_path.json) contains
294+
test vectors for derivation paths serialisations.
295+
[individual_secrets.json](./bip-encrypted-backup/test_vectors/individual_secrets.json)
296+
contains test vectors for individual secrets serialization.
297+
[encryption_secret.json](./bip-encrypted-backup/test_vectors/encryption_secret.json)
298+
contains test vectors for generation of encryption secret.
299+
[aesgcm256_encryption.json](./bip-encrypted-backup/test_vectors/aesgcm256_encryption.json)
300+
contains test vectors for ciphertexts generated using AES-GCM256.
301+
[encrypted_backup.json](./bip-encrypted-backup/test_vectors/encrypted_backup.json)
302+
contains test vectors for generation of complete encrypted backup.
303+
304+
## Acknowledgements
305+
306+
// TBD
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[
2+
{
3+
"description": "Basic encryption with short plaintext",
4+
"nonce": "000102030405060708090a0b",
5+
"plaintext": "48656c6c6f",
6+
"secret": "0000000000000000000000000000000000000000000000000000000000000000",
7+
"ciphertext": "c0ae5f3e6f609000697cc7c8de2b30ce8817ca44fa"
8+
},
9+
{
10+
"description": "Empty plaintext should fail",
11+
"nonce": "000102030405060708090a0b",
12+
"plaintext": "",
13+
"secret": "0000000000000000000000000000000000000000000000000000000000000000",
14+
"ciphertext": null
15+
},
16+
{
17+
"description": "Encryption with all zeros",
18+
"nonce": "000000000000000000000000",
19+
"plaintext": "00000000000000000000000000000000",
20+
"secret": "0000000000000000000000000000000000000000000000000000000000000000",
21+
"ciphertext": "cea7403d4d606b6e074ec5d3baf39d18d0d1c8a799996bf0265b98b5d48ab919"
22+
},
23+
{
24+
"description": "Encryption with all FFs",
25+
"nonce": "ffffffffffffffffffffffff",
26+
"plaintext": "ffffffffffffffffffffffffffffffff",
27+
"secret": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
28+
"ciphertext": "42c4417ae76f276beb09973a4b9b37155b3f5fe9af300dd8d2372023367d86b7"
29+
},
30+
{
31+
"description": "Longer plaintext",
32+
"nonce": "0f1e2d3c4b5a69788796a5b4",
33+
"plaintext": "546869732069732061206c6f6e67657220706c61696e746578742074686174207368756c6420626520656e637279707465642070726f7065726c792e",
34+
"secret": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
35+
"ciphertext": "ea2e3d6ac4724e3301f138b449495b9eed1f01207eb5f62d1c0e103f2237a8e459b1770a8c7b8eabf2d69922e5f767ad4de4d8d7bf737e49dd6fef6d7996158207af0edd60e87faf8a353d7c"
36+
}
37+
]
38+
39+

0 commit comments

Comments
 (0)