Skip to content

BIP138: Compact Encryption Scheme for Non-seed Wallet Data#1951

Open
pythcoiner wants to merge 31 commits into
bitcoin:masterfrom
pythcoiner:encrypted_descriptor
Open

BIP138: Compact Encryption Scheme for Non-seed Wallet Data#1951
pythcoiner wants to merge 31 commits into
bitcoin:masterfrom
pythcoiner:encrypted_descriptor

Conversation

@pythcoiner

@pythcoiner pythcoiner commented Sep 4, 2025

Copy link
Copy Markdown
Contributor

This is a bip for encrypted backup, an encryption scheme for bitcoin wallet related metadata.

Mailing list post: https://groups.google.com/g/bitcoindev/c/5NgJbpVDgEc

@pythcoiner
pythcoiner marked this pull request as draft September 4, 2025 06:47
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-encrypted-backup.md Outdated
Comment thread bip-0138.md
@pythcoiner

Copy link
Copy Markdown
Contributor Author

thanks for the review! will address comments tmr!

@Sjors

Sjors commented Sep 4, 2025

Copy link
Copy Markdown
Member

Open questions

  • Deterministic nonce: Currently the nonce is generated randomly. Is it safe to produce a deterministic nonce, e.g. hash("NONCE" || plaintext || key_1 || … || key_n), or are there known security concerns with this approach?

In general nonce reuse is unsafe because if you make multiple backups over time, e.g. as you add more transaction labels, you would be reusing the nonce with different message. By including the plaintext in the nonce, you do address that concern.

However it still seems unwise to mess with cryptographic standards. It doesn't seem worth the risk for saving 32 bytes on something that's going to be at least a few hundred bytes for a typical multisig.

@shocknet-justin

Copy link
Copy Markdown

Concept ACK, seems adjacent to how some lightning tools enable users to recover SCB's with just their seed to identify and decrypt the backup. Makes sense for descriptors to have something similar.

@pythcoiner
pythcoiner force-pushed the encrypted_descriptor branch 7 times, most recently from 1e4ca34 to 3b6b6ad Compare September 5, 2025 06:30
@Sjors

Sjors commented Sep 5, 2025

Copy link
Copy Markdown
Member

Concept ACK

@pythcoiner

Copy link
Copy Markdown
Contributor Author

(not yet finish addressing comments)

Comment thread bip-encrypted-backup.md Outdated
@KeysSoze

KeysSoze commented Sep 9, 2025

Copy link
Copy Markdown

Hi @pythcoiner,

By coincidence, two weeks ago I started working on a proposal for a "Standard Encrypted Wallet Payload" to be placed inside an "Encrypted Envelope". The "Wallet Payload" contains descriptors and metadata but can also act as a full wallet backup including transactions, UTXOs and addresses. The proposal is very much a work in progress.

I only just found this discussion so am reading through it to compare it to my proposal. The descriptor backup in the "Wallet Payload" of my proposal seems to have some overlap with the BIP proposed here. If there is too much overlap I may reconsider progressing with my proposal.

As mentioned, my proposal is very much a work in progress but the wallet payload proposal can be found here:

https://gist.github.com/KeysSoze/7109a7f0455897b1930f851bde6337e3

Maybe jump to the test vector section to see what a basic backup of a descriptor and some meta data would look like prior to encryption.

https://gist.github.com/KeysSoze/7109a7f0455897b1930f851bde6337e3#test-vectors

As my proposal is designed to be modular and extensible the encryption envelopes may be extended to offer Multiparty Encryption and Authentication. See:

https://gist.github.com/KeysSoze/7109a7f0455897b1930f851bde6337e3#user-content-Expanding_the_Security_Model

I have already started documenting an encryption envelope that uses AES-256-GCM and password protection:

https://gist.github.com/KeysSoze/866d009ccd082edf6802df240154b20d

I have not written a reference implementation yet but there are well established python and Rust libraries for CBOR and COSE that should make implementing the BIPs relatively simple.

@pythcoiner

pythcoiner commented Sep 13, 2025

Copy link
Copy Markdown
Contributor Author

Hi @pythcoiner,

By coincidence, two weeks ago I started working on a proposal for a "Standard Encrypted Wallet Payload" to be placed inside an "Encrypted Envelope". The "Wallet Payload" contains descriptors and metadata but can also act as a full wallet backup including transactions, UTXOs and addresses. The proposal is very much a work in progress.

Hi @KeysSoze, this work seems more related/parallel to the wallet_backup specs I've work on few month ago.

But I've adopted a slightly different approach by simply using JSON.

FYI we already implemented this wallet backup format in Liana wallet and I plan to work on a BIP proposal relatively soon.

@pythcoiner
pythcoiner force-pushed the encrypted_descriptor branch from 53f5f99 to ea22157 Compare July 15, 2026 05:42
@pythcoiner

Copy link
Copy Markdown
Contributor Author

Added:

  • A string content type, I think it can be usefull as we now can have several content items
  • Common derivation paths to automatically try
  • Pad individual secrets with decoy
  • A note about sorting & deduplicating individual secrets
  • A note about sorting & deduplicating derivation paths

& updated tests vectors

Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 15, 2026
When walking the CONTENT/LENGTH/PLAINTEXT items of a decrypted payload,
stop at the first 0x00 TYPE byte and treat the remaining bytes as padding,
rather than rejecting the payload. This lets encoders zero-fill a payload up
to a padding bucket (BIP138 "Padding") without breaking decryption.

This is kept as a separate commit because the BIP138 text is not yet
unambiguous: the content-type table still lists 0x00 as "reject", while the
payload section describes the first 0x00 as the end of the item sequence.
See bitcoin/bips#1951 for the proposed clarification.
Other malformed content (e.g. an unknown TYPE >= 0x80) is still rejected.
@murchandamus murchandamus removed the PR Author action required Needs updates, has unaddressed review comments, or is otherwise waiting for PR author label Jul 15, 2026
@murchandamus

Copy link
Copy Markdown
Member

@pythcoiner: Is the PR now caught up on review comments? There still seem to be unresolved comments above. If it’s caught up, I’d give it another full read, otherwise, I’d postpone that.

Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 15, 2026
When walking the CONTENT/LENGTH/PLAINTEXT items of a decrypted payload,
stop at the first 0x00 TYPE byte and treat the remaining bytes as padding,
rather than rejecting the payload. This lets encoders zero-fill a payload up
to a padding bucket (BIP138 "Padding") without breaking decryption.

This is kept as a separate commit because the BIP138 text is not yet
unambiguous: the content-type table still lists 0x00 as "reject", while the
payload section describes the first 0x00 as the end of the item sequence.
See bitcoin/bips#1951 for the proposed clarification.
Other malformed content (e.g. an unknown TYPE >= 0x80) is still rejected.
Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 15, 2026
Two privacy refinements from the finalized spec, folded together because the
full-backup test vectors exercise both at once and because the decoy padding
is what makes the individual-secret count untrustworthy:

- Pad the individual secrets with random decoys up to a bucket boundary (5,
  10, 20, ..., saturating at the 255 count limit), hiding the exact number of
  real recipients. Decoders cannot tell decoys from real entries; they simply
  try each until one decrypts. Because the count now includes decoys, report
  it as "individual_secrets" rather than "recipients" in the metadata struct
  and the inspectbackup output.

- Omit common derivation paths (m/44h|49h|84h|86h|87h/<coin>h/<account>h and
  m/48h/<coin>h/<account>h/1h|2h) from the header, since recovery
  implementations try these automatically. Add IsCommonDerivationPath() and
  CommonDerivationPaths() helpers.

Test vectors synced with the reference implementation (bitcoin/bips#1951).
@Sjors

Sjors commented Jul 15, 2026

Copy link
Copy Markdown
Member

I synced up my draft implementation in Sjors/bitcoin#109 with 090fbf0. It no longer needs to deviate from the BIP.

I'll give the BIP itself another full read soon(tm) - assuming you're not planning on additional changes.

Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 15, 2026
When walking the CONTENT/LENGTH/PLAINTEXT items of a decrypted payload,
stop at the first 0x00 TYPE byte and treat the remaining bytes as padding,
rather than rejecting the payload. This lets encoders zero-fill a payload up
to a padding bucket (BIP138 "Padding") without breaking decryption.

This is kept as a separate commit because the BIP138 text is not yet
unambiguous: the content-type table still lists 0x00 as "reject", while the
payload section describes the first 0x00 as the end of the item sequence.
See bitcoin/bips#1951 for the proposed clarification.
Other malformed content (e.g. an unknown TYPE >= 0x80) is still rejected.
Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 15, 2026
Two privacy refinements from the finalized spec, folded together because the
full-backup test vectors exercise both at once and because the decoy padding
is what makes the individual-secret count untrustworthy:

- Pad the individual secrets with random decoys up to a bucket boundary (5,
  10, 20, ..., saturating at the 255 count limit), hiding the exact number of
  real recipients. Decoders cannot tell decoys from real entries; they simply
  try each until one decrypts. Because the count now includes decoys, report
  it as "individual_secrets" rather than "recipients" in the metadata struct
  and the inspectbackup output.

- Omit common derivation paths (m/44h|49h|84h|86h|87h/<coin>h/<account>h and
  m/48h/<coin>h/<account>h/1h|2h) from the header, since recovery
  implementations try these automatically. Add IsCommonDerivationPath() and
  CommonDerivationPaths() helpers.

Test vectors synced with the reference implementation (bitcoin/bips#1951).
@pythcoiner

Copy link
Copy Markdown
Contributor Author

I'll give the BIP itself another full read soon(tm) - assuming you're not planning on additional changes.

I do not plan any more changes

I've also sync the rust implem and it's CLI: https://github.com/pythcoiner/bip138
(I've tested cli w/ Ledger Nano S+, Bitbox02, Coldcard Mk4, but should also work with Specter DIY & Jade (and soon(tm) Trezor)

@pythcoiner

Copy link
Copy Markdown
Contributor Author

@murchandamus indeed I had overlook some points, but after one more review pass of the comments I think I'm done.

Comment thread bip-0138.md Outdated
Comment thread bip-0138.md Outdated

@Sjors Sjors left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This proposal is in good shape now. All my suggestions are about the BIP text and some variable naming.

Comment thread bip-0138.md
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md Outdated
Comment thread bip-0138.md
Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 16, 2026
When walking the CONTENT/LENGTH/PLAINTEXT items of a decrypted payload,
stop at the first 0x00 TYPE byte and treat the remaining bytes as padding,
rather than rejecting the payload. This lets encoders zero-fill a payload up
to a padding bucket (BIP138 "Padding") without breaking decryption.

This is kept as a separate commit because the BIP138 text is not yet
unambiguous: the content-type table still lists 0x00 as "reject", while the
payload section describes the first 0x00 as the end of the item sequence.
See bitcoin/bips#1951 for the proposed clarification.
Other malformed content (e.g. an unknown TYPE >= 0x80) is still rejected.
Sjors added a commit to Sjors/bitcoin that referenced this pull request Jul 16, 2026
Two privacy refinements from the finalized spec, folded together because the
full-backup test vectors exercise both at once and because the decoy padding
is what makes the individual-secret count untrustworthy:

- Pad the individual secrets with random decoys up to a bucket boundary (5,
  10, 20, ..., saturating at the 255 count limit), hiding the exact number of
  real recipients. Decoders cannot tell decoys from real entries; they simply
  try each until one decrypts. Because the count now includes decoys, report
  it as "individual_secrets" rather than "recipients" in the metadata struct
  and the inspectbackup output.

- Omit common derivation paths (m/44h|49h|84h|86h|87h/<coin>h/<account>h and
  m/48h/<coin>h/<account>h/1h|2h) from the header, since recovery
  implementations try these automatically. Add IsCommonDerivationPath() and
  CommonDerivationPaths() helpers.

Test vectors synced with the reference implementation (bitcoin/bips#1951).

@Sjors Sjors left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ACK 5d44116

Comment thread bip-0138.md Outdated
@pythcoiner

Copy link
Copy Markdown
Contributor Author

added acknowledgments & addressed a missed review comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.