diff --git a/README.mediawiki b/README.mediawiki index 0e67e7b02e..24b7572513 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -820,6 +820,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Christopher Gilliard | Specification | Deployed +|- +| [[bip-0138.md|138]] +| Applications +| Compact encryption scheme for non-seed wallet data +| Pyth +| Specification +| Draft |- style="background-color: #ffcfcf" | [[bip-0140.mediawiki|140]] | Consensus (soft fork) diff --git a/bip-0138.md b/bip-0138.md new file mode 100644 index 0000000000..8d5e36ba33 --- /dev/null +++ b/bip-0138.md @@ -0,0 +1,512 @@ +``` + BIP: 138 + Layer: Applications + Title: Compact encryption scheme for non-seed wallet data + Authors: Pyth + Status: Draft + Type: Specification + Assigned: 2026-05-11 + License: BSD-2-Clause + Discussion: https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607 + https://groups.google.com/g/bitcoindev/c/5NgJbpVDgEc/m/TtGK9sF9BgAJ +``` + +## Introduction + +### Abstract + +This BIP defines a compact encryption scheme for **output script descriptors** (BIP-0380), +**wallet policies** (BIP-0388), **labels** (BIP-0329), and **wallet backup metadata** (BIP-0139). +The payload must not contain any private key material. + +Users can store encrypted backups on untrusted media or cloud services without leaking +addresses, script structures, or cosigner counts. The encryption key derives from the +lexicographically-sorted public keys in the descriptor, allowing any keyholder to decrypt +without additional secrets. + +Though designed for descriptors and policies, the scheme works equally well for labels +and backup metadata. + +### Copyright + +This BIP is licensed under the BSD 2-Clause License. +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the above copyright notice and this permission notice appear +in all copies. + +### Motivation + +Losing the **wallet descriptor** (or **wallet policy**) is just as catastrophic as +losing the seed itself. The seed lets you sign, but the descriptor maps you to your coins. +For multisig or miniscript wallets, keys alone won't help: without the descriptor, you +can't reconstruct the script. + +Offline storage of descriptors has two practical obstacles: + +1. **Descriptors are hard to store offline.** + Descriptors can be much longer than a 12/24-word seed. Paper and steel backups + become impractical or error-prone. + +2. **Online redundancy carries privacy risk.** + USB drives, phones, and cloud storage solve the length problem but expose your + wallet structure. Plaintext descriptors leak your pubkeys and script details. + Cloud storage is often unencrypted, and even cloud encryption could be compromised, + depending on (often opaque) implementation details. Its security also reduces to + that of the weakest device with cloud access. Each copy increases the attack surface. + +This BIP therefore proposes an **encrypted**, compact backup format that: + +* can be **safely stored in multiple places**, including untrusted online services, +* can be **decrypted only by intended holders** of specified public keys, + +See the original [Delving post](https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607/31) +for more background. + +### Expected properties + +* **Encrypted**: safe to store with untrusted cloud providers or backup services +* **Access controlled**: only designated cosigners can decrypt +* **Easy to implement**: it should not require any sophisticated tools/libraries. +* **Vendor-neutral**: works with any hardware signer + +### Scope + +This proposal targets output script descriptors (BIP-0380) and policies (BIP-0388), but the +scheme also works for labels (BIP-0329) and other wallet metadata like +wallet backup metadata (BIP-0139). + +Private key material MUST be removed before encrypting any payload. + +## Specification + +Note: in the followings sections, the operator ⊕ refers to the bitwise XOR operation. + +### Secret generation + +- Let $p_1, p_2, \dots, p_n$, be the public keys in the descriptor/wallet policy, in + increasing lexicographical order. The scheme is defined for any $n \geq 1$, + in particular it supports single-signature descriptors ($n = 1$). Each $p_i$ + is the x-only-normalized root public key of one *allowed* key expression + (see [Descriptor key requirements](#descriptor-key-requirements)). +- Let $s$ = sha256(sha256("BIP138_DECRYPTION_SECRET") | sha256("BIP138_DECRYPTION_SECRET") | $p_1$ | $p_2$ | ... | $p_n$) +- Let $s_i$ = sha256(sha256("BIP138_INDIVIDUAL_SECRET") | sha256("BIP138_INDIVIDUAL_SECRET") | $p_i$) +- Let $c_i$ = $s$ ⊕ $s_i$ + +Because $s$ and $s_i$ use distinct domain-separation tags, $s \neq s_i$ and +therefore $c_i$ is never the all-zero string. + +**Note:** To prevent attackers from decrypting the backup using publicly known +keys, explicitly exclude any public keys with x coordinate +`50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0` (the BIP341 NUMS +point, used as a taproot internal key in some applications). Additionally, exclude any +other publicly known keys. + +Applications that exclude additional keys SHOULD document this, although decryption +using these keys will simply fail. This does not affect decryption with the remaining +keys. + +### Descriptor key requirements + +Only key expressions of the extended-public-key type with a trailing +derivation step or wildcard contribute to $\{p_1, \dots, p_n\}$. Without +that derivation, the xpub's root pubkey, used to seed $s$, would also be +the on-chain pubkey of every spend, so observing one spend would let +anyone recompute $s$ and decrypt the backup.[^trailing-derivation] The +rule generalizes to any network prefix (`xpub`/`tpub`/`Vpub`/...). + +[^trailing-derivation]: **Why require trailing derivation?** + Any trailing derivation step, or the implicit child derivation forced by + a wildcard, breaks the identity between the xpub root pubkey and the + on-chain pubkey. Fixed-derivation expressions like `xpub.../0/5` are + allowed because the on-chain key (`xpub/0/5`) already differs from the + encryption seed (the xpub root). Literal pubkeys and bare xpubs are + disallowed for the same reason: the literal value is exactly what goes + on-chain, so $s$ would become recoverable from a single observed spend. + +Allowed forms (origin information `[...]` is optional and orthogonal): + +| Form | Example | +|-------------------------------------------------------------|-------------------| +| `/` (fixed derivation, no wildcard) | `xpub.../0/5` | +| `/*` (wildcard only) | `xpub.../*` | +| `//*` (fixed derivation followed by a wildcard) | `xpub.../0/*` | +| `/` (multipath, no wildcard) | `xpub.../<0;1>` | +| `//*` (multipath followed by a wildcard) | `xpub.../<0;1>/*` | + +Implementations: + +- MUST exclude every other form (literal pubkeys, bare xpubs, ...) from + $\{p_1, \dots, p_n\}$. +- MUST refuse to encode a backup if the resulting set is empty. +- SHOULD make the user aware of each excluded expression, since the + cosigner holding that key will be unable to decrypt the backup with + their key. + +### Key Normalization + +For each allowed expression, take the root extended public key (ignoring +origin information, the trailing derivation path, the wildcard, and any +multipath specifiers) and extract its x-coordinate. The result is the +32-byte **x-only public key** $p_i$.[^x-only] + +[^x-only]: **Why x-only keys?** + X-only public keys are 32 bytes, a natural size for cryptographic operations. + This format is also used in BIP340 (Schnorr signatures) and BIP341 (Taproot). + +### Encryption + +The format uses CHACHA20_POLY1305 (RFC 8439) as the encryption algorithm, +with a 96-bit random nonce and a 128-bit authentication tag to provide confidentiality +and integrity. + +[^chacha-default]: **Why CHACHA20-POLY1305 ?** + ChaCha20-Poly1305 is already used in Bitcoin Core (e.g., BIP324) and is widely + available in cryptographic libraries. It performs well in software without + hardware acceleration, making it suitable for hardware wallets and embedded devices. + +* let $nonce$ = random(96 bits) +* let $ciphertext$ = encrypt($payload$, $secret$, $nonce$) + +The $nonce$ MUST NOT be all zero. If the random source yields an all-zero nonce, +the encoder MUST generate a new one. + +### Decryption + +In order to decrypt the payload of a backup, the owner of a certain public key p +computes: + +* let $s_i$ = sha256(sha256("BIP138_INDIVIDUAL_SECRET") ‖ sha256("BIP138_INDIVIDUAL_SECRET") ‖ $p$) +* for each `individual_secret_i` generate `reconstructed_secret_i` = +`individual_secret_i` ⊕ `si` +* for each `reconstructed_secret_i` process $payload$ = +decrypt($ciphertext$, $secret$, $nonce$) + +Decryption will succeed if and only if **p** was one of the keys in the +descriptor/wallet policy. + +### Encoding + +The encrypted backup must be encoded as follows: + +`MAGIC` `VERSION` `DERIVATION_PATHS` `INDIVIDUAL_SECRETS` `ENCRYPTION` +`ENCRYPTED_PAYLOAD` + +#### Magic + +`MAGIC`: 6 bytes which are ASCII/UTF-8 representation of **BIP138**. + +#### Version + +`VERSION`: 1 byte unsigned integer representing the format version. The current +specification defines version `0x01`. + +#### Derivation Paths + +Note: the derivation-path vector should not contain duplicates. +Derivation paths are optional; they can be useful to simplify the recovery process +if one has used a non-common derivation path to derive his key.[^derivation-optional] + +[^derivation-optional]: **Why are derivation paths optional?** + When standard derivation paths are used, they are easily discoverable, making + them straightforward to brute-force. Omitting them enhances privacy by reducing + the information shared publicly about the descriptor scheme. + +`DERIVATION_PATH` follows this format: + +`COUNT` +`CHILD_COUNT` `CHILD` `...` `CHILD` +`...` +`CHILD_COUNT` `CHILD` `...` `CHILD` + +`COUNT`: 1-byte unsigned integer (0–255) indicating how many derivation paths are +included. +`CHILD_COUNT`: 1-byte unsigned integer (1–255) indicating how many children are in +the current path. +`CHILD`: 4-byte big-endian unsigned integer representing a child index per BIP-32. + +#### Individual Secrets + +At least one individual secret must be supplied.[^no-fingerprints] + +[^no-fingerprints]: **Why no fingerprints in plaintext encoding?** + Including fingerprints would leak direct information about the descriptor + participants, which compromises privacy. + +The `INDIVIDUAL_SECRETS` section follows this format: + +`COUNT` +`INDIVIDUAL_SECRET` +`INDIVIDUAL_SECRET` + +`COUNT`: 1-byte unsigned integer (1–255) indicating how many secrets are included. +`INDIVIDUAL_SECRET`: 32-byte serialization of the derived individual secret. + +Note: the individual secrets vector should not contain duplicates. Implementations +MAY deduplicate secrets during encoding or parsing. + +#### Encryption + +`ENCRYPTION`: 1-byte unsigned integer identifying the encryption algorithm. + +| Value | Definition | +|:-------|:---------------------------------------| +| 0x00 | Reserved | +| 0x01 | CHACHA20_POLY1305 | + +#### Payload Size Limits + +CHACHA20_POLY1305 (per RFC 8439) supports plaintext up to 2^38 - 64 bytes. +Implementations MAY impose stricter limits based on platform constraints +(e.g., limiting to 2^32 - 1 bytes on 32-bit architectures). + +Implementations MUST reject empty payloads. + +#### Ciphertext + +`CIPHERTEXT` is the encrypted data resulting from encryption of `PAYLOAD` with algorithm +defined in `ENCRYPTION` where `PAYLOAD` is encoded following this format: + +`CONTENT` `LENGTH` `PLAINTEXT` (`CONTENT` `LENGTH` `PLAINTEXT` ...) (`PADDING`) + +`LENGTH`: variable-length integer representing the length of `PLAINTEXT` in bytes. It MUST +be present. + +`PLAINTEXT`: the `LENGTH` bytes of payload data. + +`PADDING`: OPTIONAL bytes after the final `PLAINTEXT`, up to the end of the decrypted +`PAYLOAD`. Parsers MUST consume exactly `LENGTH` bytes of each `PLAINTEXT`. The padding +bytes are reserved for size padding (see Padding) and/or vendor-specific data, the same +way trailing bytes after `CIPHERTEXT` are reserved and ignored. + +#### Padding + +Backups that are updated over time can leak information through their size even when the +contents are encrypted. A wallet that uploads a new backup after every received transaction +can reveal activity to the storage provider through changing payload sizes. This is relevant +for mutable payloads such as BIP-329 or BIP-0139.[^padding-privacy] + +Padding is OPTIONAL and is an encoder concern only: decoders ignore the `PADDING` bytes and +MUST NOT validate any bucket. Because the padding sits inside `CIPHERTEXT`, the +`ENCRYPTED_PAYLOAD` `LENGTH` reveals only the padded size, not the real `PLAINTEXT` size. + +Encoders that pad SHOULD zero-fill `PAYLOAD` up to the smallest bucket `S_n` that fits it, +with `S_0 = 10240` (10 KiB) and `S_n = floor(S_0 * (5 / 4)^n)`. Payloads of 10 KiB or less +are padded to 10 KiB. This keeps padding overhead under 25% above the smallest fitting bucket +while coarsening the observable size. Padding is RECOMMENDED for mutable payloads and +unnecessary for static ones such as BIP-380 descriptors and BIP-388 wallet policies. + +[^padding-privacy]: **Why pad inside the ciphertext?** + Padding the encrypted payload, rather than appending bytes after the backup, hides the + real payload size from anyone who parses the backup, not just from someone who measures + the file size. The size field a parser sees is the padded `LENGTH`. + +#### Integer Encodings + +All variable-length integers are encoded as +[compact size](https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer). + +#### Content + +`CONTENT` is a variable length field defining the type of the `PLAINTEXT` that follows +it. It follows this format: + +`TYPE` (`LENGTH`) `DATA` + +A `PAYLOAD` carries one or more `CONTENT LENGTH PLAINTEXT` items, each `CONTENT` +describing the `PLAINTEXT` immediately following it. The sequence ends at the first +`TYPE` byte equal to `0x00`, or at the end of the `PAYLOAD`, whichever comes first; all +remaining bytes are `PADDING`. A `PAYLOAD` MUST contain at least one item (see +[Payload Size Limits](#payload-size-limits)). + +`TYPE`: 1-byte unsigned integer identifying how to interpret `DATA`. + +| Value | Definition | +|:-------|:---------------------------------------| +| 0x00 | Reserved | +| 0x01 | BIP Number (big-endian uint16) | +| 0x02 | Vendor-Specific Opaque Tag | + +`LENGTH`: variable-length integer representing the length of `DATA` in bytes. + +For all `TYPE` values except `0x01`, `LENGTH` MUST be present. + +`DATA`: variable-length field whose encoding depends on `TYPE`. + +For `TYPE` values defined above: +- 0x00: parsers MUST reject the payload. +- 0x01: `LENGTH` MUST be omitted and `DATA` is a 2-byte big-endian unsigned integer + representing the BIP number that defines it. +- 0x02: `DATA` MUST be `LENGTH` bytes of opaque, vendor-specific data. + +For all `TYPE` values except `0x01`, parsers MUST reject `CONTENT` if `LENGTH` exceeds +the remaining payload bytes. + +For an unknown `TYPE` less than `0x80`, parsers MUST consume its `LENGTH` bytes of +`DATA`, treat the content type as unknown, consume the following payload `LENGTH` and +`PLAINTEXT`, and continue with the next item. + +For an unknown `TYPE` greater than or equal to `0x80`, parsers MUST reject the +payload.[^type-upgrade] + +[^type-upgrade]: **Why the 0x80 threshold?** + The `TYPE >= 0x80` rule keeps the encoding upgradeable. A future version can + define a new content type at or above `0x80` that older parsers reject rather + than misread, while types below `0x80` stay backward compatible. + +#### BIP380 Descriptor Backup Content + +When `CONTENT` is `TYPE = 0x01` with `DATA = 0x017c` (BIP380), `PLAINTEXT` is a +UTF-8 BIP380 descriptor backup. It is either a single bare descriptor string, or +a JSON descriptor backup document (for several descriptors) when the first +character is `{`. + +Test vectors are in +[`bip380_descriptor_backup.json`](./bip-0138/test_vectors/bip380_descriptor_backup.json). + +Descriptor strings MUST NOT contain private key material and SHOULD include a +checksum. + +##### JSON Descriptor Backup Documents + +The descriptor backup document is a JSON object with the following fields: + +- `version`: integer. This specification defines version `1`. +- `descriptor_sets`: array of descriptor set objects. + +Each descriptor set describes BIP380 output script descriptors belonging to one +logical account or script family. + +##### Descriptor Set Fields + +`descriptor` is a required string containing a BIP380 output script descriptor. + +For BIP389 multipath descriptors, `/<0;1>` means receive and change, +respectively. The `change_descriptor` field MUST NOT be used. + +For descriptors without BIP389 multipath key expressions, `descriptor` is the +receive descriptor and `change_descriptor` is the change descriptor. + +If optional boolean `archived` is `true`, importing wallets SHOULD NOT use the +descriptor set for new address generation. + +`range` is an optional two-element array `[start, end]`, inclusive, describing +the ranged derivation indexes covered by the descriptor set. + +`birth_time` is an optional integer Unix timestamp in seconds, indicating a +lower bound for when the descriptor set may have received funds. Importing +wallets MAY use this value as a scanning hint. + +#### BIP388 Wallet Policy Backup Content + +When `CONTENT` is `TYPE = 0x01` with `DATA = 0x0184` (BIP388), `PLAINTEXT` is a +UTF-8 JSON wallet policy backup document. A BIP388 wallet policy is a descriptor +template plus a vector of key information items, as defined in BIP388. + +Test vectors are in +[`bip388.json`](./bip-0138/test_vectors/bip388.json). + +The document is one of two forms: + +- A single policy, a JSON object `{ "keys": [...], "policy": "