Skip to main content

kaspa_bip32/
xkey.rs

1//! Parser for extended key types (i.e. `xprv` and `xpub`)
2
3use crate::{ChildNumber, Error, ExtendedKeyAttrs, KEY_SIZE, Prefix, Result, Version};
4use core::{
5    fmt::{self, Display},
6    str::{self, FromStr},
7};
8use zeroize::Zeroize;
9
10/// Serialized extended key (e.g. `xprv` and `xpub`).
11#[derive(Clone)]
12pub struct ExtendedKey {
13    /// [`Prefix`] (a.k.a. "version") of the key (e.g. `xprv`, `xpub`)
14    pub prefix: Prefix,
15
16    /// Extended key attributes.
17    pub attrs: ExtendedKeyAttrs,
18
19    /// Key material (may be public or private).
20    ///
21    /// Includes an extra byte for a public key's SEC1 tag.
22    pub key_bytes: [u8; KEY_SIZE + 1],
23}
24
25impl ExtendedKey {
26    /// Size of an extended key when deserialized into bytes from Base58.
27    pub const BYTE_SIZE: usize = 78;
28
29    /// Maximum size of a Base58Check-encoded extended key in bytes.
30    ///
31    /// Note that extended keys can also be 111-bytes.
32    pub const MAX_BASE58_SIZE: usize = 112;
33
34    /// Write a Base58-encoded key to the provided buffer, returning a `&str`
35    /// containing the serialized data.
36    ///
37    /// Note that this type also impls [`Display`] and therefore you can
38    /// obtain an owned string by calling `to_string()`.
39    pub fn write_base58<'a>(&self, buffer: &'a mut [u8; Self::MAX_BASE58_SIZE]) -> Result<&'a str> {
40        let mut bytes = [0u8; Self::BYTE_SIZE]; // with 4-byte checksum
41        bytes[..4].copy_from_slice(&self.prefix.to_bytes());
42        bytes[4] = self.attrs.depth;
43        bytes[5..9].copy_from_slice(&self.attrs.parent_fingerprint);
44        bytes[9..13].copy_from_slice(&self.attrs.child_number.to_bytes());
45        bytes[13..45].copy_from_slice(&self.attrs.chain_code);
46        bytes[45..78].copy_from_slice(&self.key_bytes);
47
48        //println!("serialized {}", hex::encode(&bytes));
49        let base58_len = bs58::encode(&bytes).with_check().onto(buffer.as_mut())?;
50        bytes.zeroize();
51
52        //println!("base58_len: {base58_len}, hash {}", hex::encode(&buffer));
53
54        str::from_utf8(&buffer[..base58_len]).map_err(Error::Utf8Error)
55    }
56}
57
58impl Display for ExtendedKey {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        let mut buf = [0u8; Self::MAX_BASE58_SIZE];
61        self.write_base58(&mut buf).map_err(|_| fmt::Error).and_then(|base58| f.write_str(base58))
62    }
63}
64
65impl FromStr for ExtendedKey {
66    type Err = Error;
67
68    fn from_str(base58: &str) -> Result<Self> {
69        let mut bytes = [0u8; Self::BYTE_SIZE + 4]; // with 4-byte checksum
70        let decoded_len = bs58::decode(base58).with_check(None).onto(&mut bytes)?;
71
72        if decoded_len != Self::BYTE_SIZE {
73            return Err(Error::DecodeLength(decoded_len, Self::BYTE_SIZE));
74        }
75
76        let prefix = base58.get(..4).ok_or(Error::DecodeIssue).and_then(|chars| {
77            Prefix::validate_str(chars)?;
78            let b: [u8; 4] = bytes[..4].try_into()?;
79            let version = Version::from_be_bytes(b);
80            Ok(Prefix::from_parts_unchecked(chars, version))
81            //Err(Error::DecodeIssue)
82        })?;
83
84        let depth = bytes[4];
85        let parent_fingerprint = bytes[5..9].try_into()?;
86        let child_number = ChildNumber::from_bytes(bytes[9..13].try_into()?);
87        let chain_code = bytes[13..45].try_into()?;
88        let key_bytes = bytes[45..78].try_into()?;
89        bytes.zeroize();
90
91        let attrs = ExtendedKeyAttrs { depth, parent_fingerprint, child_number, chain_code };
92
93        Ok(ExtendedKey { prefix, attrs, key_bytes })
94    }
95}
96
97impl Drop for ExtendedKey {
98    fn drop(&mut self) {
99        self.key_bytes.zeroize();
100    }
101}
102
103// TODO(tarcieri): consolidate test vectors
104#[cfg(test)]
105mod tests {
106    use super::ExtendedKey;
107    use faster_hex::hex_decode_fallback;
108
109    macro_rules! hex {
110        ($str: literal) => {{
111            let len = $str.as_bytes().len() / 2;
112            let mut dst = vec![0; len];
113            dst.resize(len, 0);
114            hex_decode_fallback($str.as_bytes(), &mut dst);
115            dst
116        }
117        [..]};
118    }
119
120    #[test]
121    fn bip32_test_vector_1_xprv() {
122        //let xprv_base58 = "kprv5y2qurMHCsXYrNfU3GCihuwG3vMqFji7PZXajMEqyBkNh9UZUJgoHYBLTKu1eM4MvUtomcXPQ3Sw9HZ5ebbM4byoUciHo1zrPJBQfqpLorQ";
123        let xprv_base58 = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPP\
124            qjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi";
125
126        let xprv = xprv_base58.parse::<ExtendedKey>();
127        assert!(xprv.is_ok(), "Could not parse key");
128        let xprv = xprv.unwrap();
129        assert_eq!(xprv.prefix.as_str(), "xprv");
130        assert_eq!(xprv.attrs.depth, 0);
131        assert_eq!(xprv.attrs.parent_fingerprint, [0u8; 4]);
132        assert_eq!(xprv.attrs.child_number.0, 0);
133        assert_eq!(xprv.attrs.chain_code, hex!("873DFF81C02F525623FD1FE5167EAC3A55A049DE3D314BB42EE227FFED37D508"));
134        assert_eq!(xprv.key_bytes, hex!("00E8F32E723DECF4051AEFAC8E2C93C9C5B214313817CDB01A1494B917C8436B35"));
135        assert_eq!(&xprv.to_string(), xprv_base58);
136    }
137
138    #[test]
139    fn bip32_test_vector_1_xpub() {
140        let xpub_base58 = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhe\
141             PY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
142
143        let xpub = xpub_base58.parse::<ExtendedKey>();
144        assert!(xpub.is_ok(), "Could not parse key");
145        let xpub = xpub.unwrap();
146        assert_eq!(xpub.prefix.as_str(), "xpub");
147        assert_eq!(xpub.attrs.depth, 0);
148        assert_eq!(xpub.attrs.parent_fingerprint, [0u8; 4]);
149        assert_eq!(xpub.attrs.child_number.0, 0);
150        assert_eq!(xpub.attrs.chain_code, hex!("873DFF81C02F525623FD1FE5167EAC3A55A049DE3D314BB42EE227FFED37D508"));
151        assert_eq!(xpub.key_bytes, hex!("0339A36013301597DAEF41FBE593A02CC513D0B55527EC2DF1050E2E8FF49C85C2"));
152        assert_eq!(&xpub.to_string(), xpub_base58);
153    }
154}