descriptor-encrypt
Overview
A rust library and CLI tool that efficiently encrypts a Bitcoin wallet descriptor such that it can only be recovered by a set of keys that can spend the funds.
Features
descriptor-encrypt provides a robust mechanism for encrypting Bitcoin wallet descriptors with a security model that maps directly to the spending conditions of the descriptor itself. Here's what it does in detail:
Threshold-Based Security Model
- Threshold Authentication: The descriptor's access control policy (e.g., m-of-n multisig) automatically determines the decryption threshold
- Policy Mirroring: Encryption security policy directly mirrors the descriptor's spending policy - if a wallet requires 2-of-3 keys to spend, it will also require 2-of-3 keys to decrypt
Cryptographic Implementation
- Deterministic Key Derivation: Master encryption keys are derived deterministically from the descriptor's structure and content
- Shamir Secret Sharing: Implements recursive Shamir's Secret Sharing to split the master encryption key according to the descriptor's threshold requirements
- Public Key-Based Access Control: Each share is encrypted with the corresponding public key from the descriptor
- ChaCha20-Poly1305 Encryption: Uses modern, efficient encryption for both the payload (ChaCha20) and the key shares (ChaCha20Poly1305)
Complex Descriptor Support
- Full Descriptor Coverage: Supports all Bitcoin descriptor types including:
- Single-sig (wpkh, pkh)
- Multi-sig (sorted, unsorted)
- Complex scripts (Miniscript expressions)
- Taproot descriptors with internal keys and script paths
- Nested Threshold Handling: Properly handles nested threshold conditions (e.g., an OR with AND conditions inside it)
- Time and Hash Lock Support: Maintains time locks and hash locks in the template while encrypting the specific values
Template and Path Extraction
- Origin Path Extraction: Can extract derivation paths from encrypted descriptors without full decryption
- Template Generation: Can reveal the structure of an encrypted descriptor (with dummy values) without revealing the actual keys
Compact Encoding
- Tag-Based Encoding: Uses tag-based encoding to minimize the size of the descriptor template
- Variable-Length Encoding: Uses LEB128 variable-length integers to minimize the size of the encrypted data
This ensures that a descriptor can only be decrypted by the same keys needed to spend from it, creating a direct correspondence between fund access and descriptor recovery.
Installation
To build the project, use the following command:
The executable will be located at target/release/descriptor-encrypt.
CLI Usage
descriptor-encrypt is a command-line tool for encrypting and decrypting Bitcoin descriptors.
Commands
-
Encrypt a Descriptor
Encrypts a Bitcoin descriptor and outputs the result as hex.
Arguments:
<DESCRIPTOR_STRING>: The Bitcoin descriptor string to encrypt.
Options:
-w, --with-full-secrecy: Enables full secrecy mode, which leaks no information about key inclusion without full decryption.
-
Decrypt a Descriptor
Decrypts hex-encoded encrypted descriptor data using a set of public keys.
Arguments:
<DATA>: hex-encoded encrypted data.-p, --pks <PKS>: Comma-separated list of public keys and xpubs (e.g., "pk1,pk2,pk3"). At least one public key must be provided.
-
Get Template Descriptor
Retrieves a template descriptor (with dummy keys, hashes, and timelocks) from hex-encoded encrypted data.
Arguments:
<DATA>: hex-encoded encrypted data.
-
Get Origin Derivation Paths
Retrieves the origin derivation paths from hex-encoded encrypted data.
Arguments:
<DATA>: hex-encoded encrypted data.
Library Usage
The core logic of descriptor-encrypt can also be used as a library in other Rust projects.
Key Functions
encrypt(desc: Descriptor<DescriptorPublicKey>) -> Result<Vec<u8>>
- Encrypts a descriptor such that it can only be recovered by a set of keys with access to the funds.
encrypt_with_full_secrecy(desc: Descriptor<DescriptorPublicKey>) -> Result<Vec<u8>>
- Identical to
encryptexcept it leaks no information about key inclusion without full decryption. - Provides maximum privacy but slower to decrypt, as we must try all possible combinations of shares and keys. This has a running time of $O((N+1)^K)$, where $N$ is the number of provided keys and $K$ is the number of shares.
decrypt(data: &[u8], pks: Vec<DescriptorPublicKey>) -> Result<Descriptor<DescriptorPublicKey>>
- Decrypts an encrypted descriptor using a set of public keys with access to the funds.
get_template(data: &[u8]) -> Result<Descriptor<DescriptorPublicKey>>
- Returns a template descriptor with dummy keys, hashes, and timelocks from the encrypted data.
get_origin_derivation_paths(data: &[u8]) -> Result<Vec<DerivationPath>>
- Returns the origin derivation paths found in the encrypted descriptor.
Demo
License
This project is licensed under the CC0-1.0 License.
Author
Joshua Doman joshsdoman@gmail.com