1
0
mirror of https://github.com/bitcoin/bips.git synced 2026-10-12 19:24:45 +00:00
Files
bips/bip-encrypted-backup.md
2026-05-11 12:27:49 -07:00

15 KiB
Raw Blame History

BIP: ?
Layer: Applications
Title: Compact encryption scheme for non-seed wallet data
Authors: Pyth <pythcoiner@wizardsardine.com>
Status: Draft
Type: Specification
Assigned: ?
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 (draft BIP). 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.

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

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).
  • Let s = sha256(sha256("BIPXXX_DECRYPTION_SECRET") | sha256("BIPXXX_DECRYPTION_SECRET") | p_1 | p_2 | ... | p_n)
  • Let s_i = sha256(sha256("BIPXXX_INDIVIDUAL_SECRET") | sha256("BIPXXX_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.1 The rule generalizes to any network prefix (xpub/tpub/Vpub/...).

Allowed forms (origin information [...] is optional and orthogonal):

Form Example
<xpub>/<path> (fixed derivation, no wildcard) xpub.../0/5
<xpub>/* (wildcard only) xpub.../*
<xpub>/<path>/* (fixed derivation followed by a wildcard) xpub.../0/*
<xpub>/<a;b;...> (multipath, no wildcard) xpub.../<0;1>
<xpub>/<a;b;...>/* (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.2

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.

  • let nonce = random(96 bits)
  • let ciphertext = encrypt(payload, secret, nonce)

Decryption

In order to decrypt the payload of a backup, the owner of a certain public key p computes:

  • let s_i = sha256(sha256("BIPXXX_INDIVIDUAL_SECRET") ‖ sha256("BIPXXX_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 BIPXXX (TBD).

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

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

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 PLAINTEXT

Integer Encodings

All variable-length integers are encoded as compact size.

Content

CONTENT is a variable length field defining the type of PLAINTEXT being encrypted, it follows this format:

TYPE (LENGTH) DATA

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.

Parsers MUST skip unknown TYPE values less than 0x80, by consuming LENGTH bytes of DATA.

For unknown TYPE values greater than or equal to 0x80, parsers MUST stop parsing CONTENT.5

Encrypted Payload

ENCRYPTED_PAYLOAD follows this format:

NONCE LENGTH CIPHERTEXT

NONCE: 12-byte (96-bit) nonce. LENGTH: variable-length integer representing ciphertext length. CIPHERTEXT: variable-length ciphertext.

Note: CIPHERTEXT is followed by the end of the ENCRYPTED_PAYLOAD section.
Compliant parsers MUST stop reading after consuming LENGTH bytes of ciphertext; additional trailing bytes are reserved for vendor-specific extensions and MUST be ignored.

Text Representation

Implementations SHOULD encode and decode the backup using Base64 (RFC 4648).6

Rationale

See footnotes throughout the specification for design rationale.

Future Extensions

The version field enables possible future enhancements:

  • Additional encryption algorithms
  • Support for threshold-based decryption
  • Hiding number of participants
  • bech32m export

Implementation

Test Vectors

key_types.json contains test vectors for key serialisations.
content_type.json contains test vectors for contents types serialisations.
derivation_path.json contains test vectors for derivation paths serialisations.
individual_secrets.json contains test vectors for individual secrets serialization.
encryption_secret.json contains test vectors for generation of encryption secret.
chacha20poly1305_encryption.json contains test vectors for ciphertexts generated using CHACHA20-POLY1305. encrypted_backup.json contains test vectors for generation of complete encrypted backup.

Acknowledgements

// TBD


  1. 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. ↩︎

  2. 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). ↩︎

  3. 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. ↩︎

  4. Why no fingerprints in plaintext encoding? Including fingerprints would leak direct information about the descriptor participants, which compromises privacy. ↩︎

  5. Why the 0x80 threshold? The TYPE >= 0x80 rule means we're not stuck with the current TLV encoding. It has a nice upgrade property: you can still encode backward compatible stuff at the start. ↩︎

  6. Why Base64? PSBT (BIP174) is commonly exchanged as a Base64 string, so wallet software likely already supports this representation. ↩︎