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}