|
| 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 |
0 commit comments