Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions bip-encrypted-backup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
```
BIP: ?
Layer: Applications
Title: Compact encryption scheme for non-seed wallet data
Author: // TBD
Comments-Summary: No comments yet.
Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-????
Status: Draft
Type: Informational
Created: 2025-08-22
License: BSD-2-Clause
```

## Introduction

### Abstract

This BIP defines a compact encryption scheme for **wallet descriptors** (BIP-0380),
**wallet policies** (BIP-0388), **labels** (BIP-0329), and
**wallet backup metadata** (json). The payload must not contain any private key material.
This scheme enables users to outsource long‑term storage to untrusted media or cloud
services without revealing which addresses, scripts, or number of cosigners are involved.
Encryption keys are derived from the lexicographically‑sorted public keys inside the
descriptor or policy, so any party who already holds one of those keys can later decrypt
the backup without extra secrets or round‑trips. The format uses AES-GCM-256 with a 96‑bit
random nonce and a 128‑bit authentication tag to provide confidentiality and integrity.
While initially designed for descriptors and policies, the same scheme encrypts labels
and backup metadata, allowing a uniform, vendor‑neutral, and future‑extensible backup format.

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

In practice, losing the **wallet descriptor** (or **wallet policy**) is often **as
catastrophic as losing the wallet’s seed** itself. While the seed grants the
ability to sign, the descriptor grants a map to the coins. In multisig or
miniscript contexts, keys alone are **not sufficient** for recovery: without the
original descriptor the wallet cannot reconstruct the script.

Offline storage of descriptors has two practical obstacles:

1. **Descriptors are hard to store offline.**
Descriptor string representation can be far longer than a 12/24-word seed phrase.
Paper, steel, and other long-term analog media quickly become impractical for such
lengths, or error-prone to transcribe.

2. **Online redundancy carries privacy risk.**
Keeping backups on USB thumb-drives, computers, phones, or (worst) cloud drives
avoids the first problem but amplifies surveillance risk: anyone who gains these
**plaintext descriptors** learns the wallet’s public keys, script structure,
etc... Even with encryption at the cloud provider, an attacker or a subpoena can
compel access, and each extra copy multiplies the attack surface.

These constraints lead to an acute need for an **encrypted**, and
ideally compact backup format that:

* can be **safely stored in multiple places**, including untrusted on-line 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**: this allows users to outsource its storage to untrusted parties,
for example, cloud providers, specialized services, etc.
* **Has access control**: decrypting it should only be available to the desired
parties (typically, a subset of the cosigners).
* **Easy to implement**: it should not require any sophisticated tools.
* **Vendor-independent**: it should be easy to implement using any hardware signing
device.

### Scope

The primary motivation of this proposal is to store a wallet descriptor(BIP-0380) or a
wallet policy(BIP-0388), but it seems valuable enough to also use this scheme to encrypt
payload containing others wallet-related metadata, like Labels(BIP-0329) or
[wallet backup](https://github.com/pythcoiner/wallet_backup).

Note: For any kind of payload intented to be encrypted with this scheme, private key
material MUST be removed before encryption.

## 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might like to use a subset of the public keys in some cases, e.g. if a Taproot descriptor contains an unspendable internal key that is commonly used across multiple descriptors.
cc @bigspider @darosior

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, we also need to specify that BIP0341 NUMS MUST be sorted out

Copy link
Contributor

@bigspider bigspider Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, for wallet policies, in the long term we might want to introduce a placeholder for a deterministically derived NUMS key, as discussed for example here. So there wouldn't be any xpub at all. But that's not currently specified in BIP-388.

Explicitly excluding all the pubkeys with x coordinate 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 seems appropriate and should be forward-compatible with variations of this scheme.

It might be worth mentioning that one could choose just a subset of it is not intended for some of these keys to be able to recover from the backup. The caveat is that it becomes application-specific which of those keys should be able to recover. So if that's mentioned, this should be discussed a bit (for example by adding a recommendation to clearly specify the details in the documentation of the application).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what i'm actually wondering: is there any constructions that have an unspendable key NOT using the BIP341 NUMS?
@hugohn @ben-kaufman @Rob1Ham

Is there some Lightning constructions using unspendable keys?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record:

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caveat is that it becomes application-specific which of those keys should be able to recover. So if that's mentioned, this should be discussed a bit (for example by adding a recommendation to clearly specify the details in the documentation of the application).

I'm quite sure there will be usescases where the user can disable some keys

- Let $s$ = sha256("BEB_DECRYPTION_SECRET" | $p_1$ | $p_2$ | ... | $p_n$)
- Let $s_i$ = sha256("BEB_INDIVIDUAL_SECRET" | $p_i$)
- Let $c_i$ = $s$ ⊕ $s_i$

**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. In some cases, it may be possible to exclude certain keys
from this process for customs applications or user needs, it is recommended to document
such decision.



### AES-GCM Encryption

* let $nonce$ = random()
* let $ciphertext$ = aes_gcm_256_encrypt($payload$, $secret$, $nonce$)

### AES-GCM Decryption

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

* let $s_i$ = sha256("BEB_INDIVIDUAL_SECRET" ‖ $p$)
* for each `individual_secret_i` generate `reconstructed_secret_i` =
`individual_secret_i` ⊕ `si`
* for each `reconstructed_secret_i` process $payload$ =
aes_gcm_256_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`: 3 bytes which are ASCII/UTF-8 representation of **BEB** (`0x42, 0x45,
0x42`).

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

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.

#### Ciphertext

`CIPHERTEXT` is the encrypted data resulting encryption of `PAYLOAD` with algorithm
defined in `TYPE` where `PAYLOAD` is encoded following this format:

`CONTENT` `PLAINTEXT`

#### Content

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

`LENGTH` `VARIANT`

`LENGTH`: 1-byte unsigned integer representing the length of `CONTENT` content.
`VARIANT`: there is 3 variants:
- if `LENGTH` == 0, it represent undefined content, no `VARIANT` follow.
- if `LENGTH` == 2, `VARIANT` is 2-byte big-endian signed integer representing
the related BIP number that defines the exact content category.
- if 2 < `LENGTH` < 0xFF, `VARIANT` is `LENGTH` additional bytes carrying opaque,
vendor-specific data.

Note: `LENGTH` = 0xFF is reserved for futures extensions.

#### Encrypted Payload

`ENCRYPTED_PAYLOAD` follows this format:

`TYPE` `NONCE` `LENGTH` `CIPHERTEXT`

`TYPE`: 1-byte unsigned integer identifying the encryption algorithm.

| Value | Definition |
|:-------|:---------------------------------------|
| 0x00 | Undefined |
| 0x01 | AES-GCM-256 |

`NONCE`: 12-byte nonce for AES-GCM-256.
`LENGTH`: [compact
size](https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer)
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.

## Rationale

- Why derivation paths are 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.

- Why avoid including fingerprints in plaintext encoding: Including fingerprints leaks
direct information about the descriptor participants, which compromises privacy.


### Future Extensions

The version field enables possible future enhancements:

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

### Implementation

- rust [implementation](https://github.com/pythcoiner/encrypted_backup)

### Test Vectors

See rust implementation [tests](https://github.com/pythcoiner/encrypted_backup/blob/3280f6f9706497671f08d9365414315159080a84/src/ll.rs#L511)

## Acknowledgements

// TBD
43 changes: 43 additions & 0 deletions bip.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
diff --git a/bip-encrypted-backup.md b/bip-encrypted-backup.md
index 1b347de..af96d2f 100644
--- a/bip-encrypted-backup.md
+++ b/bip-encrypted-backup.md
@@ -119,7 +119,7 @@ descriptor/wallet policy.

The encrypted backup must be encoded as follows:

-`MAGIC` `VERSION` `DERIVATION_PATHS` `INDIVIDUAL_SECRETS` `CONTENT` `ENCRYPTION`
+`MAGIC` `VERSION` `DERIVATION_PATHS` `INDIVIDUAL_SECRETS` `ENCRYPTION`
`ENCRYPTED_PAYLOAD`

#### Magic
@@ -176,6 +176,29 @@ The `INDIVIDUAL_SECRETS` section follows this format:
| 0x03 | BIP-0329 Labels (JSONL) |
| 0x04 | Wallet backup (JSON) |

+#### Ciphertext
+
+`CIPHERTEXT` is the encrypted data resulting encryption of `PAYLOAD` with algorithm
+defined in `TYPE` where `PAYLOAD` is encoded following this format:
+
+`CONTENT` `PLAINTEXT`
+
+#### Content
+
+`CONTENT` define the type of `PLAINTEXT` being encrypted, it follows this format:
+
+`LENGTH` `VARIANT`
+
+`LENGTH`: 1-byte unsigned integer representing the length of `CONTENT` content.
+`VARIANT`: there is 3 variants:
+ - if `LENGTH` == 0, it represent undefined content, there is no `VARIANT`.
+ - if `LENGTH` == 2, `VARIANT` is 2-byte signed integer (big-endian) representing
+ a BIP number defining the content type.
+ - if 2 < `LENGTH` < 0xFF, `VARIANT` is of length `LENGTH` bytes and the format is
+ let undefined in this specification in order to let room for proprietary usages.
+
+Note: `LENGTH` = 0xFF is reserved for futures extensions.
+
#### Encrypted Payload

`ENCRYPTED_PAYLOAD` follows this format: