From 7eab4c29bbe09f68db1cef4a5265681b0b7b0dce Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Fri, 12 Sep 2025 13:37:19 +0200 Subject: [PATCH 1/8] move content metadata to the encrypted payload --- src/lib.rs | 91 ++++--------------------- src/ll.rs | 193 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 161 insertions(+), 123 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a21b1e9..57a4e0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use descriptor::descr_to_dpks; +pub use ll::Content; use miniscript::{ bitcoin::{bip32::DerivationPath, secp256k1}, Descriptor, DescriptorPublicKey, @@ -39,7 +40,7 @@ impl ToPayload for Vec { Ok(self.clone()) } fn content_type(&self) -> Content { - Content::Undefined + Content::Unknown } fn derivation_paths(&self) -> Result, Error> { Ok(vec![]) @@ -133,7 +134,7 @@ impl EncryptedBackup { self.keys.clone() } pub fn get_content(&self) -> Content { - self.content + self.content.clone() } pub fn set_keys(mut self, keys: Vec) -> Self { self.keys = keys; @@ -186,7 +187,7 @@ impl EncryptedBackup { match (self.encryption, self.version) { (Encryption::AesGcm256, Version::V0 | Version::V1) => Ok(ll::encrypt_aes_gcm_256_v1( self.derivation_paths, - self.content.into(), + self.content.clone(), self.keys, &bytes, )?), @@ -197,16 +198,9 @@ impl EncryptedBackup { let version: Version = ll::decode_version(bytes).map(|v| v.into())?; match version { Version::V0 | Version::V1 => { - let ( - derivation_paths, - individual_secrets, - content, - encryption_type, - nonce, - cyphertext, - ) = ll::decode_v1(bytes)?; + let (derivation_paths, individual_secrets, encryption_type, nonce, cyphertext) = + ll::decode_v1(bytes)?; self.derivation_paths = derivation_paths; - self.content = content.into(); self.encryption = encryption_type.into(); self.payload = Payload::DecryptV1 { cyphertext, @@ -220,16 +214,18 @@ impl EncryptedBackup { } pub fn extract(content: Content, bytes: Vec) -> Result { match content { - Content::Undefined => Ok(Decrypted::Raw(bytes)), + Content::Unknown => Ok(Decrypted::Raw(bytes)), Content::Bip380 => { let descr_str = String::from_utf8(bytes).map_err(|_| Error::Utf8)?; let descriptor = Descriptor::::from_str(&descr_str) .map_err(|_| Error::Descriptor)?; Ok(Decrypted::Descriptor(descriptor)) } - Content::WalletBackup => Ok(Decrypted::WalletBackup(bytes)), - Content::Bip329 | Content::Bip388 => Err(Error::NotImplemented), - Content::Unknown => Err(Error::UnknownContent), + Content::None + | Content::BIP(_) + | Content::Proprietary(_) + | Content::Bip329 + | Content::Bip388 => Err(Error::NotImplemented), } } pub fn decrypt(&self) -> Result { @@ -242,13 +238,13 @@ impl EncryptedBackup { nonce, } => { for key in &self.keys { - if let Ok(bytes) = ll::decrypt_aes_gcm_256_v1( + if let Ok((content, bytes)) = ll::decrypt_aes_gcm_256_v1( *key, &individual_secrets.clone(), cyphertext.clone(), *nonce, ) { - return Self::extract(self.content, bytes); + return Self::extract(content, bytes); } } Err(Error::WrongKey) @@ -259,27 +255,6 @@ impl EncryptedBackup { } } -#[derive(Debug, Clone, Copy, FromPrimitive, IntoPrimitive, PartialEq, Eq)] -#[repr(u8)] -pub enum Content { - Undefined, - Bip380, - Bip388, - Bip329, - WalletBackup, - #[num_enum(default)] - Unknown = 0xFF, -} - -impl Content { - pub fn is_known(&self) -> bool { - match self { - Content::Undefined | Content::Unknown => false, - Content::Bip380 | Content::Bip388 | Content::Bip329 | Content::WalletBackup => true, - } - } -} - #[derive(Debug, Clone, Copy, FromPrimitive, IntoPrimitive, PartialEq, Eq)] #[repr(u8)] pub enum Encryption { @@ -347,7 +322,6 @@ mod tests { #[test] fn test_simple_encrypted_descriptor() { - let descriptor = Descriptor::::from_str(""); let descriptor = descriptor::tests::descr_1(); let backp = EncryptedBackup::new().set_payload(&descriptor).unwrap(); let keys = backp.get_keys(); @@ -361,43 +335,6 @@ mod tests { assert_eq!(restored, Decrypted::Descriptor(descriptor)); } - #[test] - fn test_content_to_u8() { - let mut u: u8 = Content::Bip380.into(); - assert_eq!(0x01, u); - u = Content::Bip388.into(); - assert_eq!(0x02, u); - u = Content::Bip329.into(); - assert_eq!(0x03, u); - u = Content::WalletBackup.into(); - assert_eq!(0x04, u); - - u = Content::Undefined.into(); - assert_eq!(0x00, u); - - u = Content::Unknown.into(); - assert_eq!(0xFF, u); - } - - #[test] - fn test_u8_to_content() { - let mut c: Content = 0x00u8.into(); - assert_eq!(c, Content::Undefined); - c = 0x01u8.into(); - assert_eq!(c, Content::Bip380); - c = 0x02u8.into(); - assert_eq!(c, Content::Bip388); - c = 0x03u8.into(); - assert_eq!(c, Content::Bip329); - c = 0x04u8.into(); - assert_eq!(c, Content::WalletBackup); - - for i in 0x05..0xFFu8 { - c = i.into(); - assert_eq!(c, Content::Unknown); - } - } - #[test] fn test_encryption_to_u8() { let mut u: u8 = Encryption::AesGcm256.into(); diff --git a/src/ll.rs b/src/ll.rs index 47b89b9..e2cc47c 100644 --- a/src/ll.rs +++ b/src/ll.rs @@ -41,11 +41,90 @@ pub enum Error { IndividualSecretsLength, CypherTextEmpty, CypherTextLength, - Content, + ContentMetadata, Encryption, OffsetOverflow, EmptyBytes, Increment, + ContentMetadataEmpty, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Content { + None, + Bip380, + Bip388, + Bip329, + BIP(u16), + Proprietary(Vec), + Unknown, +} + +/// Encode content metadata, 3 variants: +/// - => None +/// - => encoding format defined in BIP +/// - 2> => proprietary +impl From for Vec { + fn from(value: Content) -> Self { + match value { + Content::None => [0].into(), + Content::Proprietary(mut data) => { + assert!(data.len() > 2); + assert!(data.len() < u8::MAX as usize); + let mut content = vec![data.len() as u8]; + content.append(&mut data); + content + } + Content::Unknown => unimplemented!(), + c => { + let mut content = vec![2]; + let bip_number = match c { + Content::Bip380 => 380u16.to_be_bytes(), + Content::Bip388 => 388u16.to_be_bytes(), + Content::Bip329 => 329u16.to_be_bytes(), + Content::BIP(bip) => bip.to_be_bytes(), + _ => unreachable!(), + }; + content.append(&mut bip_number.to_vec()); + content + } + } + } +} + +pub fn parse_content_metadata(bytes: &[u8]) -> Result<(usize, Content), Error> { + let len = bytes.len(); + if len == 0 { + Err(Error::ContentMetadataEmpty)? + } + let data_len = bytes[0]; + match data_len { + 0 => Ok((1, Content::None)), + 1 => Err(Error::ContentMetadata), + 2 => { + let bip_number = u16::from_be_bytes(bytes[1..3].try_into().expect("len ok")); + match bip_number { + 380 => Ok((3, Content::Bip380)), + 388 => Ok((3, Content::Bip388)), + 329 => Ok((3, Content::Bip329)), + bip_number => Ok((3, Content::BIP(bip_number))), + } + } + len => { + let end = (len + 1) as usize; + let data = &bytes[1..end].to_vec(); + Ok((end, Content::Proprietary(data.to_vec()))) + } + } +} + +impl Content { + pub fn is_known(&self) -> bool { + match self { + Content::None | Content::Unknown | Content::Proprietary(_) => false, + Content::Bip380 | Content::Bip388 | Content::Bip329 | Content::BIP(_) => true, + } + } } pub fn xor(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] { @@ -143,7 +222,7 @@ pub fn encode_individual_secrets(individual_secrets: &[[u8; 32]]) -> Result +/// pub fn encode_encrypted_payload(nonce: [u8; 12], cyphertext: &[u8]) -> Result, Error> { if cyphertext.is_empty() { return Err(Error::CypherTextEmpty); @@ -158,14 +237,13 @@ pub fn encode_encrypted_payload(nonce: [u8; 12], cyphertext: &[u8]) -> Result +/// /// NOTE: payload that will fail to decode can be encoded with this function, for instance with an /// invalid version, the inputs args must be sanitized by the caller. pub fn encode_v1( version: u8, mut derivation_paths: Vec, mut individual_secrets: Vec, - content: u8, encryption: u8, mut encrypted_payload: Vec, ) -> Vec { @@ -177,8 +255,6 @@ pub fn encode_v1( out.append(&mut derivation_paths); // out.append(&mut individual_secrets); - // - out.push(content); // out.push(encryption); // @@ -241,7 +317,7 @@ pub fn decode_derivation_paths(bytes: &[u8]) -> Result, Erro } /// Expects a payload following this format: -/// <..> +/// <..> #[allow(clippy::type_complexity)] pub fn decode_v1( bytes: &[u8], @@ -249,7 +325,6 @@ pub fn decode_v1( ( Vec, /* derivation_paths */ Vec<[u8; 32]>, /* individual_secrets */ - u8, /* content */ u8, /* encryption_type */ [u8; 12], /* nonce */ Vec, /* cyphertext */ @@ -267,9 +342,6 @@ pub fn decode_v1( // let (incr, individual_secrets) = parse_individual_secrets(&bytes[offset..])?; offset = increment_offset(bytes, offset, incr)?; - // - let (incr, content) = parse_content(&bytes[offset..])?; - offset = increment_offset(bytes, offset, incr)?; // let (incr, encryption_type) = parse_encryption(&bytes[offset..])?; offset = increment_offset(bytes, offset, incr)?; @@ -279,7 +351,6 @@ pub fn decode_v1( Ok(( derivation_paths, individual_secrets, - content, encryption_type, nonce, cyphertext, @@ -288,7 +359,7 @@ pub fn decode_v1( pub fn encrypt_aes_gcm_256_v1( derivation_paths: Vec, - content: u8, + content_metadata: Content, keys: Vec, data: &[u8], ) -> Result, Error> { @@ -310,6 +381,11 @@ pub fn encrypt_aes_gcm_256_v1( return Err(Error::DataLength); } + let content_metadata: Vec = content_metadata.into(); + if content_metadata.is_empty() { + return Err(Error::ContentMetadata); + } + let mut raw_keys = keys.into_iter().map(|k| k.serialize()).collect::>(); raw_keys.sort(); @@ -318,14 +394,17 @@ pub fn encrypt_aes_gcm_256_v1( encode_individual_secrets(&individual_secrets(&secret, raw_keys.as_slice()))?; let derivation_paths = encode_derivation_paths(derivation_paths)?; - let (nonce, cyphertext) = inner_encrypt(secret, data.to_vec())?; + // = + let mut payload = content_metadata; + payload.append(&mut data.to_vec()); + + let (nonce, cyphertext) = inner_encrypt(secret, payload.to_vec())?; let encrypted_payload = encode_encrypted_payload(nonce, cyphertext.as_slice())?; Ok(encode_v1( Version::V1.into(), derivation_paths, individual_secrets, - content, Encryption::AesGcm256.into(), encrypted_payload, )) @@ -349,7 +428,7 @@ pub fn decrypt_aes_gcm_256_v1( individual_secrets: &Vec<[u8; 32]>, cyphertext: Vec, nonce: [u8; 12], -) -> Result, Error> { +) -> Result<(Content, Vec), Error> { let raw_key = key.serialize(); let mut engine = sha256::HashEngine::default(); @@ -360,7 +439,13 @@ pub fn decrypt_aes_gcm_256_v1( for ci in individual_secrets { let secret = xor(si.as_byte_array(), ci); if let Some(out) = try_decrypt_aes_gcm_256(&cyphertext, &secret, nonce) { - return Ok(out); + let mut offset = init_offset(&out, 0)?; + // + let (incr, content) = parse_content_metadata(&out)?; + // + offset = increment_offset(&out, offset, incr)?; + let out = out[offset..].to_vec(); + return Ok((content, out)); } } @@ -387,17 +472,9 @@ pub fn parse_version(bytes: &[u8]) -> Result<(usize, u8), Error> { Ok((1, version)) } -pub fn parse_content(bytes: &[u8]) -> Result<(usize, u8), Error> { - if bytes.is_empty() { - return Err(Error::Content); - } - let content = bytes[0]; - Ok((1, content)) -} - pub fn parse_encryption(bytes: &[u8]) -> Result<(usize, u8), Error> { if bytes.is_empty() { - return Err(Error::Content); + return Err(Error::ContentMetadata); } let encryption = bytes[0]; Ok((1, encryption)) @@ -607,12 +684,30 @@ mod tests { #[test] fn test_parse_content() { - let (_, c) = parse_content(&[0x00]).unwrap(); - assert_eq!(c, 0x00); - let res = parse_content(&[]); - assert_eq!(res, Err(Error::Content)); - let (_, c) = parse_content(&[0x02, 0x01]).unwrap(); - assert_eq!(c, 0x02); + // empty bytes must fail + assert!(parse_content_metadata(&[]).is_err()); + // None + let (_, c) = parse_content_metadata(&[0]).unwrap(); + assert_eq!(c, Content::None); + // len == 1 fails + assert!(parse_content_metadata(&[1, 0]).is_err()); + // BIP380 + let (_, c) = parse_content_metadata(&[2, 0x01, 0x7c]).unwrap(); + assert_eq!(c, Content::Bip380); + // BIP388 + let (_, c) = parse_content_metadata(&[2, 0x01, 0x84]).unwrap(); + assert_eq!(c, Content::Bip388); + // BIP329 + let (_, c) = parse_content_metadata(&[2, 0x01, 0x49]).unwrap(); + assert_eq!(c, Content::Bip329); + // Arbitrary BIPs + let (_, c) = parse_content_metadata(&[2, 0xFF, 0xFF]).unwrap(); + assert_eq!(c, Content::BIP(u16::MAX)); + let (_, c) = parse_content_metadata(&[2, 0, 0]).unwrap(); + assert_eq!(c, Content::BIP(0)); + // Proprietary + let (_, c) = parse_content_metadata(&[3, 0, 0, 0]).unwrap(); + assert_eq!(c, Content::Proprietary(vec![0, 0, 0])); } #[test] @@ -798,26 +893,31 @@ mod tests { 0x01, encode_derivation_paths(vec![DerivationPath::from_str("8/9").unwrap()]).unwrap(), [0x01; 33].to_vec(), - 0x00, 0x01, encode_encrypted_payload([0x04u8; 12], &[0x00]).unwrap(), ); + // let mut expected = MAGIC.as_bytes().to_vec(); + // + expected.append(&mut vec![0x01]); + // expected.append(&mut vec![ - 0x01, 0x01, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, + 0x01, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, ]); + // expected.append(&mut [0x01; 33].to_vec()); - expected.append(&mut vec![0x00, 0x01]); + // + expected.append(&mut vec![0x01]); + // expected.append(&mut encode_encrypted_payload([0x04u8; 12], &[0x00]).unwrap()); assert_eq!(bytes, expected); let version = decode_version(&bytes).unwrap(); assert_eq!(version, 0x01); let derivs = decode_derivation_paths(&bytes).unwrap(); assert_eq!(derivs, vec![DerivationPath::from_str("8/9").unwrap()]); - let (derivs, secrets, content, encryption, nonce, cyphertext) = decode_v1(&bytes).unwrap(); + let (derivs, secrets, encryption, nonce, cyphertext) = decode_v1(&bytes).unwrap(); assert_eq!(derivs, vec![DerivationPath::from_str("8/9").unwrap()]); assert_eq!(secrets, vec![[0x01; 32]]); - assert_eq!(content, 0x00); assert_eq!(encryption, 0x01); assert_eq!(nonce, [0x04u8; 12]); assert_eq!(cyphertext, vec![0x00]); @@ -828,17 +928,17 @@ mod tests { // Empty keyvector must fail let keys = vec![]; let data = "test".as_bytes().to_vec(); - let res = encrypt_aes_gcm_256_v1(vec![], 0x00, keys, &data); + let res = encrypt_aes_gcm_256_v1(vec![], Content::Bip380, keys, &data); assert_eq!(res, Err(Error::KeyCount)); // > 255 keys must fail let keys = [pk1(); 256].to_vec(); - let res = encrypt_aes_gcm_256_v1(vec![], 0x00, keys, &data); + let res = encrypt_aes_gcm_256_v1(vec![], Content::Bip380, keys, &data); assert_eq!(res, Err(Error::KeyCount)); // Empty payload must fail let keys = [pk1()].to_vec(); - let res = encrypt_aes_gcm_256_v1(vec![], 0x00, keys, &[]); + let res = encrypt_aes_gcm_256_v1(vec![], Content::Bip380, keys, &[]); assert_eq!(res, Err(Error::DataLength)); // > 255 deriv path must fail @@ -847,7 +947,7 @@ mod tests { for _ in 0..256 { deriv_paths.push(DerivationPath::from_str("0/0").unwrap()); } - let res = encrypt_aes_gcm_256_v1(deriv_paths, 0x00, keys, &data); + let res = encrypt_aes_gcm_256_v1(deriv_paths, Content::Bip380, keys, &data); assert_eq!(res, Err(Error::DerivPathCount)); } @@ -855,7 +955,7 @@ mod tests { fn test_basic_encrypt_decrypt() { let keys = vec![pk2(), pk1()]; let data = "test".as_bytes().to_vec(); - let bytes = encrypt_aes_gcm_256_v1(vec![], 0x00, keys, &data).unwrap(); + let bytes = encrypt_aes_gcm_256_v1(vec![], Content::None, keys, &data).unwrap(); let version = decode_version(&bytes).unwrap(); assert_eq!(version, 1); @@ -863,16 +963,17 @@ mod tests { let deriv_paths = decode_derivation_paths(&bytes).unwrap(); assert!(deriv_paths.is_empty()); - let (_, individual_secrets, content, encryption_type, nonce, cyphertext) = + let (_, individual_secrets, encryption_type, nonce, cyphertext) = decode_v1(&bytes).unwrap(); - assert_eq!(content, 0x00); assert_eq!(encryption_type, 0x01); - let decrypted_1 = + let (content, decrypted_1) = decrypt_aes_gcm_256_v1(pk1(), &individual_secrets, cyphertext.clone(), nonce).unwrap(); + assert_eq!(content, Content::None); assert_eq!(String::from_utf8(decrypted_1).unwrap(), "test".to_string()); - let decrypted_2 = + let (content, decrypted_2) = decrypt_aes_gcm_256_v1(pk2(), &individual_secrets, cyphertext.clone(), nonce).unwrap(); + assert_eq!(content, Content::None); assert_eq!(String::from_utf8(decrypted_2).unwrap(), "test".to_string()); let decrypted_3 = decrypt_aes_gcm_256_v1(pk3(), &individual_secrets, cyphertext.clone(), nonce); From 4d731632420bd7b474b03dcdf52afbb07ca677c5 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 15 Sep 2025 14:04:09 +0200 Subject: [PATCH 2/8] ll: add more tests --- src/ll.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/src/ll.rs b/src/ll.rs index e2cc47c..f741730 100644 --- a/src/ll.rs +++ b/src/ll.rs @@ -474,7 +474,7 @@ pub fn parse_version(bytes: &[u8]) -> Result<(usize, u8), Error> { pub fn parse_encryption(bytes: &[u8]) -> Result<(usize, u8), Error> { if bytes.is_empty() { - return Err(Error::ContentMetadata); + return Err(Error::Encryption); } let encryption = bytes[0]; Ok((1, encryption)) @@ -542,10 +542,10 @@ pub fn parse_individual_secrets( } // let count = bytes[0]; - let mut offset = init_offset(bytes, 1)?; if count < 1 { return Err(Error::IndividualSecretsEmpty); } + let mut offset = init_offset(bytes, 1)?; let mut individual_secrets = BTreeSet::new(); for _ in 0..count { @@ -682,6 +682,68 @@ mod tests { assert_eq!(res, Err(Error::Version)); } + #[test] + pub fn test_parse_encryption() { + let (l, e) = parse_encryption(&[0]).unwrap(); + assert_eq!(l, 1); + assert_eq!(e, 0); + let (l, e) = parse_encryption(&[0, 2]).unwrap(); + assert_eq!(l, 1); + assert_eq!(e, 0); + let (l, e) = parse_encryption(&[2, 0]).unwrap(); + assert_eq!(l, 1); + assert_eq!(e, 2); + let failed = parse_encryption(&[]).unwrap_err(); + assert_eq!(failed, Error::Encryption) + } + + #[test] + pub fn test_parse_derivation_path() { + // single deriv path + let (_, p) = parse_derivation_paths(&[0x01, 0x01, 0x00, 0x00, 0x00, 0x01]).unwrap(); + assert_eq!(p.len(), 1); + + // child number must be encoded on 4 bytes + let p = parse_derivation_paths(&[0x01, 0x01, 0x00]).unwrap_err(); + assert_eq!(p, Error::Corrupted); + let p = parse_derivation_paths(&[0x01, 0x01, 0x00, 0x00]).unwrap_err(); + assert_eq!(p, Error::Corrupted); + let p = parse_derivation_paths(&[0x01, 0x01, 0x00, 0x00, 0x00]).unwrap_err(); + assert_eq!(p, Error::Corrupted); + + // empty childs + let p = parse_derivation_paths(&[0x01, 0x00]).unwrap_err(); + assert_eq!(p, Error::DerivPathEmpty); + } + + #[test] + pub fn test_parse_individual_secrets() { + // empty bytes + let fail = parse_individual_secrets(&[]).unwrap_err(); + assert_eq!(fail, Error::EmptyBytes); + + // empty vector + let fail = parse_individual_secrets(&[0x00]).unwrap_err(); + assert_eq!(fail, Error::IndividualSecretsEmpty); + + let is1 = [1u8; 32].to_vec(); + let is2 = [2u8; 32].to_vec(); + + // single secret + let mut bytes = vec![0x01]; + bytes.append(&mut is1.clone()); + let (_, is) = parse_individual_secrets(&bytes).unwrap(); + assert_eq!(is[0].to_vec(), is1); + + // multiple secrets + let mut bytes = vec![0x02]; + bytes.append(&mut is1.clone()); + bytes.append(&mut is2.clone()); + let (_, is) = parse_individual_secrets(&bytes).unwrap(); + assert_eq!(is[0].to_vec(), is1); + assert_eq!(is[1].to_vec(), is2); + } + #[test] fn test_parse_content() { // empty bytes must fail @@ -710,6 +772,53 @@ mod tests { assert_eq!(c, Content::Proprietary(vec![0, 0, 0])); } + #[test] + fn test_serialize_content() { + // Proprietary + let mut c = Content::Proprietary(vec![0, 0, 0]); + let mut serialized: Vec = c.into(); + assert_eq!(serialized, vec![3, 0, 0, 0]); + // BIP 380 + c = Content::Bip380; + serialized = c.into(); + assert_eq!(serialized, vec![0x02, 0x01, 0x7C]); + c = Content::BIP(380); + serialized = c.into(); + assert_eq!(serialized, vec![0x02, 0x01, 0x7C]); + // BIP 388 + c = Content::Bip388; + serialized = c.into(); + assert_eq!(serialized, vec![0x02, 0x01, 0x84]); + c = Content::BIP(388); + serialized = c.into(); + assert_eq!(serialized, vec![0x02, 0x01, 0x84]); + // BIP 329 + c = Content::Bip329; + serialized = c.into(); + assert_eq!(serialized, vec![0x02, 0x01, 0x49]); + c = Content::BIP(329); + serialized = c.into(); + assert_eq!(serialized, vec![0x02, 0x01, 0x49]); + } + + #[test] + fn test_content_is_known() { + let mut c = Content::None; + assert!(!c.is_known()); + c = Content::Unknown; + assert!(!c.is_known()); + c = Content::Proprietary(vec![0, 0, 0]); + assert!(!c.is_known()); + c = Content::Bip380; + assert!(c.is_known()); + c = Content::Bip388; + assert!(c.is_known()); + c = Content::Bip329; + assert!(c.is_known()); + c = Content::BIP(0); + assert!(c.is_known()); + } + #[test] fn test_simple_encode_decode_encrypted_payload() { let bytes = encode_encrypted_payload([3; 12], &[1, 2, 3, 4]).unwrap(); From b8f1f85915ab211a18caf61e2ba6ff058521962d Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 15 Sep 2025 15:52:59 +0200 Subject: [PATCH 3/8] descriptor: sort out BIP341 NUMS keys --- src/descriptor.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/descriptor.rs b/src/descriptor.rs index 82f495a..e9253b5 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -4,6 +4,7 @@ pub use mscript_12_0 as miniscript; pub use mscript_12_3_5 as miniscript; use std::collections::{BTreeSet, HashSet}; +use std::str::FromStr; use miniscript::{ bitcoin::{self, bip32::DerivationPath, secp256k1}, @@ -33,12 +34,28 @@ fn dpk_to_deriv_path(key: &DescriptorPublicKey) -> Option { } } +// See +// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs: +// > One example of such a point is H = +// > lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) which is constructed +// > by taking the hash of the standard uncompressed encoding of the secp256k1 base point G as X +// > coordinate. +fn bip341_nums() -> bitcoin::secp256k1::PublicKey { + bitcoin::secp256k1::PublicKey::from_str( + "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + ) + .expect("Valid pubkey: NUMS from BIP341") +} + pub fn descr_to_dpks( descriptor: &Descriptor, ) -> Result, Error> { let mut keys = BTreeSet::new(); descriptor.for_each_key(|k| { - keys.insert(k.clone()); + let pk = dpk_to_pk(k); + if pk != bip341_nums() { + keys.insert(k.clone()); + } true }); let keys: Vec<_> = keys.into_iter().collect(); @@ -127,6 +144,24 @@ pub mod tests { assert_eq!(dpks, expected); } + #[test] + fn test_descriptor_to_dpk_unspendable() { + let descr_str = "tr(tpubD6NzVbkrYhZ4XWBqjZ7DTB4eFvi8eQZ79UvNbQFsxXiaMNaBn83jpMWTXLX2Gx6JgC5n9jWvx6vnijcAUgxXmRtFd4ntasRGNsYSCvQteSr/<0;1>/*,{and_v(v:and_v(v:pk([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<2;3>/*),pk([79af2d8a/48'/1'/0'/2']tpubDEtHs6m9crfv1oeETj6EXteAtW7eoSSBVBaypEdWZt8VftbHF9R12xSZpzWGNuAofeGPL6cz48dLdCYbVioHL8ygA56yuPW76Xz5WZ3dt8o/<2;3>/*)),older(52596)),and_v(v:pk([d4ab66f1/48'/1'/0'/2']tpubDEXYN145WM4rVKtcWpySBYiVQ229pmrnyAGJT14BBh2QJr7ABJswchDicZfFaauLyXhDad1nCoCZQEwAW87JPotP93ykC9WJvoASnBjYBxW/<0;1>/*),pk([79af2d8a/48'/1'/0'/2']tpubDEtHs6m9crfv1oeETj6EXteAtW7eoSSBVBaypEdWZt8VftbHF9R12xSZpzWGNuAofeGPL6cz48dLdCYbVioHL8ygA56yuPW76Xz5WZ3dt8o/<0;1>/*))})#vudj49fm"; + let descriptor = Descriptor::::from_str(descr_str).unwrap(); + // unspendable keys must have been dropped + let keys = descr_to_dpks(&descriptor).unwrap(); + for key in keys { + let pk = dpk_to_pk(&key); + assert_ne!(pk, bip341_nums()); + } + // but the descriptor contains unspendable + let contains_unspendable = descriptor.for_any_key(|k| { + let pk = dpk_to_pk(k); + pk == bip341_nums() + }); + assert!(contains_unspendable); + } + #[test] fn test_dpks_to_deriv_paths() { let dpks = vec![dpk_1(), dpk_2()]; From f6808039c7a456772c7ec0b71b8bd39f86090c40 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 15 Sep 2025 18:56:15 +0200 Subject: [PATCH 4/8] lib: make miniscript_latest default feature --- Cargo.toml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e3af52f..0decfd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ path = "src/bin/main.rs" required-features = ["cli"] [features] +default = ["miniscript_latest"] miniscript_latest = ["miniscript_12_3_5"] miniscript_12_3_5 = ["mscript_12_3_5"] miniscript_12_0 = ["mscript_12_0"] diff --git a/README.md b/README.md index ec72310..9048a84 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ let descriptor = EncryptedBackup::new() |---------------------|---------|-------------------------------------------------------| | `miniscript_12_0` | – | Compile against `miniscript` v0.12.0 | | `miniscript_12_3_5` | – | Compile against `miniscript` v0.12.3.5 | -| `miniscript_latest` | – | Alias for `miniscript_12_3_5` | +| `miniscript_latest` | ✓ | Alias for `miniscript_12_3_5` | | `devices` | ✓ | Enable automatic enumeration of signing devices. | | `tokio` | ✓ | Pull in `tokio` runtime used by the `devices`feature. | From 79532bb75cc6ecdf2a309f571797be473f6edd44 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Mon, 15 Sep 2025 20:17:13 +0200 Subject: [PATCH 5/8] descriptor: more tests --- src/descriptor.rs | 118 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/src/descriptor.rs b/src/descriptor.rs index e9253b5..ede3a3b 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -88,7 +88,14 @@ pub mod tests { use super::*; use std::str::FromStr; - use miniscript::{Descriptor, DescriptorPublicKey}; + use miniscript::{ + bitcoin::bip32::{self, ChainCode, ChildNumber, Fingerprint}, + descriptor::{ + self, DerivPaths, DescriptorMultiXKey, DescriptorXKey, SinglePub, SinglePubKey, + Wildcard, + }, + Descriptor, DescriptorPublicKey, ToPublicKey, + }; pub fn descr_1() -> Descriptor { let descr_str = "wsh(or_d(pk([58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<0;1>/*),and_v(v:pkh([58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<2;3>/*),older(52596))))#pggrcdd0"; @@ -125,6 +132,53 @@ pub mod tests { assert_eq!(pk, expected); let pk = dpk_to_pk(&dpk_2()); assert_eq!(pk, expected); + + // Single + let single_str = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; + let dpk = DescriptorPublicKey::from_str(single_str).unwrap(); + let pk = dpk_to_pk(&dpk); + let expected = bitcoin::secp256k1::PublicKey::from_str(single_str).unwrap(); + assert_eq!(expected, pk); + + // Single Xonly + let xonly = bitcoin::PublicKey::from_str(single_str) + .unwrap() + .to_x_only_pubkey(); + let dpk = DescriptorPublicKey::Single(SinglePub { + origin: None, + key: descriptor::SinglePubKey::XOnly(xonly), + }); + let pk = dpk_to_pk(&dpk); + assert_eq!(expected, pk); + + // Xpub + let xpub = bip32::Xpub { + network: bitcoin::NetworkKind::Test, + depth: 1, + parent_fingerprint: Fingerprint::from_str("00000000").unwrap(), + child_number: ChildNumber::from_normal_idx(0).unwrap(), + public_key: bitcoin::secp256k1::PublicKey::from_str(single_str).unwrap(), + chain_code: ChainCode::from(&[1u8; 32]), + }; + let dpk = DescriptorPublicKey::XPub(DescriptorXKey { + origin: None, + xkey: xpub, + derivation_path: DerivationPath::default(), + wildcard: Wildcard::None, + }); + let pk = dpk_to_pk(&dpk); + assert_eq!(expected, pk); + + // MultiXpub + let dpk = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { + origin: None, + xkey: xpub, + derivation_paths: DerivPaths::new(vec![DerivationPath::from_str("0").unwrap()]) + .unwrap(), + wildcard: Wildcard::None, + }); + let pk = dpk_to_pk(&dpk); + assert_eq!(expected, pk); } #[test] @@ -135,6 +189,68 @@ pub mod tests { assert_eq!(deriv_2, DerivationPath::from_str("48'/1'/0'/2'").unwrap()); let deriv_3 = dpk_to_deriv_path(&dpk_3()); assert!(deriv_3.is_none()); + + let dp = DerivationPath::from_str("0/0").unwrap(); + let origin = Some((Fingerprint::from_str("aabbccdd").unwrap(), dp.clone())); + + // Single + let single_str = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; + let dpk = DescriptorPublicKey::from_str(single_str).unwrap(); + let none = dpk_to_deriv_path(&dpk); + assert!(none.is_none()); + let single_pk = SinglePubKey::FullKey(dpk_to_pk(&dpk).into()); + let dpk = DescriptorPublicKey::Single(SinglePub { + origin: origin.clone(), + key: single_pk, + }); + let deriv = dpk_to_deriv_path(&dpk).unwrap(); + assert_eq!(deriv, dp); + + // Xpub + let xpub = bip32::Xpub { + network: bitcoin::NetworkKind::Test, + depth: 1, + parent_fingerprint: Fingerprint::from_str("00000000").unwrap(), + child_number: ChildNumber::from_normal_idx(0).unwrap(), + public_key: bitcoin::secp256k1::PublicKey::from_str(single_str).unwrap(), + chain_code: ChainCode::from(&[1u8; 32]), + }; + let dpk = DescriptorPublicKey::XPub(DescriptorXKey { + origin: None, + xkey: xpub, + derivation_path: DerivationPath::default(), + wildcard: Wildcard::None, + }); + let none = dpk_to_deriv_path(&dpk); + assert!(none.is_none()); + let dpk = DescriptorPublicKey::XPub(DescriptorXKey { + origin: origin.clone(), + xkey: xpub, + derivation_path: DerivationPath::default(), + wildcard: Wildcard::None, + }); + let deriv = dpk_to_deriv_path(&dpk).unwrap(); + assert_eq!(deriv, dp); + + // MultiXpub + let dpk = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { + origin: None, + xkey: xpub, + derivation_paths: DerivPaths::new(vec![DerivationPath::from_str("0").unwrap()]) + .unwrap(), + wildcard: Wildcard::None, + }); + let none = dpk_to_deriv_path(&dpk); + assert!(none.is_none()); + let dpk = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { + origin: origin.clone(), + xkey: xpub, + derivation_paths: DerivPaths::new(vec![DerivationPath::from_str("0").unwrap()]) + .unwrap(), + wildcard: Wildcard::None, + }); + let deriv = dpk_to_deriv_path(&dpk).unwrap(); + assert_eq!(deriv, dp); } #[test] From 3319da34dc62f98f10bc10d47abd034b45cda88e Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 16 Sep 2025 10:02:33 +0200 Subject: [PATCH 6/8] .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 96ef6c0..0367919 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /target +/coverage +*.profdata +*.profraw Cargo.lock From 8c5723da865625e79c39c02c7882a0aa382924f1 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Tue, 16 Sep 2025 10:03:10 +0200 Subject: [PATCH 7/8] lib: more tests --- src/descriptor.rs | 2 +- src/lib.rs | 167 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 159 insertions(+), 10 deletions(-) diff --git a/src/descriptor.rs b/src/descriptor.rs index ede3a3b..9b26e11 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -103,7 +103,7 @@ pub mod tests { Descriptor::::from_str(descr_str).unwrap() } - fn dpk_1() -> DescriptorPublicKey { + pub fn dpk_1() -> DescriptorPublicKey { let dpk_str = "[58b7f8dc/48'/1'/0'/2']tpubDEPBvXvhta3pjVaKokqC3eeMQnszj9ehFaA2zD5nSdkaccwGAizu8jVB2NeSpvmP2P52MBoZvNCixqXRJnTyXx51FQzARR63tjxQSyP3Btw/<0;1>/*"; DescriptorPublicKey::from_str(dpk_str).unwrap() } diff --git a/src/lib.rs b/src/lib.rs index 57a4e0e..7d2e28f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,7 @@ pub enum Decrypted { Raw(Vec), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Payload { None, Encrypt { @@ -100,7 +100,7 @@ impl Payload { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EncryptedBackup { version: Version, content: Content, @@ -136,6 +136,12 @@ impl EncryptedBackup { pub fn get_content(&self) -> Content { self.content.clone() } + pub fn get_version(&self) -> Version { + self.version + } + pub fn get_encryption(&self) -> Encryption { + self.encryption + } pub fn set_keys(mut self, keys: Vec) -> Self { self.keys = keys; self @@ -214,21 +220,22 @@ impl EncryptedBackup { } pub fn extract(content: Content, bytes: Vec) -> Result { match content { - Content::Unknown => Ok(Decrypted::Raw(bytes)), + Content::None | Content::Unknown => Ok(Decrypted::Raw(bytes)), Content::Bip380 => { let descr_str = String::from_utf8(bytes).map_err(|_| Error::Utf8)?; let descriptor = Descriptor::::from_str(&descr_str) .map_err(|_| Error::Descriptor)?; Ok(Decrypted::Descriptor(descriptor)) } - Content::None - | Content::BIP(_) - | Content::Proprietary(_) - | Content::Bip329 - | Content::Bip388 => Err(Error::NotImplemented), + Content::BIP(_) | Content::Proprietary(_) | Content::Bip329 | Content::Bip388 => { + Err(Error::NotImplemented) + } } } pub fn decrypt(&self) -> Result { + if self.keys.is_empty() { + return Err(Error::NoKey); + } match self.version { Version::V0 | Version::V1 => match &self.payload { Payload::None | Payload::Encrypt { .. } => Err(Error::WrongPayload), @@ -294,7 +301,7 @@ impl Version { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { Ll(ll::Error), Utf8, @@ -305,6 +312,7 @@ pub enum Error { InvalidVersion, WrongPayload, UnknownVersion, + NoKey, WrongKey, DescriptorHasNoKeys, String(Box), @@ -318,6 +326,10 @@ impl From for Error { #[cfg(test)] mod tests { + use miniscript::bitcoin; + + use crate::descriptor::dpk_to_pk; + use super::*; #[test] @@ -335,6 +347,143 @@ mod tests { assert_eq!(restored, Decrypted::Descriptor(descriptor)); } + #[test] + fn test_encrypt_bytes() { + let payload = vec![0x00u8, 0x00, 0x00]; + let mut backp = EncryptedBackup::new().set_payload(&payload).unwrap(); + assert!(!backp.payload.is_none()); + + assert!(backp.get_keys().is_empty()); + let pk1 = dpk_to_pk(&descriptor::tests::dpk_1()); + backp = backp.set_keys(vec![pk1]); + let pks = backp.get_keys(); + assert_eq!(pks.len(), 1); + assert_eq!(*pks.first().unwrap(), pk1); + + assert!(backp.get_derivation_paths().is_empty()); + let deriv = DerivationPath::from_str("0/0").unwrap(); + backp = backp.set_derivation_paths(vec![deriv.clone()]); + assert_eq!(backp.get_derivation_paths(), vec![deriv]); + + assert_eq!(backp.get_content(), Content::Unknown); + let fail = backp.clone().encrypt().unwrap_err(); + assert_eq!(fail, Error::UnknownContent); + backp = backp.set_content_type(Content::None); + assert_eq!(backp.get_content(), Content::None); + + assert_eq!(backp.get_encryption(), Encryption::AesGcm256); + backp = backp.set_encryption(Encryption::Undefined); + assert_eq!(backp.get_encryption(), Encryption::Undefined); + let fail = backp.clone().encrypt().unwrap_err(); + assert_eq!(fail, Error::EncryptionUndefined); + backp = backp.set_encryption(Encryption::AesGcm256); + assert_eq!(backp.get_encryption(), Encryption::AesGcm256); + + backp = backp.set_version(Version::Unknown); + let fail = backp.clone().encrypt().unwrap_err(); + assert_eq!(fail, Error::InvalidVersion); + backp = backp.set_version(Version::V0); + assert_eq!(backp.get_version(), Version::V0); + backp = backp.set_version(Version::V1); + assert_eq!(backp.get_version(), Version::V1); + + let bytes = backp.encrypt().unwrap(); + + let fail = EncryptedBackup::new() + .set_encrypted_payload(&bytes) + .unwrap() + .decrypt() + .unwrap_err(); + assert_eq!(fail, Error::NoKey); + + let w_key = bitcoin::secp256k1::PublicKey::from_slice(&[ + 4, 54, 57, 149, 239, 162, 148, 175, 246, 254, 239, 75, 154, 152, 10, 82, 234, 224, 85, + 220, 40, 100, 57, 121, 30, 162, 94, 156, 135, 67, 74, 49, 179, 57, 236, 53, 162, 124, + 149, 144, 168, 77, 74, 30, 72, 211, 229, 110, 111, 55, 96, 193, 86, 227, 183, 152, 195, + 155, 51, 247, 123, 113, 60, 228, 188, + ]) + .unwrap(); + let fail = EncryptedBackup::new() + .set_encrypted_payload(&bytes) + .unwrap() + .set_keys(vec![w_key]) + .decrypt() + .unwrap_err(); + assert_eq!(fail, Error::WrongKey); + + let restored = EncryptedBackup::new() + .set_encrypted_payload(&bytes) + .unwrap() + .set_keys(vec![pk1]) + .decrypt() + .unwrap(); + assert_eq!(restored, Decrypted::Raw(vec![0x00u8, 0x00, 0x00])); + } + + pub fn dummy_encrypted_payload() -> Vec { + let key = dpk_to_pk(&descriptor::tests::dpk_1()); + EncryptedBackup::new() + .set_payload(&vec![0x00]) + .unwrap() + .set_keys(vec![key]) + .set_content_type(Content::None) + .encrypt() + .unwrap() + } + + #[test] + fn test_encrypt_wrong_payload() { + // No payload + let fail = EncryptedBackup::new() + .set_content_type(Content::None) + .encrypt() + .unwrap_err(); + assert_eq!(fail, Error::WrongPayload); + + let dummy_payload = dummy_encrypted_payload(); + + // wrong payload + let fail = EncryptedBackup::new() + .set_encrypted_payload(&dummy_payload) + .unwrap() + .set_content_type(Content::None) + .encrypt() + .unwrap_err(); + assert_eq!(fail, Error::WrongPayload); + } + + #[test] + fn test_decrypt_wrong_payload() { + let key = dpk_to_pk(&descriptor::tests::dpk_1()); + // No payload + let fail = EncryptedBackup::new() + .set_keys(vec![key]) + .decrypt() + .unwrap_err(); + assert_eq!(fail, Error::WrongPayload); + + // wrong payload + let fail = EncryptedBackup::new() + .set_keys(vec![key]) + .set_payload(&vec![0x00]) + .unwrap() + .decrypt() + .unwrap_err(); + assert_eq!(fail, Error::WrongPayload); + + let dummy = dummy_encrypted_payload(); + + // unknown version + let fail = EncryptedBackup::new() + .set_keys(vec![key]) + .set_encrypted_payload(&dummy) + .unwrap() + .set_version(Version::Unknown) + .decrypt() + .unwrap_err(); + assert_eq!(fail, Error::UnknownVersion); + } + #[test] fn test_encryption_to_u8() { let mut u: u8 = Encryption::AesGcm256.into(); From 970207078d41ed52027dc5afbf48014d1bec9d59 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Wed, 17 Sep 2025 11:23:27 +0200 Subject: [PATCH 8/8] ll: drop duplicate keys & derivation paths in encrypt_aes_gcm_256_v1() --- src/descriptor.rs | 2 +- src/ll.rs | 42 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/descriptor.rs b/src/descriptor.rs index 9b26e11..8bfb5c8 100644 --- a/src/descriptor.rs +++ b/src/descriptor.rs @@ -40,7 +40,7 @@ fn dpk_to_deriv_path(key: &DescriptorPublicKey) -> Option { // > lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) which is constructed // > by taking the hash of the standard uncompressed encoding of the secp256k1 base point G as X // > coordinate. -fn bip341_nums() -> bitcoin::secp256k1::PublicKey { +pub fn bip341_nums() -> bitcoin::secp256k1::PublicKey { bitcoin::secp256k1::PublicKey::from_str( "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", ) diff --git a/src/ll.rs b/src/ll.rs index f741730..13b8598 100644 --- a/src/ll.rs +++ b/src/ll.rs @@ -17,7 +17,7 @@ use miniscript::bitcoin::{ }; use rand::{rngs::OsRng, TryRngCore}; -use crate::{Encryption, Version}; +use crate::{descriptor::bip341_nums, Encryption, Version}; const DECRYPTION_SECRET: &str = "BEB_BACKUP_DECRYPTION_SECRET"; const INDIVIDUAL_SECRET: &str = "BEB_BACKUP_INDIVIDUAL_SECRET"; @@ -363,7 +363,19 @@ pub fn encrypt_aes_gcm_256_v1( keys: Vec, data: &[u8], ) -> Result, Error> { - // TODO: drop duplictaes in derivation_paths & keys + // drop duplicates keys and sort out bip341 nums + let keys = keys + .into_iter() + .filter(|k| *k != bip341_nums()) + .collect::>(); + + // drop duplicates derivation paths + let derivation_paths = derivation_paths + .into_iter() + .collect::>() + .into_iter() + .collect::>(); + if keys.len() > u8::MAX as usize || keys.is_empty() { return Err(Error::KeyCount); } @@ -588,6 +600,8 @@ pub fn parse_encrypted_payload( #[cfg(test)] mod tests { use aes_gcm::aead::{rand_core::RngCore, OsRng}; + use miniscript::bitcoin::XOnlyPublicKey; + use rand::random; use super::*; use std::str::FromStr; @@ -1041,7 +1055,18 @@ mod tests { assert_eq!(res, Err(Error::KeyCount)); // > 255 keys must fail - let keys = [pk1(); 256].to_vec(); + let mut keys = BTreeSet::new(); + while keys.len() < 256 { + let key: [u8; 32] = random(); + if let Ok(k) = XOnlyPublicKey::from_slice(&key) { + let k = bitcoin::secp256k1::PublicKey::from_x_only_public_key( + k, + secp256k1::Parity::Odd, + ); + keys.insert(k); + } + } + let keys = keys.into_iter().collect::>(); let res = encrypt_aes_gcm_256_v1(vec![], Content::Bip380, keys, &data); assert_eq!(res, Err(Error::KeyCount)); @@ -1052,10 +1077,15 @@ mod tests { // > 255 deriv path must fail let keys = [pk1()].to_vec(); - let mut deriv_paths = vec![]; - for _ in 0..256 { - deriv_paths.push(DerivationPath::from_str("0/0").unwrap()); + let mut deriv_paths = BTreeSet::new(); + while deriv_paths.len() < 256 { + let raw_deriv: [u32; 4] = random(); + let childs: Vec = + raw_deriv.iter().copied().map(ChildNumber::from).collect(); + let deriv: DerivationPath = childs.into(); + deriv_paths.insert(deriv); } + let deriv_paths = deriv_paths.into_iter().collect(); let res = encrypt_aes_gcm_256_v1(deriv_paths, Content::Bip380, keys, &data); assert_eq!(res, Err(Error::DerivPathCount)); }