All proposals
BIP 138

Compact Encryption Scheme for Non-seed Wallet Data

Draft Type Specification Layer Applications
Preamble
BIP
138
Layer
Applications
Title
Compact Encryption Scheme for Non-seed Wallet Data
Author
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
//github.com/bitcoin/bips/pull/1951
Requires
32, 340, 380, 388, 389

Introduction

Abstract

This BIP defines a compact encryption scheme for output script descriptors (BIP-0380), wallet policies (BIP-0388), and other wallet metadata. 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 almost 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 are insufficient: without the descriptor, you can't reconstruct the script.

Multisig setups can be designed to retain access to coins even if one of the seeds is lost, for example a 3-of-3 MuSig2 keypath with a script path that lets 2-of-3 recover. But when one seed is lost, its public key may also be lost, rendering the coins inaccessible. This risk is not intuitive.

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, while USB sticks and other offline digital storage solutions degrade over time and are unlikely to survive flood or fire.

  2. Online redundancy carries privacy risk.
    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, using their xpub
    • can't be decrypted using public keys revealed on chain
  • Easy to implement:
    • it should not require any sophisticated tools/libraries
    • it should not require access to private key material, which secure signing systems may not yield
  • 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. This is because the use of public key material for decryption makes the scheme unsuitable for storing private key material.

Specification

Note: in the following sections, the operator ⊕ refers to the bitwise XOR operation.

Secret generation

  • Let p1, p2, …, pn, be the public keys in the descriptor/wallet policy, in increasing lexicographical order. The scheme is defined for any n ≥ 1, in particular it supports single-signature descriptors (n = 1). Each pi is the x-only-normalized root public key of one allowed key expression (see Descriptor key requirements).
  • Let s = tagged_hash("BIP138_DECRYPTION_SECRET", p1 | p2 | ... | pn)
  • Let si = tagged_hash("BIP138_INDIVIDUAL_SECRET", pi)
  • Let ci = ssi

where tagged_hash(tag, m) = sha256(sha256(tag) | sha256(tag) | m) is the tagged hash construction from BIP340.

Because s and si use distinct domain-separation tags, s ≠ si and therefore ci is never the all-zero string.

Keys published on chain are already kept out of {p1, …, pn} by the Descriptor key requirements. Implementations MUST exclude the key with x coordinate 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 (the BIP341 NUMS point). Applications MAY exclude any further key they know to be public.

Applications that exclude keys beyond the NUMS point SHOULD document which keys they exclude, so that users know which keys cannot open the backup. A cosigner holding an excluded key will simply fail to decrypt, while the cosigners holding the keys that remain in {p1, …, pn} can still decrypt the backup.

Note: A key is publicly known when a third party can learn it without the wallet owner disclosing it. There is no registry of such keys. In practice they come from two sources: the blockchain, and well-known constants.

Warning: An xpub can be known to a third party without ever appearing on chain. Some single-signature wallets, such as the Ledger and Trezor desktop apps, send the BIP44/49/84/86 account xpub to a server, the server then derive every key from that xpub. For a single-signature descriptor this leaks little the server does not already know, but reusing such an xpub as a key of a multisig descriptor lets that server decrypt every backup of the multisig. Users SHOULD build a multisig from accounts whose xpub was never sent to such a server, such as BIP48 or BIP87 accounts.

Descriptor key requirements

Only key expressions of the extended-public-key type with a trailing derivation step or wildcard contribute to {p1, …, pn}. 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 {p1, …, pn}.
  • MUST refuse to encode a backup if the resulting set is empty.
  • MUST 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 pi.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.3

  • 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 si = tagged_hash("BIP138_INDIVIDUAL_SECRET", p)
  • for each individual_secret_i generate reconstructed_secret_i = individual_secret_isi
  • 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

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

All implementations that can fetch xpubs from a signing device SHOULD try the following common paths automatically:

  • m/44h/<coin>h/{0..9}h
  • m/49h/<coin>h/{0..9}h
  • m/84h/<coin>h/{0..9}h
  • m/86h/<coin>h/{0..9}h
  • m/87h/<coin>h/{0..9}h
  • m/48h/<coin>h/{0..9}h/1h
  • m/48h/<coin>h/{0..9}h/2h

<coin> is the BIP44 coin type of the network being recovered: 0 for mainnet and 1 for test networks. {0..9}h covers accounts 0 through 9.

Some signing devices can fail or require confirmation when fetching xpubs for some of these paths. This list is not exhaustive: an implementation MAY treat a path as allowed to fail for any device it supports, in particular m/87h/<coin>h/{0..9}h, which is widely under-supported. Implementations SHOULD at least classify these:5

  • BitBox02: m/44h/<coin>h/{0..9}h, m/87h/<coin>h/{0..9}h
  • Ledger: m/87h/<coin>h/{0..9}h

If fetching an xpub for one of these common paths fails or times out and decryption fails, implementations SHOULD warn the user.

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.

The derivation-path vector SHOULD be encoded in lexicographic order. This avoids leaking information through implementation-specific ordering. The vector SHOULD NOT contain duplicates. Implementations SHOULD deduplicate paths during encoding or parsing.

Individual Secrets

At least one individual secret must be supplied.6

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.

The individual secrets vector SHOULD be encoded in lexicographic order. This avoids leaking information through implementation-specific ordering. The vector SHOULD NOT contain duplicates. Implementations SHOULD deduplicate secrets during encoding or parsing.

Encoders SHOULD append random decoy INDIVIDUAL_SECRET entries to hide the exact number of real decryption keys. The total count SHOULD be rounded up to the smallest bucket greater than or equal to the real count, starting at 5 and doubling for each following bucket: 5, 10, 20, and so on. The bucket sequence saturates at the 255 COUNT limit: when the smallest fitting bucket would exceed 255, encoders SHOULD pad to 255 instead. Decoders do not distinguish real entries from decoys; they try each entry until one decrypts the payload.

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_TYPE CONTENT_LENGTH CONTENT (CONTENT_TYPE CONTENT_LENGTH CONTENT ...) (PADDING)

CONTENT_LENGTH: variable-length integer representing the length of CONTENT in bytes. It MUST be present.

CONTENT: the CONTENT_LENGTH bytes of payload data.

PADDING: OPTIONAL bytes after the final CONTENT, up to the end of the decrypted PAYLOAD. Parsers MUST consume exactly CONTENT_LENGTH bytes of each CONTENT. 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.7

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

Integer Encodings

All variable-length integers are encoded as compact size.

Content Type

CONTENT_TYPE is a variable length field defining the type of the CONTENT that follows it. It follows this format:

TYPE (TYPE_LENGTH) TYPE_PARAMS

A PAYLOAD carries one or more CONTENT_TYPE CONTENT_LENGTH CONTENT items, each CONTENT_TYPE describing the CONTENT 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).

TYPE: 1-byte unsigned integer identifying how to interpret TYPE_PARAMS.

Value Definition
0x00 End of content items; padding follows
0x01 BIP Number (big-endian uint16)
0x02 Vendor-Specific Opaque Tag
0x03 String

TYPE_LENGTH: variable-length integer representing the length of TYPE_PARAMS in bytes.

For all TYPE values except 0x01, TYPE_LENGTH MUST be present.8

TYPE_PARAMS: variable-length field whose encoding depends on TYPE.

For TYPE values defined above:

  • 0x00: parsers MUST stop reading content items and treat the remaining bytes as padding.
  • 0x01: TYPE_LENGTH MUST be omitted and TYPE_PARAMS is a 2-byte big-endian unsigned integer representing the BIP number that defines it.
  • 0x02: TYPE_PARAMS MUST be TYPE_LENGTH bytes of opaque, vendor-specific data.
  • 0x03: TYPE_PARAMS MUST be empty, and the following CONTENT is the string itself, which MUST be valid UTF-8.

For all TYPE values except 0x01, parsers MUST reject CONTENT_TYPE if TYPE_LENGTH exceeds the remaining payload bytes.

For an unknown TYPE less than 0x80, parsers MUST consume its TYPE_LENGTH bytes of TYPE_PARAMS, treat the content type as unknown, consume the following payload CONTENT_LENGTH and CONTENT, and continue with the next item.

For an unknown TYPE greater than or equal to 0x80, parsers MUST reject the payload.9

BIP380 Descriptor Backup Content

When CONTENT_TYPE is TYPE = 0x01 with TYPE_PARAMS = 0x017c (BIP380), CONTENT 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.

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_TYPE is TYPE = 0x01 with TYPE_PARAMS = 0x0184 (BIP388), CONTENT 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.

The document is one of two forms:

  • A single policy, a JSON object { "keys": [...], "policy": "<template>" }.
  • Multiple policies, a JSON object { "version": 1, "policy_sets": [...] }.

Parsers select the form by structure: an object with a policy_sets field is the multiple form, an object with a policy field is the single form.

Policy Fields

keys is a required array of BIP388 key information items, each a serialized extended public key with optional key origin ([fingerprint/path]xpub). It MUST contain at least one key. Keys MUST NOT contain private key material.

policy is a required non-empty string containing a BIP388 wallet descriptor template, with @i key placeholders referring to keys.

Policy Set Fields

A policy set has the same keys and policy fields as the single form, plus the optional metadata fields:

If optional boolean archived is true, importing wallets SHOULD NOT use the policy for new address generation.

range is an optional two-element array [start, end], inclusive, describing the ranged derivation indexes covered by the policy.

birth_time is an optional integer Unix timestamp in seconds, indicating a lower bound for when the policy may have received funds. Importing wallets MAY use this value as a scanning hint.

Multiple Policies Document

The multiple form is a JSON object with the following fields:

  • version: integer. This specification defines version 1.
  • policy_sets: array of policy set objects.

Encrypted Payload

ENCRYPTED_PAYLOAD follows this format:

NONCE LENGTH CIPHERTEXT

NONCE: 12-byte (96-bit) nonce. Parsers MUST reject a backup whose NONCE is all zero. 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).10

File Extension

Backups saved to a file SHOULD use the .bip138 file extension for the raw binary encoding and the .txt extension for the Base64 text representation, so that the extension tells implementations which encoding to expect.

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 content 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 backups, including BIP380 and BIP388 content, a multi-content payload, and an all-zero-nonce backup that parsers MUST reject.
bip380_descriptor_backup.json contains test vectors for BIP380 descriptor backup documents.
bip388.json contains test vectors for BIP388 wallet policy backup documents.

Backward Compatibility

This BIP defines a new format and changes no existing one. Liana wallet uses an early version of this BIP, which is not compatible: its magic is BEB rather than BIP138.

Acknowledgements

This BIP builds on the backup scheme proposed by @bigspider on Delving Bitcoin, who also suggested the payload padding design.

Thanks to @Sjors for extensive reviews, many design contributions to the payload encoding, and a proof-of-concept implementation on top of Bitcoin Core. Thanks also to @murchandamus, @jonatack, @craigraw, @jp1ac4, and @torkelrogstad for their reviews and suggestions.


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

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

  5. Why this restricted list? The list covers common account paths for the network being recovered, accounts 0 through 9. Some signing devices reject or display some of these paths before exporting an xpub, so implementations should treat the listed paths for that device as optional rather than aborting recovery.↩︎

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

  7. 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 ENCRYPTED_PAYLOAD LENGTH.↩︎

  8. Why is TYPE_LENGTH mandatory? TYPE_LENGTH is what lets a parser skip a content type it does not implement and still read the items that follow; without it, the parser cannot know where TYPE_PARAMS ends. A string note placed before a descriptor, for example, must not stop an implementation that only handles descriptors from recovering that descriptor. A type whose TYPE_PARAMS is always empty still carries a zero TYPE_LENGTH for this reason. 0x01 is exempt because every implementation handles it, so it is never skipped blindly.↩︎

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

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