Skip to main content

hive_rs/crypto/
keys.rs

1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use secp256k1::ecdh;
5use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
6use secp256k1::rand::thread_rng;
7use secp256k1::{Message, PublicKey as SecpPublicKey, Secp256k1, SecretKey};
8
9use crate::crypto::signature::Signature;
10use crate::crypto::utils::{double_sha256, ripemd160, sha256, sha512};
11use crate::error::{HiveError, Result};
12use crate::serialization::serializer::transaction_digest;
13use crate::types::{ChainId, SignedTransaction, Transaction};
14
15const NETWORK_ID: u8 = 0x80;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum KeyRole {
19    Owner,
20    Active,
21    Posting,
22    Memo,
23}
24
25impl KeyRole {
26    pub fn as_str(self) -> &'static str {
27        match self {
28            Self::Owner => "owner",
29            Self::Active => "active",
30            Self::Posting => "posting",
31            Self::Memo => "memo",
32        }
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct PublicKey {
38    pub(crate) key: Option<SecpPublicKey>,
39    pub(crate) prefix: String,
40}
41
42impl PublicKey {
43    pub fn from_string(value: &str) -> Result<Self> {
44        if value.len() < 3 {
45            return Err(HiveError::InvalidKey(
46                "public key must include a 3-byte prefix".to_string(),
47            ));
48        }
49
50        let prefix = &value[..3];
51        let encoded = &value[3..];
52        let decoded = bs58::decode(encoded)
53            .into_vec()
54            .map_err(|err| HiveError::InvalidKey(format!("invalid base58 public key: {err}")))?;
55
56        if decoded.len() != 37 {
57            return Err(HiveError::InvalidKey(format!(
58                "public key payload must be 37 bytes, got {}",
59                decoded.len()
60            )));
61        }
62
63        let key_bytes: [u8; 33] = decoded[..33]
64            .try_into()
65            .map_err(|_| HiveError::InvalidKey("invalid public key payload".to_string()))?;
66        let checksum = &decoded[33..37];
67        let expected = &ripemd160(&key_bytes)[0..4];
68
69        if checksum != expected {
70            return Err(HiveError::InvalidKey(
71                "public key checksum mismatch".to_string(),
72            ));
73        }
74
75        if key_bytes == [0_u8; 33] {
76            return Ok(Self {
77                key: None,
78                prefix: prefix.to_string(),
79            });
80        }
81
82        let key = SecpPublicKey::from_slice(&key_bytes)
83            .map_err(|err| HiveError::InvalidKey(format!("invalid public key bytes: {err}")))?;
84        Ok(Self {
85            key: Some(key),
86            prefix: prefix.to_string(),
87        })
88    }
89
90    pub fn from_bytes(bytes: [u8; 33], prefix: impl Into<String>) -> Result<Self> {
91        if bytes == [0_u8; 33] {
92            return Ok(Self {
93                key: None,
94                prefix: prefix.into(),
95            });
96        }
97        let key = SecpPublicKey::from_slice(&bytes)
98            .map_err(|err| HiveError::InvalidKey(format!("invalid public key bytes: {err}")))?;
99        Ok(Self {
100            key: Some(key),
101            prefix: prefix.into(),
102        })
103    }
104
105    pub(crate) fn from_secp256k1(key: SecpPublicKey, prefix: impl Into<String>) -> Self {
106        Self {
107            key: Some(key),
108            prefix: prefix.into(),
109        }
110    }
111
112    pub fn to_string_with_prefix(&self, prefix: &str) -> String {
113        let key_bytes = self.compressed_bytes();
114        let checksum = ripemd160(&key_bytes);
115        let mut data = Vec::with_capacity(37);
116        data.extend_from_slice(&key_bytes);
117        data.extend_from_slice(&checksum[..4]);
118        format!("{prefix}{}", bs58::encode(data).into_string())
119    }
120
121    pub fn compressed_bytes(&self) -> [u8; 33] {
122        match self.key {
123            Some(key) => key.serialize(),
124            None => [0_u8; 33],
125        }
126    }
127
128    pub fn is_null(&self) -> bool {
129        self.key.is_none()
130    }
131
132    pub fn prefix(&self) -> &str {
133        self.prefix.as_str()
134    }
135
136    pub fn verify(&self, digest: &[u8; 32], signature: &Signature) -> bool {
137        let Some(public_key) = &self.key else {
138            return false;
139        };
140
141        let msg = Message::from_digest_slice(digest);
142        let sig = secp256k1::ecdsa::Signature::from_compact(&signature.compact_bytes());
143        match (msg, sig) {
144            (Ok(msg), Ok(sig)) => {
145                let secp = Secp256k1::verification_only();
146                secp.verify_ecdsa(&msg, &sig, public_key).is_ok()
147            }
148            _ => false,
149        }
150    }
151}
152
153impl Display for PublicKey {
154    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155        write!(f, "{}", self.to_string_with_prefix(&self.prefix))
156    }
157}
158
159impl FromStr for PublicKey {
160    type Err = HiveError;
161
162    fn from_str(s: &str) -> Result<Self> {
163        Self::from_string(s)
164    }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct PrivateKey {
169    pub(crate) secret: SecretKey,
170}
171
172impl PrivateKey {
173    pub fn from_wif(wif: &str) -> Result<Self> {
174        let decoded = bs58::decode(wif)
175            .into_vec()
176            .map_err(|err| HiveError::InvalidKey(format!("invalid base58 wif: {err}")))?;
177
178        if decoded.len() != 37 {
179            return Err(HiveError::InvalidKey(format!(
180                "wif payload must be 37 bytes, got {}",
181                decoded.len()
182            )));
183        }
184
185        if decoded[0] != NETWORK_ID {
186            return Err(HiveError::InvalidKey(
187                "private key network id mismatch".to_string(),
188            ));
189        }
190
191        let payload = &decoded[..33];
192        let checksum = &decoded[33..37];
193        let expected = &double_sha256(payload)[..4];
194        if checksum != expected {
195            return Err(HiveError::InvalidKey(
196                "private key checksum mismatch".to_string(),
197            ));
198        }
199
200        let key_bytes: [u8; 32] = payload[1..33]
201            .try_into()
202            .map_err(|_| HiveError::InvalidKey("invalid private key bytes".to_string()))?;
203        Self::from_bytes(key_bytes)
204    }
205
206    pub fn from_seed(seed: &str) -> Result<Self> {
207        Self::from_bytes(sha256(seed.as_bytes()))
208    }
209
210    pub fn from_login(username: &str, password: &str, role: KeyRole) -> Result<Self> {
211        let seed = format!("{username}{}{password}", role.as_str());
212        Self::from_seed(&seed)
213    }
214
215    pub fn from_bytes(bytes: [u8; 32]) -> Result<Self> {
216        let secret = SecretKey::from_slice(&bytes)
217            .map_err(|err| HiveError::InvalidKey(format!("invalid private key bytes: {err}")))?;
218        Ok(Self { secret })
219    }
220
221    pub fn generate() -> Self {
222        let mut rng = thread_rng();
223        let secret = SecretKey::new(&mut rng);
224        Self { secret }
225    }
226
227    pub fn to_wif(&self) -> String {
228        let mut payload = [0_u8; 33];
229        payload[0] = NETWORK_ID;
230        payload[1..].copy_from_slice(&self.secret.secret_bytes());
231        let checksum = double_sha256(&payload);
232        let mut full = Vec::with_capacity(37);
233        full.extend_from_slice(&payload);
234        full.extend_from_slice(&checksum[..4]);
235        bs58::encode(full).into_string()
236    }
237
238    pub fn public_key(&self) -> PublicKey {
239        let secp = Secp256k1::new();
240        let key = SecpPublicKey::from_secret_key(&secp, &self.secret);
241        PublicKey::from_secp256k1(key, "STM")
242    }
243
244    pub fn sign(&self, digest: &[u8; 32]) -> Result<Signature> {
245        let secp = Secp256k1::new();
246        let msg = Message::from_digest_slice(digest)
247            .map_err(|err| HiveError::Signing(format!("invalid digest: {err}")))?;
248
249        let mut attempts = 0_u16;
250        loop {
251            attempts = attempts.saturating_add(1);
252            let nonce_seed = sha256(&[digest.as_slice(), &[(attempts as u8)]].concat());
253            let recoverable =
254                secp.sign_ecdsa_recoverable_with_noncedata(&msg, &self.secret, &nonce_seed);
255            let (recovery_id, compact) = recoverable.serialize_compact();
256            if Signature::is_canonical_compact(&compact) {
257                return Signature::from_compact(compact, recovery_id.to_i32() as u8);
258            }
259
260            if attempts == u16::MAX {
261                return Err(HiveError::Signing(
262                    "unable to produce canonical signature".to_string(),
263                ));
264            }
265        }
266    }
267
268    pub fn get_shared_secret(&self, public_key: &PublicKey) -> [u8; 64] {
269        let Some(key) = &public_key.key else {
270            return [0_u8; 64];
271        };
272
273        let point = ecdh::shared_secret_point(key, &self.secret);
274        let x_coord = &point[..32];
275        sha512(x_coord)
276    }
277
278    pub fn secret_bytes(&self) -> [u8; 32] {
279        self.secret.secret_bytes()
280    }
281}
282
283impl Display for PrivateKey {
284    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
285        write!(f, "{}", self.to_wif())
286    }
287}
288
289impl FromStr for PrivateKey {
290    type Err = HiveError;
291
292    fn from_str(s: &str) -> Result<Self> {
293        Self::from_wif(s)
294    }
295}
296
297impl TryFrom<&str> for PrivateKey {
298    type Error = HiveError;
299
300    fn try_from(value: &str) -> Result<Self> {
301        Self::from_wif(value)
302    }
303}
304
305impl TryFrom<String> for PrivateKey {
306    type Error = HiveError;
307
308    fn try_from(value: String) -> Result<Self> {
309        Self::from_wif(&value)
310    }
311}
312
313pub(crate) fn recoverable_from_signature(signature: &Signature) -> Result<RecoverableSignature> {
314    let rec_id = RecoveryId::from_i32(signature.recovery_id() as i32)
315        .map_err(|err| HiveError::Signing(format!("invalid recovery id: {err}")))?;
316    RecoverableSignature::from_compact(&signature.compact_bytes(), rec_id)
317        .map_err(|err| HiveError::Signing(format!("invalid compact signature: {err}")))
318}
319
320pub fn sign_transaction(
321    transaction: &Transaction,
322    keys: &[&PrivateKey],
323    chain_id: &ChainId,
324) -> Result<SignedTransaction> {
325    let digest = transaction_digest(transaction, chain_id)?;
326    let signatures = keys
327        .iter()
328        .map(|key| key.sign(&digest).map(|sig| sig.to_hex()))
329        .collect::<Result<Vec<_>>>()?;
330
331    Ok(SignedTransaction {
332        ref_block_num: transaction.ref_block_num,
333        ref_block_prefix: transaction.ref_block_prefix,
334        expiration: transaction.expiration.clone(),
335        operations: transaction.operations.clone(),
336        extensions: transaction.extensions.clone(),
337        signatures,
338    })
339}
340
341#[cfg(test)]
342mod tests {
343    use crate::crypto::keys::{sign_transaction, KeyRole, PrivateKey, PublicKey};
344    use crate::types::{ChainId, Operation, Transaction, VoteOperation};
345
346    #[test]
347    fn from_login_matches_dhive_vector() {
348        let key = PrivateKey::from_login("foo", "barman", KeyRole::Active).expect("valid key");
349        assert_eq!(
350            key.public_key().to_string(),
351            "STM87F7tN56tAUL2C6J9Gzi9HzgNpZdi6M2cLQo7TjDU5v178QsYA"
352        );
353    }
354
355    #[test]
356    fn wif_round_trip() {
357        let key = PrivateKey::generate();
358        let wif = key.to_wif();
359        let parsed = PrivateKey::from_wif(&wif).expect("wif should parse");
360        assert_eq!(parsed.secret_bytes(), key.secret_bytes());
361    }
362
363    #[test]
364    fn known_wif_to_public_key() {
365        let key = PrivateKey::from_wif("5KG4sr3rMH1QuduYj79p36h7PrEeZakHEPjB9NkLWqgw19DDieL")
366            .expect("wif should parse");
367        assert_eq!(
368            key.public_key().to_string(),
369            "STM87F7tN56tAUL2C6J9Gzi9HzgNpZdi6M2cLQo7TjDU5v178QsYA"
370        );
371    }
372
373    #[test]
374    fn public_key_round_trip() {
375        let key = PublicKey::from_string("STM87F7tN56tAUL2C6J9Gzi9HzgNpZdi6M2cLQo7TjDU5v178QsYA")
376            .expect("public key should parse");
377        assert_eq!(
378            key.to_string(),
379            "STM87F7tN56tAUL2C6J9Gzi9HzgNpZdi6M2cLQo7TjDU5v178QsYA"
380        );
381    }
382
383    #[test]
384    fn detects_null_public_key() {
385        let key = PublicKey::from_string("STM1111111111111111111111111111111114T1Anm")
386            .expect("null public key should parse");
387        assert!(key.is_null());
388        assert_eq!(key.compressed_bytes(), [0_u8; 33]);
389    }
390
391    #[test]
392    fn sign_transaction_matches_dhive_vector() {
393        let key = PrivateKey::from_wif("5KG4sr3rMH1QuduYj79p36h7PrEeZakHEPjB9NkLWqgw19DDieL")
394            .expect("wif should parse");
395        let tx = Transaction {
396            ref_block_num: 1234,
397            ref_block_prefix: 1122334455,
398            expiration: "2017-07-15T16:51:19".to_string(),
399            operations: vec![Operation::Vote(VoteOperation {
400                voter: "foo".to_string(),
401                author: "bar".to_string(),
402                permlink: "baz".to_string(),
403                weight: 10000,
404            })],
405            extensions: vec!["long-pants".to_string()],
406        };
407
408        let chain_id = ChainId { bytes: [0_u8; 32] };
409        let signed = sign_transaction(&tx, &[&key], &chain_id).expect("transaction should sign");
410        assert_eq!(
411            signed.signatures[0],
412            "1f037a09c1110a8bd8757ad3081a11456d241feedd4366723bb9f9046cc6a1b21b26bf4b8372546bc2446c7498ff5742dce0143ff1fe13591eb8dd88b9a7fef2f2"
413        );
414    }
415}