Skip to content

Commit f73a0d5

Browse files
committed
bip encrypted_backup
1 parent 2e3dd3f commit f73a0d5

File tree

9 files changed

+1381
-0
lines changed

9 files changed

+1381
-0
lines changed

bip-encrypted-backup.md

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
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 storage is often unencrypted, and even cloud encryption could be compromised,
54+
depending on (often opaque) implementation details. Its security also reduces to
55+
that of the weakest device with cloud access. Each copy increases the attack surface.
56+
57+
This BIP therefore proposes an **encrypted**, and 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
87+
increasing lexicographical order
88+
- Let $s$ = sha256("BIP_XXXX_DECRYPTION_SECRET" | $p_1$ | $p_2$ | ... | $p_n$)
89+
- Let $s_i$ = sha256("BIP_XXXX_INDIVIDUAL_SECRET" | $p_i$)
90+
- Let $c_i$ = $s$ ⊕ $s_i$
91+
92+
**Note:** To prevent attackers from decrypting the backup using publicly known
93+
keys, explicitly exclude any public keys with x coordinate
94+
`50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0` (the BIP341 NUMS
95+
point, used as a taproot internal key in some applications). Additionally, exclude any
96+
other publicly known keys.
97+
98+
Applications that exclude additional keys SHOULD document this, although decryption
99+
using these keys will simply fail. This does not affect decryption with the remaining
100+
keys.
101+
102+
### Key Normalization
103+
104+
Before computing the encryption secret, all public keys in the descriptor/wallet policy
105+
MUST be normalized to **32-byte x-only public key format**.[^x-only]
106+
107+
[^x-only]: **Why x-only keys?**
108+
X-only public keys are 32 bytes, a natural size for cryptographic operations.
109+
This format is also used in BIP340 (Schnorr signatures) and BIP341 (Taproot).
110+
111+
The normalization process depends on the key type:
112+
113+
#### Extended Public Keys (xpubs)
114+
115+
For extended public keys (including those with origin information and/or multipaths):
116+
- Extract the root extended public key
117+
- Extract the **x-coordinate** from its public key (32 bytes)
118+
- Ignore derivation paths, origin information, and multipath specifiers
119+
120+
#### Compressed Public Keys
121+
122+
For 33-byte compressed public keys (0x02 or 0x03 prefix):
123+
- Remove the prefix byte
124+
- Result is 32 bytes (x-coordinate only)
125+
126+
#### X-only Public Keys
127+
128+
Already in the correct format—use as-is (32 bytes).
129+
130+
#### Uncompressed Public Keys
131+
132+
For 65-byte uncompressed public keys (0x04 prefix):
133+
- Extract the x-coordinate (bytes 1-32)
134+
- Result is 32 bytes
135+
136+
See [keys_types.json](./bip-encrypted-backup/test_vectors/keys_types.json) for
137+
normalization test vectors.
138+
139+
### Encryption
140+
141+
The format uses AEAD_CHACHA20_POLY1305 (RFC 8439) as the default encryption algorithm,
142+
with a 96-bit random nonce and a 128-bit authentication tag to provide confidentiality
143+
and integrity. AEAD_AES_256_GCM is also supported as an alternative.[^chacha-default]
144+
145+
[^chacha-default]: **Why CHACHA20-POLY1305 as default?**
146+
ChaCha20-Poly1305 is already used in Bitcoin Core (e.g., BIP324) and is widely
147+
available in cryptographic libraries. It performs well in software without
148+
hardware acceleration, making it suitable for hardware wallets and embedded devices.
149+
150+
* let $nonce$ = random(96 bits)
151+
* let $ciphertext$ = encrypt($payload$, $secret$, $nonce$)
152+
153+
### Decryption
154+
155+
In order to decrypt the payload of a backup, the owner of a certain public key p
156+
computes:
157+
158+
* let $s_i$ = sha256("BIP_XXXX_INDIVIDUAL_SECRET" ‖ $p$)
159+
* for each `individual_secret_i` generate `reconstructed_secret_i` =
160+
`individual_secret_i``si`
161+
* for each `reconstructed_secret_i` process $payload$ =
162+
decrypt($ciphertext$, $secret$, $nonce$)
163+
164+
Decryption will succeed if and only if **p** was one of the keys in the
165+
descriptor/wallet policy.
166+
167+
### Encoding
168+
169+
The encrypted backup must be encoded as follows:
170+
171+
`MAGIC` `VERSION` `DERIVATION_PATHS` `INDIVIDUAL_SECRETS` `ENCRYPTION`
172+
`ENCRYPTED_PAYLOAD`
173+
174+
#### Magic
175+
176+
`MAGIC`: 6 bytes which are ASCII/UTF-8 representation of **BIPXXX** (TBD).
177+
178+
#### Version
179+
180+
`VERSION`: 1 byte unsigned integer representing the format version. The current
181+
specification defines version `0x01`.
182+
183+
#### Derivation Paths
184+
185+
Note: the derivation-path vector should not contain duplicates.
186+
Derivation paths are optional; they can be useful to simplify the recovery process
187+
if one has used a non-common derivation path to derive his key.[^derivation-optional]
188+
189+
[^derivation-optional]: **Why are derivation paths optional?**
190+
When standard derivation paths are used, they are easily discoverable, making
191+
them straightforward to brute-force. Omitting them enhances privacy by reducing
192+
the information shared publicly about the descriptor scheme.
193+
194+
`DERIVATION_PATH` follows this format:
195+
196+
`COUNT`
197+
`CHILD_COUNT` `CHILD` `...` `CHILD`
198+
`...`
199+
`CHILD_COUNT` `CHILD` `...` `CHILD`
200+
201+
`COUNT`: 1-byte unsigned integer (0–255) indicating how many derivation paths are
202+
included.
203+
`CHILD_COUNT`: 1-byte unsigned integer (1–255) indicating how many children are in
204+
the current path.
205+
`CHILD`: 4-byte big-endian unsigned integer representing a child index per BIP-32.
206+
207+
#### Individual Secrets
208+
209+
At least one individual secret must be supplied.[^no-fingerprints]
210+
211+
[^no-fingerprints]: **Why no fingerprints in plaintext encoding?**
212+
Including fingerprints would leak direct information about the descriptor
213+
participants, which compromises privacy.
214+
215+
The `INDIVIDUAL_SECRETS` section follows this format:
216+
217+
`COUNT`
218+
`INDIVIDUAL_SECRET`
219+
`INDIVIDUAL_SECRET`
220+
221+
`COUNT`: 1-byte unsigned integer (1–255) indicating how many secrets are included.
222+
`INDIVIDUAL_SECRET`: 32-byte serialization of the derived individual secret.
223+
224+
Note: the individual secrets vector should not contain duplicates. Implementations
225+
MAY deduplicate secrets during encoding or parsing.
226+
227+
#### Encryption
228+
229+
`ENCRYPTION`: 1-byte unsigned integer identifying the encryption algorithm.
230+
231+
| Value | Definition |
232+
|:-------|:---------------------------------------|
233+
| 0x00 | Reserved |
234+
| 0x01 | AEAD_CHACHA20_POLY1305 (default) |
235+
| 0x02 | AEAD_AES_256_GCM |
236+
237+
#### Payload Size Limits
238+
239+
AEAD_CHACHA20_POLY1305 (per RFC 8439) supports plaintext up to 2^38 - 64 bytes.
240+
AEAD_AES_256_GCM (per RFC 5116) supports plaintext up to 2^36 - 31 bytes.
241+
Implementations MAY impose stricter limits based on platform constraints
242+
(e.g., limiting to 2^32 - 1 bytes on 32-bit architectures).
243+
244+
Implementations MUST reject empty payloads.
245+
246+
#### Ciphertext
247+
248+
`CIPHERTEXT` is the encrypted data resulting from encryption of `PAYLOAD` with algorithm
249+
defined in `ENCRYPTION` where `PAYLOAD` is encoded following this format:
250+
251+
`CONTENT` `PLAINTEXT`
252+
253+
#### Integer Encodings
254+
255+
All variable-length integers are encoded as
256+
[compact size](https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer).
257+
258+
#### Content
259+
260+
`CONTENT` is a variable length field defining the type of `PLAINTEXT` being encrypted,
261+
it follows this format:
262+
263+
`TYPE` (`LENGTH`) `DATA`
264+
265+
`TYPE`: 1-byte unsigned integer identifying how to interpret `DATA`.
266+
267+
| Value | Definition |
268+
|:-------|:---------------------------------------|
269+
| 0x00 | Reserved |
270+
| 0x01 | BIP Number (big-endian uint16) |
271+
| 0x02 | Vendor-Specific Opaque Tag |
272+
273+
`LENGTH`: variable-length integer representing the length of `DATA` in bytes.
274+
275+
For all `TYPE` values except `0x01`, `LENGTH` MUST be present.
276+
277+
`DATA`: variable-length field whose encoding depends on `TYPE`.
278+
279+
For `TYPE` values defined above:
280+
- 0x00: parsers MUST reject the payload.
281+
- 0x01: `LENGTH` MUST be omitted and `DATA` is a 2-byte big-endian unsigned integer
282+
representing the BIP number that defines it.
283+
- 0x02: `DATA` MUST be `LENGTH` bytes of opaque, vendor-specific data.
284+
285+
For all `TYPE` values except `0x01`, parsers MUST reject `CONTENT` if `LENGTH` exceeds
286+
the remaining payload bytes.
287+
288+
Parsers MUST skip unknown `TYPE` values less than `0x80`, by consuming `LENGTH` bytes
289+
of `DATA`.
290+
291+
For unknown `TYPE` values greater than or equal to `0x80`, parsers MUST stop parsing
292+
`CONTENT`.[^type-upgrade]
293+
294+
[^type-upgrade]: **Why the 0x80 threshold?**
295+
The `TYPE >= 0x80` rule means we're not stuck with the current TLV encoding.
296+
It has a nice upgrade property: you can still encode backward compatible stuff
297+
at the start.
298+
299+
#### Encrypted Payload
300+
301+
`ENCRYPTED_PAYLOAD` follows this format:
302+
303+
`NONCE` `LENGTH` `CIPHERTEXT`
304+
305+
`NONCE`: 12-byte (96-bit) nonce.
306+
`LENGTH`: variable-length integer representing ciphertext length.
307+
`CIPHERTEXT`: variable-length ciphertext.
308+
309+
Note: `CIPHERTEXT` is followed by the end of the `ENCRYPTED_PAYLOAD` section.
310+
Compliant parsers MUST stop reading after consuming `LENGTH` bytes of ciphertext;
311+
additional trailing bytes are reserved for vendor-specific extensions and MUST
312+
be ignored.
313+
314+
### Text Representation
315+
316+
Implementations SHOULD encode and decode the backup using Base64 (RFC 4648).[^psbt-base64]
317+
318+
[^psbt-base64]: **Why Base64?**
319+
PSBT (BIP174) is commonly exchanged as a Base64 string, so wallet software
320+
likely already supports this representation.
321+
322+
## Rationale
323+
324+
See footnotes throughout the specification for design rationale.
325+
326+
### Future Extensions
327+
328+
The version field enables possible future enhancements:
329+
330+
- Additional encryption algorithms
331+
- Support for threshold-based decryption
332+
- Hiding number of participants
333+
- bech32m export
334+
335+
### Implementation
336+
337+
- Rust [implementation](https://github.com/pythcoiner/bitcoin-encrypted-backup)
338+
339+
### Test Vectors
340+
341+
[key_types.json](./bip-encrypted-backup/test_vectors/keys_types.json) contains test
342+
vectors for key serialisations.
343+
[content_type.json](./bip-encrypted-backup/test_vectors/content_type.json) contains test
344+
vectors for contents types serialisations.
345+
[derivation_path.json](./bip-encrypted-backup/test_vectors/derivation_path.json) contains
346+
test vectors for derivation paths serialisations.
347+
[individual_secrets.json](./bip-encrypted-backup/test_vectors/individual_secrets.json)
348+
contains test vectors for individual secrets serialization.
349+
[encryption_secret.json](./bip-encrypted-backup/test_vectors/encryption_secret.json)
350+
contains test vectors for generation of encryption secret.
351+
[chacha20poly1305_encryption.json](./bip-encrypted-backup/test_vectors/chacha20poly1305_encryption.json)
352+
contains test vectors for ciphertexts generated using CHACHA20-POLY1305.
353+
[aesgcm256_encryption.json](./bip-encrypted-backup/test_vectors/aesgcm256_encryption.json)
354+
contains test vectors for ciphertexts generated using AES-GCM256.
355+
[encrypted_backup.json](./bip-encrypted-backup/test_vectors/encrypted_backup.json)
356+
contains test vectors for generation of complete encrypted backup.
357+
358+
## Acknowledgements
359+
360+
// TBD

0 commit comments

Comments
 (0)