|
| 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