use anyhow::{Result, anyhow, ensure};
use descriptor_tree::{KeylessDescriptorTree, ToDescriptorTree};
use itertools::Itertools;
use miniscript::{
Threshold,
descriptor::{Descriptor, DescriptorPublicKey},
};
use sha2::{Digest, Sha256};
use super::cipher::{AuthenticatedCipher, KeyCipher, UnauthenticatedCipher};
use super::shamir::{Share, reconstruct_secret, split_secret};
type Data = Vec<u8>;
type EncryptedShare = Vec<u8>;
type Nonce = [u8; 12];
type ShamirThreshold = Threshold<ShamirTree, 0>;
#[derive(Clone, Debug)]
enum ShamirTree {
Leaf(EncryptedShare),
Threshold(ShamirThreshold),
}
pub fn encrypt_with_authenticated_shards(
descriptor: Descriptor<DescriptorPublicKey>,
master_encryption_key: [u8; 32],
nonce: Nonce,
plaintext: Data,
) -> Result<(Vec<EncryptedShare>, Data)> {
let cipher = AuthenticatedCipher {};
encrypt_with_cipher(cipher, descriptor, master_encryption_key, nonce, plaintext)
}
#[allow(dead_code)]
pub fn encrypt_with_full_secrecy(
descriptor: Descriptor<DescriptorPublicKey>,
master_encryption_key: [u8; 32],
nonce: Nonce,
plaintext: Data,
) -> Result<(Vec<EncryptedShare>, Data)> {
let cipher = UnauthenticatedCipher {};
encrypt_with_cipher(cipher, descriptor, master_encryption_key, nonce, plaintext)
}
pub fn decrypt_with_authenticated_shards(
descriptor: Descriptor<DescriptorPublicKey>,
encrypted_shares: Vec<EncryptedShare>,
public_keys: Vec<DescriptorPublicKey>,
nonce: Nonce,
ciphertext: Data,
) -> Result<Data> {
let cipher = AuthenticatedCipher {};
let pks = public_keys.iter().map(Some).collect();
let tree = ShamirTree::reconstruct(&descriptor, &encrypted_shares)?;
tree.decrypt(pks, nonce, ciphertext, &cipher)
}
pub fn decrypt_with_full_secrecy(
descriptor: Descriptor<DescriptorPublicKey>,
encrypted_shares: Vec<EncryptedShare>,
public_keys: Vec<DescriptorPublicKey>,
nonce: Nonce,
ciphertext: Data,
) -> Result<Data> {
let cipher = UnauthenticatedCipher {};
let tree = ShamirTree::reconstruct(&descriptor, &encrypted_shares)?;
let num_slots = encrypted_shares.len();
let mut unique_keys = public_keys.clone();
unique_keys.sort();
unique_keys.dedup();
let mut choices_for_each_slot: Vec<Option<&DescriptorPublicKey>> =
unique_keys.iter().map(Some).collect();
choices_for_each_slot.push(None);
let combinations_iterator =
std::iter::repeat_n(choices_for_each_slot.iter().cloned(), num_slots)
.multi_cartesian_product();
for key_combination in combinations_iterator {
if let Ok(decrypted_payload) =
tree.decrypt(key_combination, nonce, ciphertext.clone(), &cipher)
{
return Ok(decrypted_payload);
}
}
Err(Error::DecryptionFailed.into())
}
fn encrypt_with_cipher<T: KeyCipher>(
cipher: T,
descriptor: Descriptor<DescriptorPublicKey>,
master_encryption_key: [u8; 32],
nonce: Nonce,
plaintext: Data,
) -> Result<(Vec<EncryptedShare>, Data)> {
let keyless_node = descriptor.to_tree().prune_keyless();
ensure!(keyless_node.is_some(), Error::NoKeysRequired);
let encrypted_payload = cipher.encrypt_payload(plaintext, master_encryption_key, nonce)?;
let mut hasher = Sha256::new();
hasher.update(&encrypted_payload);
let hash = hasher.finalize();
let tree = ShamirTree::build_tree(
&keyless_node.unwrap(),
master_encryption_key.to_vec(),
&hash.as_slice().try_into().unwrap(),
&cipher,
&mut 0,
)?;
let encrypted_shares = tree.extract_encrypted_shares();
Ok((encrypted_shares, encrypted_payload))
}
impl ShamirTree {
fn build_tree<T: KeyCipher>(
node: &KeylessDescriptorTree<DescriptorPublicKey>,
share: Data,
hash: &[u8; 32],
cipher: &T,
leaf_index: &mut usize,
) -> Result<Self> {
match node {
KeylessDescriptorTree::Key(pk) => {
let index = *leaf_index;
*leaf_index += 1;
Ok(ShamirTree::Leaf(
cipher.encrypt_share(share, pk, hash, index)?,
))
}
KeylessDescriptorTree::Threshold(thresh) => {
let xs: Vec<u8> = (1..=thresh.n() as u8).collect();
let shares = split_secret(&share, thresh.k(), &xs).map_err(|e| anyhow!(e))?;
let mut shamir_nodes = Vec::new();
for (node, share) in thresh.iter().zip(shares.into_iter()) {
let tree = Self::build_tree::<T>(node, share.ys, hash, cipher, leaf_index)?;
shamir_nodes.push(tree);
}
let shamir_thresh = ShamirThreshold::new(thresh.k(), shamir_nodes)?;
Ok(ShamirTree::Threshold(shamir_thresh))
}
}
}
fn extract_encrypted_shares(&self) -> Vec<EncryptedShare> {
match self {
ShamirTree::Leaf(share) => vec![share.clone()],
ShamirTree::Threshold(thresh) => thresh
.iter()
.flat_map(|node| node.extract_encrypted_shares())
.collect(),
}
}
fn reconstruct(
descriptor: &Descriptor<DescriptorPublicKey>,
shares: &Vec<EncryptedShare>,
) -> Result<Self> {
let keyless_node = descriptor.to_tree().prune_keyless();
ensure!(keyless_node.is_some(), Error::NoKeysRequired);
let mut leaf_index = 0;
let tree = ShamirTree::reconstruct_tree(&keyless_node.unwrap(), shares, &mut leaf_index)?;
ensure! {
leaf_index == shares.len(),
Error::TooManyShares
}
Ok(tree)
}
fn reconstruct_tree(
tree: &KeylessDescriptorTree<DescriptorPublicKey>,
shares: &Vec<EncryptedShare>,
leaf_index: &mut usize,
) -> Result<Self> {
match tree {
KeylessDescriptorTree::Key(_) => {
ensure! {
*leaf_index < shares.len(),
Error::InsufficientShares
}
let index = *leaf_index;
*leaf_index += 1;
Ok(ShamirTree::Leaf(shares[index].clone()))
}
KeylessDescriptorTree::Threshold(thresh) => {
let mut shamir_nodes = Vec::new();
for node_inner in thresh.iter() {
let tree = Self::reconstruct_tree(node_inner, shares, leaf_index)?;
shamir_nodes.push(tree);
}
let shamir_thresh = ShamirThreshold::new(thresh.k(), shamir_nodes)?;
Ok(ShamirTree::Threshold(shamir_thresh))
}
}
}
fn decrypt<T: KeyCipher>(
&self,
keys: Vec<Option<&DescriptorPublicKey>>,
nonce: Nonce,
ciphertext: Data,
cipher: &T,
) -> Result<Data> {
let mut hasher = Sha256::new();
hasher.update(&ciphertext);
let hash = hasher.finalize();
let secret = self.decrypt_tree::<T>(
&keys,
&hash.as_slice().try_into().unwrap(),
cipher,
&mut 0,
true,
)?;
assert!(secret.len() == 32);
cipher.decrypt_payload(ciphertext, secret.as_slice().try_into().unwrap(), nonce)
}
fn decrypt_tree<T: KeyCipher>(
&self,
keys: &Vec<Option<&DescriptorPublicKey>>,
hash: &[u8; 32],
cipher: &T,
leaf_index: &mut usize,
decrypt_leaves: bool,
) -> Result<Data, Error> {
match self {
ShamirTree::Leaf(encrypted_share) => {
let index = *leaf_index;
*leaf_index += 1;
if !decrypt_leaves {
return Ok(vec![]);
}
if let Ok(plaintext) =
cipher.decrypt_share(encrypted_share.to_vec(), keys, hash, index)
{
return Ok(plaintext);
}
Err(Error::KeysRequired(1))
}
ShamirTree::Threshold(thresh) => {
let mut shares = Vec::new();
let mut keys_required = Vec::new();
for (i, node) in thresh.iter().enumerate() {
match node.decrypt_tree::<T>(
keys,
hash,
cipher,
leaf_index,
shares.len() < thresh.k(),
) {
Ok(ys) => {
let share = Share {
x: (i as u8) + 1,
ys,
};
shares.push(share);
}
Err(Error::KeysRequired(n)) => {
keys_required.push(n);
}
err => return err,
}
}
if shares.len() >= thresh.k() {
reconstruct_secret(&shares, thresh.k()).map_err(Error::InvalidShamir)
} else {
keys_required.sort();
let nodes_required = thresh.k() - shares.len();
let min_keys_required = keys_required[0..nodes_required].iter().sum();
Err(Error::KeysRequired(min_keys_required))
}
}
}
}
}
#[derive(Debug, PartialEq)]
pub enum Error {
NoKeysRequired,
InsufficientShares,
TooManyShares,
InvalidShamir(String),
KeysRequired(usize),
DecryptionFailed,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::NoKeysRequired => write!(f, "descriptor must require a key"),
Self::InsufficientShares => write!(f, "insufficient shares"),
Self::TooManyShares => write!(f, "too many shares"),
Self::InvalidShamir(err) => write!(f, "invalid shamir: {err}"),
Self::KeysRequired(num_required) => {
write!(f, "requires {num_required} additional key(s)")
}
Self::DecryptionFailed => {
write!(f, "unable to decrypt")
}
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1;
use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
use std::str::FromStr;
const NONCE_VALUE: Nonce = [0u8; 12_usize];
fn create_test_key(index: u32) -> DescriptorPublicKey {
let secp = secp256k1::Secp256k1::new();
let secret_value = 1u32 + index;
let mut sk_bytes = [0u8; 32];
sk_bytes[28..32].copy_from_slice(&secret_value.to_be_bytes());
let pk = bitcoin::PublicKey {
inner: secp256k1::PublicKey::from_secret_key(
&secp,
&secp256k1::SecretKey::from_slice(&sk_bytes).expect("sk"),
),
compressed: true,
};
DescriptorPublicKey::Single(miniscript::descriptor::SinglePub {
key: miniscript::descriptor::SinglePubKey::FullKey(pk),
origin: None,
})
}
fn create_threshold_descriptor(
k: usize,
n: usize,
) -> (Descriptor<DescriptorPublicKey>, Vec<DescriptorPublicKey>) {
let mut pubkeys = Vec::new();
for i in 1..=n as u32 {
let pubkey_val = create_test_key(3 * i);
pubkeys.push(pubkey_val);
}
let pubkey_strs_vec: Vec<String> =
pubkeys.iter().map(|pk_val| pk_val.to_string()).collect();
let desc_str = format!("wsh(multi({},{}))", k, pubkey_strs_vec.join(","));
let descriptor = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
(descriptor, pubkeys)
}
fn get_encrypted_data(
descriptor: Descriptor<DescriptorPublicKey>,
) -> (Vec<EncryptedShare>, Data, Data) {
let master_key = [1u8; 32];
let plaintext: Data = b"This is test plaintext".to_vec();
let (shares, ciphertext) = encrypt_with_authenticated_shards(
descriptor,
master_key,
NONCE_VALUE,
plaintext.clone(),
)
.unwrap();
(shares, plaintext, ciphertext)
}
fn get_encrypted_data_with_full_secrecy(
descriptor: Descriptor<DescriptorPublicKey>,
) -> (Vec<EncryptedShare>, Data, Data) {
let master_key = [1u8; 32];
let plaintext: Data = b"This is test plaintext for unauth".to_vec();
let (shares, ciphertext) =
encrypt_with_full_secrecy(descriptor, master_key, NONCE_VALUE, plaintext.clone())
.unwrap();
(shares, plaintext, ciphertext)
}
#[test]
fn test_single_key_encryption() {
let pubkey_val = create_test_key(1);
let desc_str = format!("wpkh({})", pubkey_val);
let descriptor = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
let (shares, plaintext, ciphertext) = get_encrypted_data(descriptor.clone());
assert_eq!(
shares.len(),
1,
"Single key descriptor should produce one share"
);
let decrypted_plaintext = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
vec![pubkey_val.clone()],
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_plaintext, plaintext,
"Decrypted plaintext doesn't match original"
);
let wrong_key_val = create_test_key(2);
let result = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
vec![wrong_key_val],
NONCE_VALUE,
ciphertext,
);
assert!(result.is_err(), "Decryption should fail with wrong key");
}
#[test]
fn test_multi_key_threshold_1of2() {
let (descriptor, pubkeys) = create_threshold_descriptor(1, 2);
let (shares, plaintext, ciphertext) = get_encrypted_data(descriptor.clone());
assert_eq!(shares.len(), 2, "1-of-2 multisig should produce 2 shares");
for key in pubkeys {
let key_subset_vec = vec![key.clone()];
let decrypted_plaintext = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_vec,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_plaintext, plaintext,
"Decrypted plaintext doesn't match original"
);
}
}
#[test]
fn test_multi_key_threshold_2of3() {
let (descriptor, pubkeys) = create_threshold_descriptor(2, 3);
let (shares, plaintext, ciphertext) = get_encrypted_data(descriptor.clone());
assert_eq!(shares.len(), 3, "2-of-3 multisig should produce 3 shares");
for i in 0..3 {
for j in (i + 1)..3 {
let key_subset_vec = vec![pubkeys[i].clone(), pubkeys[j].clone()];
let decrypted_plaintext = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_vec,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_plaintext, plaintext,
"Decrypted plaintext doesn't match original"
);
}
}
for key in pubkeys {
let single_key_vec = vec![key.clone()];
let result = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
single_key_vec,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
result.is_err(),
"Decryption should fail with only 1 key for 2-of-3"
);
}
}
#[test]
fn test_multi_key_threshold_3of5() {
let (descriptor, pubkeys) = create_threshold_descriptor(3, 5);
let (shares, plaintext, ciphertext) = get_encrypted_data(descriptor.clone());
assert_eq!(shares.len(), 5, "3-of-5 multisig should produce 5 shares");
let key_subset_exact = vec![pubkeys[0].clone(), pubkeys[2].clone(), pubkeys[4].clone()];
let decrypted_exact = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_exact,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_exact, plaintext,
"Decrypted plaintext doesn't match original with exact keys"
);
let key_subset_more = vec![
pubkeys[0].clone(),
pubkeys[1].clone(),
pubkeys[2].clone(),
pubkeys[3].clone(),
];
let decrypted_more = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_more,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_more, plaintext,
"Decrypted plaintext doesn't match original with more keys"
);
let key_subset_less = vec![pubkeys[0].clone(), pubkeys[1].clone()];
let decrypted_less = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_less,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_less.is_err(),
"Decryption should fail with only 2 keys for 3-of-5"
);
}
#[test]
fn test_nested_thresholds() {
let key1 = create_test_key(1);
let key2 = create_test_key(2);
let key3 = create_test_key(3);
let key4 = create_test_key(4);
let desc_str = format!(
"wsh(thresh(2,or_d(pk({}),pk({})),s:pk({}),s:pk({})))",
key1, key2, key3, key4
);
let descriptor = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
let (shares, plaintext, ciphertext) = get_encrypted_data(descriptor.clone());
assert_eq!(shares.len(), 4, "Nested thresholds should produce 4 shares");
let key_subset1 = vec![key3.clone(), key4.clone()];
let decrypted1 = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset1,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(decrypted1, plaintext, "Mismatch for key3 + key4");
let key_subset2 = vec![key1.clone(), key3.clone()];
let decrypted2 = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset2,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(decrypted2, plaintext, "Mismatch for key1 + key3");
let key_subset2 = vec![key1.clone(), key4.clone()];
let decrypted2 = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset2,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(decrypted2, plaintext, "Mismatch for key1 + key3");
let key_subset2 = vec![key2.clone(), key3.clone()];
let decrypted2 = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset2,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(decrypted2, plaintext, "Mismatch for key1 + key3");
let key_subset_insufficient1 = vec![key3.clone()];
let decrypted_insufficient1 = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_insufficient1,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_insufficient1.is_err(),
"Decryption should fail with only key3"
);
let key_subset_insufficient2 = vec![key1.clone(), key2.clone()];
let decrypted_insufficient2 = decrypt_with_authenticated_shards(
descriptor.clone(),
shares.clone(),
key_subset_insufficient2,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_insufficient2.is_err(),
"Decryption should fail with only inner threshold keys (key1+key2)"
);
}
#[test]
fn test_reconstruction_with_incorrect_shares_content() {
let (descriptor, pubkeys) = create_threshold_descriptor(2, 3);
let (mut shares, _, ciphertext) = get_encrypted_data(descriptor.clone());
if !shares.is_empty() {
shares[0] = [2u8; 48].to_vec();
}
let key_subset = vec![pubkeys[0].clone(), pubkeys[1].clone()];
let decrypted_plaintext = decrypt_with_authenticated_shards(
descriptor.clone(),
shares,
key_subset,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_plaintext.is_err(),
"Decryption should fail when share content is corrupted leading to incorrect secret reconstruction"
);
}
#[test]
fn test_error_conditions() {
let desc_pk_keyless = Descriptor::<DescriptorPublicKey>::from_str("wsh(1)").unwrap();
let master_key = [1u8; 32];
let p_text: Data = b"Test".to_vec();
let result = encrypt_with_authenticated_shards(
desc_pk_keyless.clone(),
master_key,
NONCE_VALUE,
p_text.clone(),
);
assert!(
result.is_err(),
"Encryption should fail with a keyless descriptor"
);
let (valid_descriptor, pubkeys_valid) = create_threshold_descriptor(1, 1);
let (shares_valid, _, ciphertext_valid) = get_encrypted_data(valid_descriptor.clone());
let (desc_2_of_3, keys_2_of_3) = create_threshold_descriptor(2, 3);
let one_share: Vec<EncryptedShare> = vec![[2u8; 48].to_vec()];
let result = decrypt_with_authenticated_shards(
desc_2_of_3.clone(),
one_share,
keys_2_of_3.iter().take(2).cloned().collect(),
NONCE_VALUE,
ciphertext_valid.clone(),
);
assert!(result.is_err(), "Recovery should fail if too few shares");
let (desc_1_of_1, keys_1_of_1) = create_threshold_descriptor(1, 1);
let two_shares: Vec<EncryptedShare> = vec![[2u8; 48].to_vec(), [3u8; 48].to_vec()];
let result = decrypt_with_authenticated_shards(
desc_1_of_1.clone(),
two_shares,
keys_1_of_1,
NONCE_VALUE,
ciphertext_valid.clone(),
);
assert!(result.is_err(), "Recovery should fail if too many shares");
let wrong_ciphertext = b"Wrong encryption context".to_vec();
let result = decrypt_with_authenticated_shards(
valid_descriptor.clone(),
shares_valid.clone(),
pubkeys_valid.clone(),
NONCE_VALUE,
wrong_ciphertext,
);
assert!(
result.is_err(),
"Decryption should fail with wrong ciphertext"
);
}
#[test]
fn test_single_key_encryption_with_full_secrecy() {
let pubkey_val = create_test_key(101);
let desc_str = format!("wpkh({})", pubkey_val);
let descriptor = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
let (shares, plaintext, ciphertext) =
get_encrypted_data_with_full_secrecy(descriptor.clone());
assert_eq!(
shares.len(),
1,
"Single key descriptor should produce one share"
);
let decrypted_plaintext = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
vec![pubkey_val.clone()],
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_plaintext, plaintext,
"Decrypted plaintext doesn't match original (unauthenticated)"
);
let wrong_key_val = create_test_key(102);
let result = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
vec![wrong_key_val],
NONCE_VALUE,
ciphertext,
);
assert!(
result.is_err(),
"Decryption should fail with wrong key (unauthenticated)"
);
assert_eq!(
result.unwrap_err().downcast_ref::<Error>().unwrap(),
&Error::DecryptionFailed
);
}
#[test]
fn test_multi_key_threshold_1of2_with_full_secrecy() {
let (descriptor, pubkeys) = create_threshold_descriptor(1, 2);
let (shares, plaintext, ciphertext) =
get_encrypted_data_with_full_secrecy(descriptor.clone());
assert_eq!(
shares.len(),
2,
"1-of-2 multisig should produce 2 shares (unauthenticated)"
);
for key in pubkeys {
let key_subset_vec = vec![key.clone()];
let decrypted_plaintext = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_vec,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_plaintext, plaintext,
"Decrypted plaintext doesn't match original (unauthenticated)"
);
}
}
#[test]
fn test_multi_key_threshold_2of3_with_full_secrecy() {
let (descriptor, pubkeys) = create_threshold_descriptor(2, 3);
let (shares, plaintext, ciphertext) =
get_encrypted_data_with_full_secrecy(descriptor.clone());
assert_eq!(
shares.len(),
3,
"2-of-3 multisig should produce 3 shares (unauthenticated)"
);
for i in 0..3 {
for j in (i + 1)..3 {
let key_subset_vec = vec![pubkeys[i].clone(), pubkeys[j].clone()];
let decrypted_plaintext = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_vec,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_plaintext, plaintext,
"Decrypted plaintext doesn't match original (unauthenticated)"
);
}
}
for key in pubkeys {
let single_key_vec = vec![key.clone()];
let result = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
single_key_vec,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
result.is_err(),
"Decryption should fail with only 1 key for 2-of-3 (unauthenticated)"
);
assert_eq!(
result.unwrap_err().downcast_ref::<Error>().unwrap(),
&Error::DecryptionFailed
);
}
}
#[test]
fn test_multi_key_threshold_3of5_with_full_secrecy() {
let (descriptor, pubkeys) = create_threshold_descriptor(3, 5);
let (shares, plaintext, ciphertext) =
get_encrypted_data_with_full_secrecy(descriptor.clone());
assert_eq!(
shares.len(),
5,
"3-of-5 multisig should produce 5 shares (unauthenticated)"
);
let key_subset_exact = vec![pubkeys[0].clone(), pubkeys[2].clone(), pubkeys[4].clone()];
let decrypted_exact = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_exact,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_exact, plaintext,
"Decrypted plaintext doesn't match original with exact keys (unauthenticated)"
);
let key_subset_more = vec![
pubkeys[0].clone(),
pubkeys[1].clone(),
pubkeys[2].clone(),
pubkeys[3].clone(),
];
let decrypted_more = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_more,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted_more, plaintext,
"Decrypted plaintext doesn't match original with more keys (unauthenticated)"
);
let key_subset_less = vec![pubkeys[0].clone(), pubkeys[1].clone()];
let decrypted_less = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_less,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_less.is_err(),
"Decryption should fail with only 2 keys for 3-of-5 (unauthenticated)"
);
assert_eq!(
decrypted_less.unwrap_err().downcast_ref::<Error>().unwrap(),
&Error::DecryptionFailed
);
}
#[test]
fn test_nested_thresholds_with_full_secrecy() {
let key1 = create_test_key(201);
let key2 = create_test_key(202);
let key3 = create_test_key(203);
let key4 = create_test_key(204);
let desc_str = format!(
"wsh(thresh(2,or_d(pk({}),pk({})),s:pk({}),s:pk({})))",
key1, key2, key3, key4
);
let descriptor = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
let (shares, plaintext, ciphertext) =
get_encrypted_data_with_full_secrecy(descriptor.clone());
assert_eq!(
shares.len(),
4,
"Nested thresholds should produce 4 shares (unauthenticated)"
);
let key_subset1 = vec![key3.clone(), key4.clone()];
let decrypted1 = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset1,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted1, plaintext,
"Mismatch for key3 + key4 (unauthenticated)"
);
let key_subset2 = vec![key1.clone(), key3.clone()];
let decrypted2 = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset2,
NONCE_VALUE,
ciphertext.clone(),
)
.unwrap();
assert_eq!(
decrypted2, plaintext,
"Mismatch for key1 + key3 (unauthenticated)"
);
let key_subset_insufficient1 = vec![key3.clone()];
let decrypted_insufficient1 = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_insufficient1,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_insufficient1.is_err(),
"Decryption should fail with only key3 (unauthenticated)"
);
assert_eq!(
decrypted_insufficient1
.unwrap_err()
.downcast_ref::<Error>()
.unwrap(),
&Error::DecryptionFailed
);
let key_subset_insufficient2 = vec![key1.clone(), key2.clone()];
let decrypted_insufficient2 = decrypt_with_full_secrecy(
descriptor.clone(),
shares.clone(),
key_subset_insufficient2,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_insufficient2.is_err(),
"Decryption should fail with only inner threshold keys (unauthenticated)"
);
assert_eq!(
decrypted_insufficient2
.unwrap_err()
.downcast_ref::<Error>()
.unwrap(),
&Error::DecryptionFailed
);
}
#[test]
fn test_reconstruction_with_incorrect_shares_content_with_full_secrecy() {
let (descriptor, pubkeys) = create_threshold_descriptor(2, 3);
let (mut shares, _plaintext, ciphertext) =
get_encrypted_data_with_full_secrecy(descriptor.clone());
if !shares.is_empty() {
shares[0] = [2u8; 32].to_vec(); }
let key_subset = vec![pubkeys[0].clone(), pubkeys[1].clone()];
let decrypted_plaintext = decrypt_with_full_secrecy(
descriptor.clone(),
shares,
key_subset,
NONCE_VALUE,
ciphertext.clone(),
);
assert!(
decrypted_plaintext.is_err(),
"Decryption should fail when share content is corrupted (unauthenticated)"
);
assert_eq!(
decrypted_plaintext
.unwrap_err()
.downcast_ref::<Error>()
.unwrap(),
&Error::DecryptionFailed
);
}
#[test]
fn test_error_conditions_with_full_secrecy() {
let desc_pk_keyless = Descriptor::<DescriptorPublicKey>::from_str("wsh(1)").unwrap();
let master_key = [1u8; 32];
let p_text: Data = b"Test".to_vec();
let result = encrypt_with_full_secrecy(
desc_pk_keyless.clone(),
master_key,
NONCE_VALUE,
p_text.clone(),
);
assert!(
result.is_err(),
"Encryption should fail with a keyless descriptor"
);
let (valid_descriptor, pubkeys_valid) = create_threshold_descriptor(1, 1);
let (shares_valid, _, ciphertext_valid) =
get_encrypted_data_with_full_secrecy(valid_descriptor.clone());
let (desc_2_of_3, keys_2_of_3) = create_threshold_descriptor(2, 3);
let one_share: Vec<EncryptedShare> = vec![[2u8; 48].to_vec()];
let result = decrypt_with_full_secrecy(
desc_2_of_3.clone(),
one_share,
keys_2_of_3.iter().take(2).cloned().collect(),
NONCE_VALUE,
ciphertext_valid.clone(),
);
assert!(result.is_err(), "Recovery should fail if too few shares");
let (desc_1_of_1, keys_1_of_1) = create_threshold_descriptor(1, 1);
let two_shares: Vec<EncryptedShare> = vec![[2u8; 48].to_vec(), [3u8; 48].to_vec()];
let result = decrypt_with_full_secrecy(
desc_1_of_1.clone(),
two_shares,
keys_1_of_1,
NONCE_VALUE,
ciphertext_valid.clone(),
);
assert!(result.is_err(), "Recovery should fail if too many shares");
let wrong_ciphertext = b"Wrong encryption context".to_vec();
let result = decrypt_with_full_secrecy(
valid_descriptor.clone(),
shares_valid.clone(),
pubkeys_valid.clone(),
NONCE_VALUE,
wrong_ciphertext,
);
assert!(
result.is_err(),
"Decryption should fail with wrong ciphertext"
);
}
}