penumbra_sdk_keys/keys/
spend.rs1use bip32::XPrv;
2use std::convert::TryFrom;
3
4use hmac::Hmac;
5use pbkdf2::pbkdf2;
6use penumbra_sdk_proto::{penumbra::core::keys::v1 as pb, DomainType};
7use serde::{Deserialize, Serialize};
8
9use super::{
10 bip44::Bip44Path,
11 seed_phrase::{SeedPhrase, NUM_PBKDF2_ROUNDS},
12 FullViewingKey, IncomingViewingKey, NullifierKey, OutgoingViewingKey,
13};
14use crate::{
15 prf,
16 rdsa::{SigningKey, SpendAuth},
17};
18
19pub const SPENDKEY_LEN_BYTES: usize = 32;
20
21#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
28pub struct SpendKeyBytes(pub [u8; SPENDKEY_LEN_BYTES]);
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(try_from = "pb::SpendKey", into = "pb::SpendKey")]
33pub struct SpendKey {
34 seed: SpendKeyBytes,
35 ask: SigningKey<SpendAuth>,
36 fvk: FullViewingKey,
37}
38
39impl PartialEq for SpendKey {
40 fn eq(&self, other: &Self) -> bool {
41 self.seed == other.seed
42 }
43}
44
45impl Eq for SpendKey {}
46
47impl DomainType for SpendKey {
48 type Proto = pb::SpendKey;
49}
50
51impl TryFrom<pb::SpendKey> for SpendKey {
52 type Error = anyhow::Error;
53
54 fn try_from(msg: pb::SpendKey) -> Result<Self, Self::Error> {
55 Ok(SpendKeyBytes::try_from(msg.inner.as_slice())?.into())
56 }
57}
58
59impl From<SpendKey> for pb::SpendKey {
60 fn from(msg: SpendKey) -> Self {
61 Self {
62 inner: msg.to_bytes().0.to_vec(),
63 }
64 }
65}
66
67impl From<SpendKeyBytes> for SpendKey {
68 fn from(seed: SpendKeyBytes) -> Self {
69 let ask = SigningKey::new_from_field(prf::expand_ff(b"Penumbra_ExpndSd", &seed.0, &[0; 1]));
70 let nk = NullifierKey(prf::expand_ff(b"Penumbra_ExpndSd", &seed.0, &[1; 1]));
71 let fvk = FullViewingKey::from_components(ask.into(), nk);
72
73 Self { seed, ask, fvk }
74 }
75}
76
77impl SpendKey {
78 pub fn to_bytes(&self) -> SpendKeyBytes {
82 self.seed.clone()
83 }
84
85 pub fn from_seed_phrase_bip39(seed_phrase: SeedPhrase, index: u64) -> Self {
94 let password = format!("{seed_phrase}");
95 let salt = format!("mnemonic{index}");
96 let mut spend_seed_bytes = [0u8; 32];
97 pbkdf2::<Hmac<sha2::Sha512>>(
98 password.as_bytes(),
99 salt.as_bytes(),
100 NUM_PBKDF2_ROUNDS,
101 &mut spend_seed_bytes,
102 )
103 .expect("seed phrase hash always succeeds");
104 SpendKeyBytes(spend_seed_bytes).into()
105 }
106
107 pub fn from_seed_phrase_bip44(seed_phrase: SeedPhrase, path: &Bip44Path) -> Self {
108 let password = format!("{seed_phrase}");
109 let salt = "mnemonic";
110 let mut seed_bytes = [0u8; 64];
111 pbkdf2::<Hmac<sha2::Sha512>>(
112 password.as_bytes(),
113 salt.as_bytes(),
114 NUM_PBKDF2_ROUNDS,
115 &mut seed_bytes,
116 )
117 .expect("seed phrase hash always succeeds");
118
119 let child_key = XPrv::derive_from_path(
122 &seed_bytes[..],
123 &path.path().parse().expect("valid BIP44 path"),
124 )
125 .expect("can derive child key");
126 let child_key_bytes = child_key.to_bytes();
127
128 SpendKeyBytes(child_key_bytes).into()
129 }
130
131 pub fn spend_auth_key(&self) -> &SigningKey<SpendAuth> {
135 &self.ask
136 }
137
138 pub fn full_viewing_key(&self) -> &FullViewingKey {
139 &self.fvk
140 }
141
142 pub fn nullifier_key(&self) -> &NullifierKey {
143 self.fvk.nullifier_key()
144 }
145
146 pub fn outgoing_viewing_key(&self) -> &OutgoingViewingKey {
147 self.fvk.outgoing()
148 }
149
150 pub fn incoming_viewing_key(&self) -> &IncomingViewingKey {
151 self.fvk.incoming()
152 }
153}
154
155impl From<[u8; SPENDKEY_LEN_BYTES]> for SpendKeyBytes {
156 fn from(bytes: [u8; SPENDKEY_LEN_BYTES]) -> Self {
157 Self(bytes)
158 }
159}
160
161impl TryFrom<&[u8]> for SpendKeyBytes {
162 type Error = anyhow::Error;
163 fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
164 if slice.len() != SPENDKEY_LEN_BYTES {
165 anyhow::bail!("spendseed must be 32 bytes, got {:?}", slice.len());
166 }
167
168 let mut bytes = [0u8; SPENDKEY_LEN_BYTES];
169 bytes.copy_from_slice(&slice[0..32]);
170 Ok(SpendKeyBytes(bytes))
171 }
172}
173
174impl std::fmt::Display for SpendKey {
175 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
176 use penumbra_sdk_proto::serializers::bech32str;
177 let proto = pb::SpendKey::from(self.clone());
178 f.write_str(&bech32str::encode(
179 &proto.inner,
180 bech32str::spend_key::BECH32_PREFIX,
181 bech32str::Bech32m,
182 ))
183 }
184}
185
186impl std::str::FromStr for SpendKey {
187 type Err = anyhow::Error;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 use penumbra_sdk_proto::serializers::bech32str;
191 pb::SpendKey {
192 inner: bech32str::decode(s, bech32str::spend_key::BECH32_PREFIX, bech32str::Bech32m)?,
193 }
194 .try_into()
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use std::str::FromStr;
201
202 use super::*;
203
204 #[test]
205 fn bip44_test_ledger() {
206 let seed = SeedPhrase::from_str("comfort ten front cycle churn burger oak absent rice ice urge result art couple benefit cabbage frequent obscure hurry trick segment cool job debate").unwrap();
208
209 let expected_bytes =
210 hex::decode("1b8113fad04f5db00e6acf541949950f85eca3e02e70254838b750b42a2caa51")
211 .expect("valid");
212 let expected_spendkey = SpendKeyBytes(expected_bytes.try_into().expect("fits in 32 bytes"));
213
214 let derivation_path = Bip44Path::new(0);
215 let software_spendkey = SpendKey::from_seed_phrase_bip44(seed, &derivation_path);
216
217 assert_eq!(software_spendkey.to_bytes(), expected_spendkey);
218 }
219}