1#![allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
10
11use ed25519_zebra::{SigningKey, VerificationKey};
12#[cfg(feature = "host")]
13use sodiumoxide::crypto::box_;
14#[cfg(feature = "host")]
15use sodiumoxide::crypto::sealedbox;
16#[cfg(feature = "host")]
17use sodiumoxide::crypto::sign::ed25519 as sign_ed25519;
18use sp_core::crypto::{
19 AccountId32, CryptoType, DeriveError, DeriveJunction, Pair as PairT, SecretStringError,
20 Ss58Codec,
21};
22use sp_core::{ed25519, sr25519, ByteArray};
23use zeroize::Zeroizing;
24
25use crate::error::CoreError;
26
27mod base58;
28mod hashed;
29pub use hashed::{verify_hashed_authorization, HashedKeypair};
30mod falcon;
31mod mldsa;
32pub use mldsa::{verify_mldsa_authorization, MldsaKeypair, ROLE_TRANSACTION};
33mod receiving;
34pub use receiving::{
35 decode_hashed_receiving_address, decode_mldsa_receiving_address,
36 encode_hashed_receiving_address, encode_mldsa_receiving_address,
37};
38#[cfg(feature = "host")]
39mod encrypted_json;
40
41pub const CRYPTO_ED25519: u8 = 0;
43pub const CRYPTO_SR25519: u8 = 1;
44pub const CRYPTO_ECDSA: u8 = 2;
45pub const ECDSA_PUBLIC_KEY_LEN: usize = sp_core::ecdsa::PUBLIC_KEY_SERIALIZED_SIZE;
47pub const ED25519_SIGNATURE_LEN: usize = ed25519::SIGNATURE_SERIALIZED_SIZE;
49pub const SR25519_SIGNATURE_LEN: usize = sr25519::SIGNATURE_SERIALIZED_SIZE;
50pub const ECDSA_SIGNATURE_LEN: usize = sp_core::ecdsa::SIGNATURE_SERIALIZED_SIZE;
51pub const CRYPTO_HASHED: u8 = 4;
53pub const CRYPTO_HASHED_ED25519: u8 = 6;
54pub const CRYPTO_MLDSA65: u8 = 7;
56pub const CRYPTO_MLDSA65_FIXED: u8 = 8;
58
59pub const CRYPTO_FALCON512: u8 = 9;
60pub const CRYPTO_FALCON512_HASHED: u8 = 10;
61pub const CRYPTO_FALCON1024: u8 = 11;
62pub const CRYPTO_FALCON1024_HASHED: u8 = 12;
63
64pub fn is_falcon_crypto(crypto_type: u8) -> bool {
65 matches!(
66 crypto_type,
67 CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED | CRYPTO_FALCON1024 | CRYPTO_FALCON1024_HASHED
68 )
69}
70
71pub fn is_pq_crypto(crypto_type: u8) -> bool {
72 is_mldsa_crypto(crypto_type) || is_falcon_crypto(crypto_type)
73}
74
75pub fn is_fixed_pq_crypto(crypto_type: u8) -> bool {
76 matches!(
77 crypto_type,
78 CRYPTO_MLDSA65_FIXED | CRYPTO_FALCON512 | CRYPTO_FALCON1024
79 )
80}
81
82pub fn pq_profile(crypto_type: u8) -> Option<u8> {
83 match crypto_type {
84 CRYPTO_MLDSA65 => Some(4),
85 CRYPTO_MLDSA65_FIXED => Some(5),
86 CRYPTO_FALCON512_HASHED => Some(6),
87 CRYPTO_FALCON512 => Some(7),
88 CRYPTO_FALCON1024_HASHED => Some(8),
89 CRYPTO_FALCON1024 => Some(9),
90 _ => None,
91 }
92}
93
94pub fn is_mldsa_crypto(crypto_type: u8) -> bool {
95 matches!(crypto_type, CRYPTO_MLDSA65 | CRYPTO_MLDSA65_FIXED)
96}
97
98pub fn is_hashed_crypto(crypto_type: u8) -> bool {
99 matches!(crypto_type, CRYPTO_HASHED | CRYPTO_HASHED_ED25519)
100}
101
102pub fn is_rotating_crypto(crypto_type: u8) -> bool {
103 is_hashed_crypto(crypto_type)
104 || matches!(
105 crypto_type,
106 CRYPTO_MLDSA65 | CRYPTO_FALCON512_HASHED | CRYPTO_FALCON1024_HASHED
107 )
108}
109
110pub fn account_crypto_type(crypto_type: u8, mode: Option<&str>) -> Result<u8, CoreError> {
112 if is_falcon_crypto(crypto_type) {
113 let (fixed, hashed) = if matches!(crypto_type, CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED) {
114 (CRYPTO_FALCON512, CRYPTO_FALCON512_HASHED)
115 } else {
116 (CRYPTO_FALCON1024, CRYPTO_FALCON1024_HASHED)
117 };
118 return match mode {
119 None => Ok(crypto_type),
120 Some("hashed") => Ok(hashed),
121 Some("standard") => Ok(fixed),
122 _ => Err(crypto_err("account type must be standard or hashed")),
123 };
124 }
125 if is_mldsa_crypto(crypto_type) {
126 return match mode {
127 None => Ok(crypto_type),
128 Some("hashed") => Ok(CRYPTO_MLDSA65),
129 Some("standard") => Ok(CRYPTO_MLDSA65_FIXED),
130 _ => Err(crypto_err("account type must be standard or hashed")),
131 };
132 }
133 let Some(mode) = mode else {
134 return match crypto_type {
135 CRYPTO_SR25519 | CRYPTO_ED25519 | CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
136 Ok(crypto_type)
137 }
138 _ => Err(crypto_err("unsupported signing scheme")),
139 };
140 };
141 let scheme = match crypto_type {
142 CRYPTO_SR25519 | CRYPTO_HASHED => CRYPTO_SR25519,
143 CRYPTO_ED25519 | CRYPTO_HASHED_ED25519 => CRYPTO_ED25519,
144 _ => return Err(crypto_err("unsupported signing scheme")),
145 };
146 match (mode, scheme) {
147 ("standard", CRYPTO_SR25519) => Ok(CRYPTO_SR25519),
148 ("standard", CRYPTO_ED25519) => Ok(CRYPTO_ED25519),
149 ("hashed", CRYPTO_SR25519) => Ok(CRYPTO_HASHED),
150 ("hashed", CRYPTO_ED25519) => Ok(CRYPTO_HASHED_ED25519),
151 _ => Err(crypto_err("account type must be standard or hashed")),
152 }
153}
154
155pub const DEFAULT_SS58_FORMAT: u16 = 42;
156
157fn crypto_err(msg: impl Into<String>) -> CoreError {
158 CoreError::Crypto(msg.into())
159}
160
161#[cfg(feature = "host")]
162pub(crate) fn ensure_sodium() -> Result<(), CoreError> {
163 sodiumoxide::init().map_err(|_| crypto_err("failed to initialize libsodium"))?;
164 Ok(())
165}
166
167fn as_bytes<T: AsRef<[u8]>>(value: &T) -> Vec<u8> {
169 value.as_ref().to_vec()
170}
171
172pub fn public_key_from_ss58(ss58_address: &str) -> Result<[u8; 32], CoreError> {
175 let account = AccountId32::from_ss58check(ss58_address)
176 .map_err(|e| crypto_err(format!("invalid ss58 address: {e:?}")))?;
177 Ok(account.into())
178}
179
180pub fn ss58_from_public(public_key: [u8; 32], ss58_format: u16) -> String {
184 let mut buf = [0u8; 7 + 2 + 32 + 2];
186 buf[..7].copy_from_slice(b"SS58PRE");
187 let ident = ss58_format & 0b0011_1111_1111_1111;
188 let prefix_len = if ident < 64 {
189 buf[7] = ident as u8;
190 1
191 } else {
192 buf[7] = (((ident & 0b0000_0000_1111_1100) >> 2) as u8) | 0b0100_0000;
194 buf[8] = ((ident >> 8) as u8) | (((ident & 0b11) as u8) << 6);
195 2
196 };
197 buf[7 + prefix_len..7 + prefix_len + 32].copy_from_slice(&public_key);
198 let body_end = 7 + prefix_len + 32;
199 let checksum = sp_core::hashing::blake2_512(&buf[..body_end]);
200 buf[body_end] = checksum[0];
201 buf[body_end + 1] = checksum[1];
202 base58::base58_encode(&buf[7..body_end + 2])
203}
204
205pub fn account_id_from_ecdsa_public(public_key: &[u8]) -> Result<[u8; 32], CoreError> {
208 let compressed = <[u8; ECDSA_PUBLIC_KEY_LEN]>::try_from(public_key).map_err(|_| {
209 crypto_err(format!(
210 "ecdsa public key must be {ECDSA_PUBLIC_KEY_LEN} bytes (compressed secp256k1)"
211 ))
212 })?;
213 if !matches!(compressed[0], 0x02 | 0x03) {
214 return Err(crypto_err(
215 "ecdsa public key must be compressed (0x02/0x03 prefix)",
216 ));
217 }
218 Ok(sp_core::hashing::blake2_256(&compressed))
219}
220
221pub fn ss58_from_ecdsa_public(public_key: &[u8], ss58_format: u16) -> Result<String, CoreError> {
223 Ok(ss58_from_public(
224 account_id_from_ecdsa_public(public_key)?,
225 ss58_format,
226 ))
227}
228
229fn verify_with_crypto(
230 crypto_type: u8,
231 public_key: &[u8; 32],
232 message: &[u8],
233 signature: &[u8],
234) -> Result<bool, CoreError> {
235 match crypto_type {
236 CRYPTO_SR25519 => {
237 let public = sr25519::Public::from_raw(*public_key);
238 let sig = sr25519::Signature::from_slice(signature)
239 .map_err(|_| crypto_err("invalid sr25519 signature length"))?;
240 Ok(sr25519::Pair::verify(&sig, message, &public))
241 }
242 CRYPTO_ED25519 => {
243 let public = ed25519::Public::from_raw(*public_key);
244 let sig = ed25519::Signature::from_slice(signature)
245 .map_err(|_| crypto_err("invalid ed25519 signature length"))?;
246 Ok(ed25519::Pair::verify(&sig, message, &public))
247 }
248 CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => Ok(hashed::verify_message(
249 crypto_type,
250 public_key,
251 message,
252 signature,
253 )),
254 other => Err(crypto_err(format!("unknown crypto type {other}"))),
255 }
256}
257
258#[cfg(feature = "host")]
259fn ed25519_to_x25519_pk(public_key: &[u8; 32]) -> Result<box_::PublicKey, CoreError> {
260 ensure_sodium()?;
261 let ed25519_pk = sign_ed25519::PublicKey::from_slice(public_key)
262 .ok_or_else(|| crypto_err("invalid ed25519 public key"))?;
263 sign_ed25519::to_curve25519_pk(&ed25519_pk)
264 .map_err(|_| crypto_err("failed to convert ed25519 public key to x25519"))
265}
266
267fn ed25519_seed(pair: &Zeroizing<SigningKey>) -> &[u8] {
268 let pair: &SigningKey = pair;
269 pair.as_ref()
270}
271
272fn ed25519_public(pair: &Zeroizing<SigningKey>) -> [u8; 32] {
273 let pair: &SigningKey = pair;
274 VerificationKey::from(pair).into()
275}
276
277#[cfg(feature = "host")]
278fn ed25519_x25519_from_pair(
279 pair: &Zeroizing<SigningKey>,
280) -> Result<(box_::PublicKey, box_::SecretKey), CoreError> {
281 ensure_sodium()?;
282 let seed = sign_ed25519::Seed::from_slice(ed25519_seed(pair))
283 .ok_or_else(|| crypto_err("failed to derive x25519 keypair for decryption"))?;
284 let (pk, sk) = sign_ed25519::keypair_from_seed(&seed);
285 let x25519_pk = sign_ed25519::to_curve25519_pk(&pk)
286 .map_err(|_| crypto_err("failed to derive x25519 keypair for decryption"))?;
287 let x25519_sk = sign_ed25519::to_curve25519_sk(&sk)
288 .map_err(|_| crypto_err("failed to derive x25519 keypair for decryption"))?;
289 Ok((x25519_pk, x25519_sk))
290}
291
292struct WipingSeed(Zeroizing<[u8; 32]>);
293
294impl Default for WipingSeed {
295 fn default() -> Self {
296 Self(Zeroizing::new([0; 32]))
297 }
298}
299
300impl Clone for WipingSeed {
301 fn clone(&self) -> Self {
302 let mut clone = Self::default();
303 clone.as_mut().copy_from_slice(self.as_ref());
304 clone
305 }
306}
307
308impl AsRef<[u8]> for WipingSeed {
309 fn as_ref(&self) -> &[u8] {
310 &self.0[..]
311 }
312}
313
314impl AsMut<[u8]> for WipingSeed {
315 fn as_mut(&mut self) -> &mut [u8] {
316 &mut self.0[..]
317 }
318}
319
320const HDKD_PREIMAGE_LEN: usize = 1 + 11 + 32 + 32;
321
322struct WipingEd25519Pair {
325 secret: Zeroizing<SigningKey>,
326}
327
328impl WipingEd25519Pair {
329 fn into_secret(self) -> Zeroizing<SigningKey> {
330 self.secret
331 }
332
333 fn seed(&self) -> WipingSeed {
334 let mut seed = WipingSeed::default();
335 seed.as_mut().copy_from_slice(ed25519_seed(&self.secret));
336 seed
337 }
338}
339
340impl CryptoType for WipingEd25519Pair {
341 type Pair = Self;
342}
343
344impl PairT for WipingEd25519Pair {
345 type Public = ed25519::Public;
346 type Seed = WipingSeed;
347 type Signature = ed25519::Signature;
348
349 fn from_seed_slice(seed: &[u8]) -> Result<Self, SecretStringError> {
350 let secret =
351 SigningKey::try_from(seed).map_err(|_| SecretStringError::InvalidSeedLength)?;
352 Ok(Self {
353 secret: Zeroizing::new(secret),
354 })
355 }
356
357 fn derive<Iter: Iterator<Item = DeriveJunction>>(
358 &self,
359 path: Iter,
360 _seed: Option<Self::Seed>,
361 ) -> Result<(Self, Option<Self::Seed>), DeriveError> {
362 let mut seed = self.seed();
363 for junction in path {
364 match junction {
365 DeriveJunction::Soft(_) => return Err(DeriveError::SoftKeyInPath),
366 DeriveJunction::Hard(chain_code) => {
367 let mut preimage = Zeroizing::new([0u8; HDKD_PREIMAGE_LEN]);
372 preimage[0] = 0x2c;
373 preimage[1..12].copy_from_slice(b"Ed25519HDKD");
374 preimage[12..44].copy_from_slice(&seed.0[..]);
375 preimage[44..].copy_from_slice(&chain_code);
376 let derived = Zeroizing::new(sp_core::hashing::blake2_256(&preimage[..]));
377 seed.0.copy_from_slice(&derived[..]);
378 }
379 }
380 }
381 Ok((Self::from_seed(&seed), Some(seed)))
382 }
383
384 fn public(&self) -> Self::Public {
385 ed25519::Public::from_raw(ed25519_public(&self.secret))
386 }
387
388 fn sign(&self, message: &[u8]) -> Self::Signature {
389 Self::Signature::from_raw(self.secret.sign(message).to_bytes())
390 }
391
392 fn verify<M: AsRef<[u8]>>(
393 signature: &Self::Signature,
394 message: M,
395 public: &Self::Public,
396 ) -> bool {
397 ed25519::Pair::verify(signature, message, public)
398 }
399
400 fn to_raw_vec(&self) -> Vec<u8> {
401 ed25519_seed(&self.secret).to_vec()
402 }
403}
404
405#[allow(clippy::large_enum_variant)]
408pub enum KeypairInner {
409 Ed25519(Zeroizing<SigningKey>),
410 Sr25519(sr25519::Pair),
411 Hashed(HashedKeypair),
412 Mldsa(MldsaKeypair),
413 PublicOnly {
414 public_key: [u8; 32],
415 crypto_type: u8,
416 },
417}
418
419impl KeypairInner {
420 fn private_key_bytes(&self) -> Option<Vec<u8>> {
421 match self {
422 KeypairInner::Ed25519(pair) => Some(ed25519_seed(pair).to_vec()),
423 KeypairInner::Sr25519(pair) => Some(pair.to_raw_vec()),
424 KeypairInner::Hashed(pair) => pair.master_seed().map(|seed| seed.to_vec()),
425 KeypairInner::Mldsa(pair) => pair.master_seed().map(|seed| seed.to_vec()),
426 KeypairInner::PublicOnly { .. } => None,
427 }
428 }
429}
430
431pub struct Keypair {
433 inner: KeypairInner,
434 ss58_format: u16,
435 mnemonic: Option<Zeroizing<String>>,
440 seed: Option<Zeroizing<Vec<u8>>>,
443}
444
445impl Keypair {
446 pub fn account_type(&self) -> &'static str {
447 if is_rotating_crypto(self.crypto_type()) {
448 "hashed"
449 } else {
450 "standard"
451 }
452 }
453
454 pub fn signing_scheme(&self) -> &'static str {
455 match self.crypto_type() {
456 CRYPTO_HASHED | CRYPTO_SR25519 => "sr25519",
457 CRYPTO_HASHED_ED25519 | CRYPTO_ED25519 => "ed25519",
458 CRYPTO_MLDSA65 | CRYPTO_MLDSA65_FIXED => "mldsa65",
459 CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED => "falcon512",
460 CRYPTO_FALCON1024 | CRYPTO_FALCON1024_HASHED => "falcon1024",
461 _ => "unknown",
462 }
463 }
464 pub fn new(
466 ss58_address: Option<&str>,
467 public_key: Option<&[u8]>,
468 crypto_type: u8,
469 ss58_format: u16,
470 ) -> Result<Self, CoreError> {
471 match crypto_type {
472 CRYPTO_SR25519
473 | CRYPTO_ED25519
474 | CRYPTO_HASHED
475 | CRYPTO_HASHED_ED25519
476 | CRYPTO_MLDSA65
477 | CRYPTO_MLDSA65_FIXED
478 | CRYPTO_FALCON512
479 | CRYPTO_FALCON512_HASHED
480 | CRYPTO_FALCON1024
481 | CRYPTO_FALCON1024_HASHED => {}
482 other => return Err(crypto_err(format!("unknown crypto type {other}"))),
483 }
484
485 let public_key = match (ss58_address, public_key) {
486 (Some(addr), None) => public_key_from_ss58(addr)?,
487 (None, Some(pk)) => <[u8; 32]>::try_from(pk)
488 .map_err(|_| crypto_err("public key must be exactly 32 bytes"))?,
489 (Some(addr), Some(pk)) => {
490 let from_addr = public_key_from_ss58(addr)?;
491 let from_pk = <[u8; 32]>::try_from(pk)
492 .map_err(|_| crypto_err("public key must be exactly 32 bytes"))?;
493 if from_addr != from_pk {
494 return Err(crypto_err(
495 "ss58 address and public key refer to different accounts",
496 ));
497 }
498 from_pk
499 }
500 (None, None) => {
501 return Err(crypto_err(
502 "no ss58 formatted address or public key provided",
503 ));
504 }
505 };
506
507 Ok(Self {
508 inner: KeypairInner::PublicOnly {
509 public_key,
510 crypto_type,
511 },
512 ss58_format,
513 mnemonic: None,
514 seed: None,
515 })
516 }
517
518 pub fn from_mnemonic(
520 mnemonic: &str,
521 crypto_type: u8,
522 password: Option<&str>,
523 ) -> Result<Self, CoreError> {
524 let (inner, seed) = match crypto_type {
525 crypto if is_pq_crypto(crypto) => {
526 let phrase = bip39::Mnemonic::parse_in(bip39::Language::English, mnemonic)
527 .map_err(|_| crypto_err("invalid post-quantum mnemonic"))?;
528 if phrase.word_count() != 24 {
529 return Err(crypto_err(
530 "post-quantum wallets require a 24-word mnemonic with 256-bit entropy",
531 ));
532 }
533 let expanded = Zeroizing::new(phrase.to_seed(password.unwrap_or("")));
534 let seed = Zeroizing::new(expanded[..32].to_vec());
535 (
536 KeypairInner::Mldsa(MldsaKeypair::from_seed(&seed, crypto_type)?),
537 seed,
538 )
539 }
540 CRYPTO_SR25519 => {
541 let (pair, seed) = sr25519::Pair::from_phrase(mnemonic, password)
542 .map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
543 (KeypairInner::Sr25519(pair), Zeroizing::new(seed.to_vec()))
544 }
545 CRYPTO_ED25519 => {
546 let (pair, seed) = WipingEd25519Pair::from_phrase(mnemonic, password)
547 .map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
548 (
549 KeypairInner::Ed25519(pair.into_secret()),
550 Zeroizing::new(seed.as_ref().to_vec()),
551 )
552 }
553 CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
554 let (_, seed) = sr25519::Pair::from_phrase(mnemonic, password)
555 .map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
556 (
557 KeypairInner::Hashed(HashedKeypair::from_seed(&seed, crypto_type)?),
558 Zeroizing::new(seed.to_vec()),
559 )
560 }
561 other => return Err(crypto_err(format!("unknown crypto type {other}"))),
562 };
563 Ok(Self {
564 inner,
565 ss58_format: DEFAULT_SS58_FORMAT,
566 mnemonic: password
570 .is_none()
571 .then(|| Zeroizing::new(mnemonic.to_string())),
572 seed: Some(seed),
573 })
574 }
575
576 pub fn from_seed(seed: &[u8], crypto_type: u8) -> Result<Self, CoreError> {
578 let inner = match crypto_type {
579 crypto if is_pq_crypto(crypto) => {
580 KeypairInner::Mldsa(MldsaKeypair::from_seed(seed, crypto_type)?)
581 }
582 CRYPTO_SR25519 => KeypairInner::Sr25519(
583 sr25519::Pair::from_seed_slice(seed)
584 .map_err(|e| crypto_err(format!("invalid seed: {e:?}")))?,
585 ),
586 CRYPTO_ED25519 => KeypairInner::Ed25519(
587 WipingEd25519Pair::from_seed_slice(seed)
588 .map_err(|e| crypto_err(format!("invalid seed: {e:?}")))?
589 .into_secret(),
590 ),
591 CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
592 KeypairInner::Hashed(HashedKeypair::from_seed(seed, crypto_type)?)
593 }
594 other => return Err(crypto_err(format!("unknown crypto type {other}"))),
595 };
596 Ok(Self {
597 inner,
598 ss58_format: DEFAULT_SS58_FORMAT,
599 mnemonic: None,
600 seed: Some(Zeroizing::new(seed.to_vec())),
601 })
602 }
603
604 pub fn from_uri(uri: &str, crypto_type: u8) -> Result<Self, CoreError> {
606 if is_hashed_crypto(crypto_type) || is_pq_crypto(crypto_type) {
607 return Err(crypto_err(
608 "hashed and post-quantum wallets require a mnemonic or a 32-byte master seed; secret URIs are unsupported",
609 ));
610 }
611 let inner = match crypto_type {
612 CRYPTO_SR25519 => KeypairInner::Sr25519(
613 sr25519::Pair::from_string(uri, None)
614 .map_err(|e| crypto_err(format!("invalid secret uri: {e:?}")))?,
615 ),
616 CRYPTO_ED25519 => KeypairInner::Ed25519(
617 WipingEd25519Pair::from_string(uri, None)
618 .map_err(|e| crypto_err(format!("invalid secret uri: {e:?}")))?
619 .into_secret(),
620 ),
621 other => return Err(crypto_err(format!("unknown crypto type {other}"))),
622 };
623 Ok(Self {
626 inner,
627 ss58_format: DEFAULT_SS58_FORMAT,
628 mnemonic: None,
629 seed: None,
630 })
631 }
632
633 pub fn from_private_key(private_key: &str, crypto_type: u8) -> Result<Self, CoreError> {
635 let private_key_vec = Zeroizing::new(
637 hex::decode(private_key.trim_start_matches("0x"))
638 .map_err(|_| crypto_err("invalid private_key hex string"))?,
639 );
640
641 if is_hashed_crypto(crypto_type) || is_pq_crypto(crypto_type) {
642 return Self::from_seed(&private_key_vec, crypto_type);
643 }
644
645 let inner = match crypto_type {
646 CRYPTO_SR25519 => {
647 KeypairInner::Sr25519(sr25519::Pair::from_seed_slice(&private_key_vec).map_err(
648 |error| crypto_err(format!("invalid sr25519 private key: {error:?}")),
649 )?)
650 }
651 CRYPTO_ED25519 => {
652 let seed = if private_key_vec.len() >= 32 {
653 &private_key_vec[..32]
654 } else {
655 return Err(crypto_err("ed25519 private key must be at least 32 bytes"));
656 };
657 KeypairInner::Ed25519(
658 WipingEd25519Pair::from_seed_slice(seed)
659 .map_err(|error| {
660 crypto_err(format!("invalid ed25519 private key: {error:?}"))
661 })?
662 .into_secret(),
663 )
664 }
665 other => return Err(crypto_err(format!("unknown crypto type {other}"))),
666 };
667
668 Ok(Self {
669 inner,
670 ss58_format: DEFAULT_SS58_FORMAT,
671 mnemonic: None,
672 seed: None,
673 })
674 }
675
676 pub fn generate_mnemonic(n_words: usize) -> Result<String, CoreError> {
678 use bip39::{Language, Mnemonic};
679
680 if !matches!(n_words, 12 | 15 | 18 | 21 | 24) {
681 return Err(crypto_err(format!(
682 "unsupported mnemonic length {n_words}; expected 12, 15, 18, 21, or 24"
683 )));
684 }
685
686 Mnemonic::generate_in(Language::English, n_words)
687 .map(|mnemonic| mnemonic.to_string())
688 .map_err(|error| crypto_err(format!("failed to generate mnemonic: {error}")))
689 }
690
691 #[cfg(feature = "host")]
693 pub fn from_encrypted_json(json_data: &str, passphrase: &str) -> Result<Self, CoreError> {
694 encrypted_json::create_from_encrypted_json(json_data, passphrase)
695 }
696
697 pub fn crypto_type(&self) -> u8 {
698 match &self.inner {
699 KeypairInner::Ed25519(_) => CRYPTO_ED25519,
700 KeypairInner::Sr25519(_) => CRYPTO_SR25519,
701 KeypairInner::Hashed(key) => key.crypto_type(),
702 KeypairInner::Mldsa(key) => match key.descriptor.algorithm {
703 4 => CRYPTO_MLDSA65,
704 5 => CRYPTO_MLDSA65_FIXED,
705 6 => CRYPTO_FALCON512_HASHED,
706 7 => CRYPTO_FALCON512,
707 8 => CRYPTO_FALCON1024_HASHED,
708 9 => CRYPTO_FALCON1024,
709 _ => unreachable!("validated post-quantum profile"),
710 },
711 KeypairInner::PublicOnly { crypto_type, .. } => *crypto_type,
712 }
713 }
714
715 pub fn public_key_bytes(&self) -> [u8; 32] {
718 match &self.inner {
719 KeypairInner::Ed25519(pair) => ed25519_public(pair),
720 KeypairInner::Sr25519(pair) => pair.public().0,
721 KeypairInner::Hashed(pair) => pair.account_id(),
722 KeypairInner::Mldsa(pair) => pair.account_id(),
723 KeypairInner::PublicOnly { public_key, .. } => *public_key,
724 }
725 }
726
727 pub fn ss58_address(&self) -> String {
728 ss58_from_public(self.public_key_bytes(), self.ss58_format)
729 }
730
731 pub fn ss58_format(&self) -> u16 {
732 self.ss58_format
733 }
734
735 pub fn private_key_bytes(&self) -> Option<Vec<u8>> {
736 self.inner.private_key_bytes()
737 }
738
739 pub fn mnemonic(&self) -> Option<&str> {
741 self.mnemonic.as_deref().map(String::as_str)
742 }
743
744 pub fn seed_bytes(&self) -> Option<&[u8]> {
746 self.seed.as_deref().map(Vec::as_slice)
747 }
748
749 #[cfg(feature = "host")]
750 pub(crate) fn from_inner(inner: KeypairInner, ss58_format: u16) -> Self {
751 Self {
752 inner,
753 ss58_format,
754 mnemonic: None,
755 seed: None,
756 }
757 }
758
759 pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
761 match &self.inner {
762 KeypairInner::Ed25519(pair) => Ok(pair.sign(message).to_bytes().to_vec()),
763 KeypairInner::Sr25519(pair) => Ok(as_bytes(&pair.sign(message))),
764 KeypairInner::Hashed(_) => self.sign_hashed_message(message),
765 KeypairInner::Mldsa(_) => Err(crypto_err(
766 "post-quantum arbitrary message signing is not supported",
767 )),
768 KeypairInner::PublicOnly { .. } => {
769 Err(crypto_err("no private key set to create signatures"))
770 }
771 }
772 }
773
774 pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, CoreError> {
776 let public_key = self.public_key_bytes();
777 let crypto_type = self.crypto_type();
778 if verify_with_crypto(crypto_type, &public_key, message, signature)? {
779 return Ok(true);
780 }
781 let wrapped = [b"<Bytes>", message, b"</Bytes>"].concat();
782 verify_with_crypto(crypto_type, &public_key, &wrapped, signature)
783 }
784
785 #[cfg(feature = "host")]
787 pub fn encrypt(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
788 if self.crypto_type() != CRYPTO_ED25519 {
789 return Err(crypto_err(
790 "encrypt/decrypt is only supported for ed25519 keypairs",
791 ));
792 }
793 ensure_sodium()?;
794 let x25519_pk = ed25519_to_x25519_pk(&self.public_key_bytes())?;
795 Ok(sealedbox::seal(message, &x25519_pk))
796 }
797
798 #[cfg(feature = "host")]
800 pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CoreError> {
801 if self.crypto_type() != CRYPTO_ED25519 {
802 return Err(crypto_err(
803 "encrypt/decrypt is only supported for ed25519 keypairs",
804 ));
805 }
806 let KeypairInner::Ed25519(pair) = &self.inner else {
807 return Err(crypto_err(
808 "decryption requires a keypair with a private key",
809 ));
810 };
811 ensure_sodium()?;
812 let (x25519_pk, x25519_sk) = ed25519_x25519_from_pair(pair)?;
813 sealedbox::open(ciphertext, &x25519_pk, &x25519_sk)
814 .map_err(|_| crypto_err("decryption failed: invalid ciphertext or wrong key"))
815 }
816
817 #[cfg(feature = "host")]
819 pub fn encrypt_for(
820 ss58_address: &str,
821 message: &[u8],
822 crypto_type: u8,
823 ) -> Result<Vec<u8>, CoreError> {
824 let recipient = Self::new(Some(ss58_address), None, crypto_type, DEFAULT_SS58_FORMAT)?;
825 recipient.encrypt(message)
826 }
827}
828
829pub fn verify_strict(
837 message: &[u8],
838 signature: &[u8],
839 ss58_address: &str,
840 crypto_type: u8,
841) -> Result<bool, CoreError> {
842 let public_key = public_key_from_ss58(ss58_address)?;
843 verify_with_crypto(crypto_type, &public_key, message, signature)
844}
845
846pub fn verify(
848 message: &[u8],
849 signature: &[u8],
850 ss58_address: &str,
851 crypto_type: u8,
852) -> Result<bool, CoreError> {
853 let public_key = public_key_from_ss58(ss58_address)?;
854 if verify_with_crypto(crypto_type, &public_key, message, signature)? {
855 return Ok(true);
856 }
857 let wrapped = [b"<Bytes>", message, b"</Bytes>"].concat();
858 verify_with_crypto(crypto_type, &public_key, &wrapped, signature)
859}
860
861#[cfg(test)]
862mod tests {
863 #![allow(clippy::unwrap_used, clippy::expect_used)]
864
865 use sp_core::crypto::Ss58AddressFormat;
866
867 use super::*;
868
869 #[test]
870 fn ss58_rendering_matches_sp_core() {
871 let mut state = 0x243f_6a88_85a3_08d3u64;
872 for format in [0u16, 2, 42, 63, 64, 255, 4096, 16383] {
873 for i in 0..50u32 {
874 let mut key = [0u8; 32];
875 key[..4].copy_from_slice(&i.to_le_bytes());
876 for b in &mut key[4..] {
877 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
878 *b = (state >> 33) as u8;
879 }
880 assert_eq!(
881 ss58_from_public(key, format),
882 AccountId32::from(key)
883 .to_ss58check_with_version(Ss58AddressFormat::custom(format)),
884 "diverged for format {format}"
885 );
886 }
887 }
888 }
889
890 #[test]
891 fn alice_uri_matches_known_address() {
892 let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
893 assert_eq!(
894 kp.ss58_address(),
895 "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
896 );
897 }
898
899 #[test]
900 fn public_only_from_ss58_matches_full_key() {
901 let full = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
902 let public = Keypair::new(Some(&full.ss58_address()), None, CRYPTO_SR25519, 42).unwrap();
903 assert_eq!(public.ss58_address(), full.ss58_address());
904 assert_eq!(public.public_key_bytes(), full.public_key_bytes());
905 }
906
907 #[test]
908 fn sr25519_verify_roundtrip() {
909 let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
910 let message = b"hello";
911 let sig = kp.sign(message).unwrap();
912 assert!(kp.verify(message, &sig).unwrap());
913 }
914
915 #[test]
916 fn ecdsa_ss58_matches_sp_core() {
917 use sp_core::ecdsa;
918
919 let pair = ecdsa::Pair::from_string("//Alice", None).unwrap();
922 let public = pair.public();
923 let compressed: &[u8] = public.as_ref();
924 assert_eq!(compressed.len(), ECDSA_PUBLIC_KEY_LEN);
925 let expected = AccountId32::from(sp_core::hashing::blake2_256(compressed));
926 let account = account_id_from_ecdsa_public(compressed).unwrap();
927 assert_eq!(account, <[u8; 32]>::from(expected.clone()));
928 for format in [0u16, 42, 4096] {
929 assert_eq!(
930 ss58_from_ecdsa_public(compressed, format).unwrap(),
931 expected.to_ss58check_with_version(Ss58AddressFormat::custom(format))
932 );
933 }
934 }
935
936 #[test]
937 fn ecdsa_ss58_rejects_non_compressed_keys() {
938 assert!(account_id_from_ecdsa_public(&[0x02; 32]).is_err());
939 assert!(account_id_from_ecdsa_public(&[0x04; 33]).is_err());
940 assert!(account_id_from_ecdsa_public(&[0x04; 65]).is_err());
941 }
942
943 #[test]
944 fn sr25519_verify_bytes_wrapping() {
945 let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
946 let sig = kp.sign(b"<Bytes>hello</Bytes>").unwrap();
947 assert!(kp.verify(b"hello", &sig).unwrap());
948 }
949
950 #[test]
951 fn ed25519_signing_object_has_a_wiping_owner_and_keeps_chain_bytes() {
952 fn stored(inner: &KeypairInner) -> Option<&Zeroizing<SigningKey>> {
955 match inner {
956 KeypairInner::Ed25519(secret) => Some(secret),
957 _ => None,
958 }
959 }
960 let keypair = Keypair::from_uri("//Alice", CRYPTO_ED25519).unwrap();
961 assert!(stored(&keypair.inner).is_some());
962
963 let seed = hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
964 .unwrap();
965 let keypair = Keypair::from_seed(&seed, CRYPTO_ED25519).unwrap();
966 assert_eq!(
967 hex::encode(keypair.public_key_bytes()),
968 "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
969 );
970 assert_eq!(
971 hex::encode(keypair.sign(b"").unwrap()),
972 "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
973 );
974 }
975
976 #[test]
977 fn zeroize_clears_every_signing_key_secret() {
978 use zeroize::Zeroize;
979 let mut key = SigningKey::from([7u8; 32]);
980 key.zeroize();
981 assert_eq!(<[u8; 32]>::from(key), [0u8; 32]);
984 }
985
986 #[test]
987 fn hard_derivation_keeps_substrate_vectors() {
988 let alice = Keypair::from_uri("//Alice", CRYPTO_ED25519).unwrap();
989 assert_eq!(
990 hex::encode(alice.public_key_bytes()),
991 "88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee"
992 );
993 let bob = Keypair::from_uri("//Bob", CRYPTO_ED25519).unwrap();
994 assert_eq!(
995 hex::encode(bob.public_key_bytes()),
996 "d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae69"
997 );
998 }
999}