#![allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
use ed25519_zebra::{SigningKey, VerificationKey};
#[cfg(feature = "host")]
use sodiumoxide::crypto::box_;
#[cfg(feature = "host")]
use sodiumoxide::crypto::sealedbox;
#[cfg(feature = "host")]
use sodiumoxide::crypto::sign::ed25519 as sign_ed25519;
use sp_core::crypto::{
AccountId32, CryptoType, DeriveError, DeriveJunction, Pair as PairT, SecretStringError,
Ss58Codec,
};
use sp_core::{ed25519, sr25519, ByteArray};
use zeroize::Zeroizing;
use crate::error::CoreError;
mod base58;
mod hashed;
pub use hashed::{verify_hashed_authorization, HashedKeypair};
mod falcon;
mod mldsa;
pub use mldsa::{verify_mldsa_authorization, MldsaKeypair, ROLE_TRANSACTION};
mod receiving;
pub use receiving::{
decode_hashed_receiving_address, decode_mldsa_receiving_address,
encode_hashed_receiving_address, encode_mldsa_receiving_address,
};
#[cfg(feature = "host")]
mod encrypted_json;
pub const CRYPTO_ED25519: u8 = 0;
pub const CRYPTO_SR25519: u8 = 1;
pub const CRYPTO_ECDSA: u8 = 2;
pub const ECDSA_PUBLIC_KEY_LEN: usize = sp_core::ecdsa::PUBLIC_KEY_SERIALIZED_SIZE;
pub const ED25519_SIGNATURE_LEN: usize = ed25519::SIGNATURE_SERIALIZED_SIZE;
pub const SR25519_SIGNATURE_LEN: usize = sr25519::SIGNATURE_SERIALIZED_SIZE;
pub const ECDSA_SIGNATURE_LEN: usize = sp_core::ecdsa::SIGNATURE_SERIALIZED_SIZE;
pub const CRYPTO_HASHED: u8 = 4;
pub const CRYPTO_HASHED_ED25519: u8 = 6;
pub const CRYPTO_MLDSA65: u8 = 7;
pub const CRYPTO_MLDSA65_FIXED: u8 = 8;
pub const CRYPTO_FALCON512: u8 = 9;
pub const CRYPTO_FALCON512_HASHED: u8 = 10;
pub const CRYPTO_FALCON1024: u8 = 11;
pub const CRYPTO_FALCON1024_HASHED: u8 = 12;
pub fn is_falcon_crypto(crypto_type: u8) -> bool {
matches!(
crypto_type,
CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED | CRYPTO_FALCON1024 | CRYPTO_FALCON1024_HASHED
)
}
pub fn is_pq_crypto(crypto_type: u8) -> bool {
is_mldsa_crypto(crypto_type) || is_falcon_crypto(crypto_type)
}
pub fn is_fixed_pq_crypto(crypto_type: u8) -> bool {
matches!(
crypto_type,
CRYPTO_MLDSA65_FIXED | CRYPTO_FALCON512 | CRYPTO_FALCON1024
)
}
pub fn pq_profile(crypto_type: u8) -> Option<u8> {
match crypto_type {
CRYPTO_MLDSA65 => Some(4),
CRYPTO_MLDSA65_FIXED => Some(5),
CRYPTO_FALCON512_HASHED => Some(6),
CRYPTO_FALCON512 => Some(7),
CRYPTO_FALCON1024_HASHED => Some(8),
CRYPTO_FALCON1024 => Some(9),
_ => None,
}
}
pub fn is_mldsa_crypto(crypto_type: u8) -> bool {
matches!(crypto_type, CRYPTO_MLDSA65 | CRYPTO_MLDSA65_FIXED)
}
pub fn is_hashed_crypto(crypto_type: u8) -> bool {
matches!(crypto_type, CRYPTO_HASHED | CRYPTO_HASHED_ED25519)
}
pub fn is_rotating_crypto(crypto_type: u8) -> bool {
is_hashed_crypto(crypto_type)
|| matches!(
crypto_type,
CRYPTO_MLDSA65 | CRYPTO_FALCON512_HASHED | CRYPTO_FALCON1024_HASHED
)
}
pub fn account_crypto_type(crypto_type: u8, mode: Option<&str>) -> Result<u8, CoreError> {
if is_falcon_crypto(crypto_type) {
let (fixed, hashed) = if matches!(crypto_type, CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED) {
(CRYPTO_FALCON512, CRYPTO_FALCON512_HASHED)
} else {
(CRYPTO_FALCON1024, CRYPTO_FALCON1024_HASHED)
};
return match mode {
None => Ok(crypto_type),
Some("hashed") => Ok(hashed),
Some("standard") => Ok(fixed),
_ => Err(crypto_err("account type must be standard or hashed")),
};
}
if is_mldsa_crypto(crypto_type) {
return match mode {
None => Ok(crypto_type),
Some("hashed") => Ok(CRYPTO_MLDSA65),
Some("standard") => Ok(CRYPTO_MLDSA65_FIXED),
_ => Err(crypto_err("account type must be standard or hashed")),
};
}
let Some(mode) = mode else {
return match crypto_type {
CRYPTO_SR25519 | CRYPTO_ED25519 | CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
Ok(crypto_type)
}
_ => Err(crypto_err("unsupported signing scheme")),
};
};
let scheme = match crypto_type {
CRYPTO_SR25519 | CRYPTO_HASHED => CRYPTO_SR25519,
CRYPTO_ED25519 | CRYPTO_HASHED_ED25519 => CRYPTO_ED25519,
_ => return Err(crypto_err("unsupported signing scheme")),
};
match (mode, scheme) {
("standard", CRYPTO_SR25519) => Ok(CRYPTO_SR25519),
("standard", CRYPTO_ED25519) => Ok(CRYPTO_ED25519),
("hashed", CRYPTO_SR25519) => Ok(CRYPTO_HASHED),
("hashed", CRYPTO_ED25519) => Ok(CRYPTO_HASHED_ED25519),
_ => Err(crypto_err("account type must be standard or hashed")),
}
}
pub const DEFAULT_SS58_FORMAT: u16 = 42;
fn crypto_err(msg: impl Into<String>) -> CoreError {
CoreError::Crypto(msg.into())
}
#[cfg(feature = "host")]
pub(crate) fn ensure_sodium() -> Result<(), CoreError> {
sodiumoxide::init().map_err(|_| crypto_err("failed to initialize libsodium"))?;
Ok(())
}
fn as_bytes<T: AsRef<[u8]>>(value: &T) -> Vec<u8> {
value.as_ref().to_vec()
}
pub fn public_key_from_ss58(ss58_address: &str) -> Result<[u8; 32], CoreError> {
let account = AccountId32::from_ss58check(ss58_address)
.map_err(|e| crypto_err(format!("invalid ss58 address: {e:?}")))?;
Ok(account.into())
}
pub fn ss58_from_public(public_key: [u8; 32], ss58_format: u16) -> String {
let mut buf = [0u8; 7 + 2 + 32 + 2];
buf[..7].copy_from_slice(b"SS58PRE");
let ident = ss58_format & 0b0011_1111_1111_1111;
let prefix_len = if ident < 64 {
buf[7] = ident as u8;
1
} else {
buf[7] = (((ident & 0b0000_0000_1111_1100) >> 2) as u8) | 0b0100_0000;
buf[8] = ((ident >> 8) as u8) | (((ident & 0b11) as u8) << 6);
2
};
buf[7 + prefix_len..7 + prefix_len + 32].copy_from_slice(&public_key);
let body_end = 7 + prefix_len + 32;
let checksum = sp_core::hashing::blake2_512(&buf[..body_end]);
buf[body_end] = checksum[0];
buf[body_end + 1] = checksum[1];
base58::base58_encode(&buf[7..body_end + 2])
}
pub fn account_id_from_ecdsa_public(public_key: &[u8]) -> Result<[u8; 32], CoreError> {
let compressed = <[u8; ECDSA_PUBLIC_KEY_LEN]>::try_from(public_key).map_err(|_| {
crypto_err(format!(
"ecdsa public key must be {ECDSA_PUBLIC_KEY_LEN} bytes (compressed secp256k1)"
))
})?;
if !matches!(compressed[0], 0x02 | 0x03) {
return Err(crypto_err(
"ecdsa public key must be compressed (0x02/0x03 prefix)",
));
}
Ok(sp_core::hashing::blake2_256(&compressed))
}
pub fn ss58_from_ecdsa_public(public_key: &[u8], ss58_format: u16) -> Result<String, CoreError> {
Ok(ss58_from_public(
account_id_from_ecdsa_public(public_key)?,
ss58_format,
))
}
fn verify_with_crypto(
crypto_type: u8,
public_key: &[u8; 32],
message: &[u8],
signature: &[u8],
) -> Result<bool, CoreError> {
match crypto_type {
CRYPTO_SR25519 => {
let public = sr25519::Public::from_raw(*public_key);
let sig = sr25519::Signature::from_slice(signature)
.map_err(|_| crypto_err("invalid sr25519 signature length"))?;
Ok(sr25519::Pair::verify(&sig, message, &public))
}
CRYPTO_ED25519 => {
let public = ed25519::Public::from_raw(*public_key);
let sig = ed25519::Signature::from_slice(signature)
.map_err(|_| crypto_err("invalid ed25519 signature length"))?;
Ok(ed25519::Pair::verify(&sig, message, &public))
}
CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => Ok(hashed::verify_message(
crypto_type,
public_key,
message,
signature,
)),
other => Err(crypto_err(format!("unknown crypto type {other}"))),
}
}
#[cfg(feature = "host")]
fn ed25519_to_x25519_pk(public_key: &[u8; 32]) -> Result<box_::PublicKey, CoreError> {
ensure_sodium()?;
let ed25519_pk = sign_ed25519::PublicKey::from_slice(public_key)
.ok_or_else(|| crypto_err("invalid ed25519 public key"))?;
sign_ed25519::to_curve25519_pk(&ed25519_pk)
.map_err(|_| crypto_err("failed to convert ed25519 public key to x25519"))
}
fn ed25519_seed(pair: &Zeroizing<SigningKey>) -> &[u8] {
let pair: &SigningKey = pair;
pair.as_ref()
}
fn ed25519_public(pair: &Zeroizing<SigningKey>) -> [u8; 32] {
let pair: &SigningKey = pair;
VerificationKey::from(pair).into()
}
#[cfg(feature = "host")]
fn ed25519_x25519_from_pair(
pair: &Zeroizing<SigningKey>,
) -> Result<(box_::PublicKey, box_::SecretKey), CoreError> {
ensure_sodium()?;
let seed = sign_ed25519::Seed::from_slice(ed25519_seed(pair))
.ok_or_else(|| crypto_err("failed to derive x25519 keypair for decryption"))?;
let (pk, sk) = sign_ed25519::keypair_from_seed(&seed);
let x25519_pk = sign_ed25519::to_curve25519_pk(&pk)
.map_err(|_| crypto_err("failed to derive x25519 keypair for decryption"))?;
let x25519_sk = sign_ed25519::to_curve25519_sk(&sk)
.map_err(|_| crypto_err("failed to derive x25519 keypair for decryption"))?;
Ok((x25519_pk, x25519_sk))
}
struct WipingSeed(Zeroizing<[u8; 32]>);
impl Default for WipingSeed {
fn default() -> Self {
Self(Zeroizing::new([0; 32]))
}
}
impl Clone for WipingSeed {
fn clone(&self) -> Self {
let mut clone = Self::default();
clone.as_mut().copy_from_slice(self.as_ref());
clone
}
}
impl AsRef<[u8]> for WipingSeed {
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}
impl AsMut<[u8]> for WipingSeed {
fn as_mut(&mut self) -> &mut [u8] {
&mut self.0[..]
}
}
const HDKD_PREIMAGE_LEN: usize = 1 + 11 + 32 + 32;
struct WipingEd25519Pair {
secret: Zeroizing<SigningKey>,
}
impl WipingEd25519Pair {
fn into_secret(self) -> Zeroizing<SigningKey> {
self.secret
}
fn seed(&self) -> WipingSeed {
let mut seed = WipingSeed::default();
seed.as_mut().copy_from_slice(ed25519_seed(&self.secret));
seed
}
}
impl CryptoType for WipingEd25519Pair {
type Pair = Self;
}
impl PairT for WipingEd25519Pair {
type Public = ed25519::Public;
type Seed = WipingSeed;
type Signature = ed25519::Signature;
fn from_seed_slice(seed: &[u8]) -> Result<Self, SecretStringError> {
let secret =
SigningKey::try_from(seed).map_err(|_| SecretStringError::InvalidSeedLength)?;
Ok(Self {
secret: Zeroizing::new(secret),
})
}
fn derive<Iter: Iterator<Item = DeriveJunction>>(
&self,
path: Iter,
_seed: Option<Self::Seed>,
) -> Result<(Self, Option<Self::Seed>), DeriveError> {
let mut seed = self.seed();
for junction in path {
match junction {
DeriveJunction::Soft(_) => return Err(DeriveError::SoftKeyInPath),
DeriveJunction::Hard(chain_code) => {
let mut preimage = Zeroizing::new([0u8; HDKD_PREIMAGE_LEN]);
preimage[0] = 0x2c;
preimage[1..12].copy_from_slice(b"Ed25519HDKD");
preimage[12..44].copy_from_slice(&seed.0[..]);
preimage[44..].copy_from_slice(&chain_code);
let derived = Zeroizing::new(sp_core::hashing::blake2_256(&preimage[..]));
seed.0.copy_from_slice(&derived[..]);
}
}
}
Ok((Self::from_seed(&seed), Some(seed)))
}
fn public(&self) -> Self::Public {
ed25519::Public::from_raw(ed25519_public(&self.secret))
}
fn sign(&self, message: &[u8]) -> Self::Signature {
Self::Signature::from_raw(self.secret.sign(message).to_bytes())
}
fn verify<M: AsRef<[u8]>>(
signature: &Self::Signature,
message: M,
public: &Self::Public,
) -> bool {
ed25519::Pair::verify(signature, message, public)
}
fn to_raw_vec(&self) -> Vec<u8> {
ed25519_seed(&self.secret).to_vec()
}
}
#[allow(clippy::large_enum_variant)]
pub enum KeypairInner {
Ed25519(Zeroizing<SigningKey>),
Sr25519(sr25519::Pair),
Hashed(HashedKeypair),
Mldsa(MldsaKeypair),
PublicOnly {
public_key: [u8; 32],
crypto_type: u8,
},
}
impl KeypairInner {
fn private_key_bytes(&self) -> Option<Vec<u8>> {
match self {
KeypairInner::Ed25519(pair) => Some(ed25519_seed(pair).to_vec()),
KeypairInner::Sr25519(pair) => Some(pair.to_raw_vec()),
KeypairInner::Hashed(pair) => pair.master_seed().map(|seed| seed.to_vec()),
KeypairInner::Mldsa(pair) => pair.master_seed().map(|seed| seed.to_vec()),
KeypairInner::PublicOnly { .. } => None,
}
}
}
pub struct Keypair {
inner: KeypairInner,
ss58_format: u16,
mnemonic: Option<Zeroizing<String>>,
seed: Option<Zeroizing<Vec<u8>>>,
}
impl Keypair {
pub fn account_type(&self) -> &'static str {
if is_rotating_crypto(self.crypto_type()) {
"hashed"
} else {
"standard"
}
}
pub fn signing_scheme(&self) -> &'static str {
match self.crypto_type() {
CRYPTO_HASHED | CRYPTO_SR25519 => "sr25519",
CRYPTO_HASHED_ED25519 | CRYPTO_ED25519 => "ed25519",
CRYPTO_MLDSA65 | CRYPTO_MLDSA65_FIXED => "mldsa65",
CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED => "falcon512",
CRYPTO_FALCON1024 | CRYPTO_FALCON1024_HASHED => "falcon1024",
_ => "unknown",
}
}
pub fn new(
ss58_address: Option<&str>,
public_key: Option<&[u8]>,
crypto_type: u8,
ss58_format: u16,
) -> Result<Self, CoreError> {
match crypto_type {
CRYPTO_SR25519
| CRYPTO_ED25519
| CRYPTO_HASHED
| CRYPTO_HASHED_ED25519
| CRYPTO_MLDSA65
| CRYPTO_MLDSA65_FIXED
| CRYPTO_FALCON512
| CRYPTO_FALCON512_HASHED
| CRYPTO_FALCON1024
| CRYPTO_FALCON1024_HASHED => {}
other => return Err(crypto_err(format!("unknown crypto type {other}"))),
}
let public_key = match (ss58_address, public_key) {
(Some(addr), None) => public_key_from_ss58(addr)?,
(None, Some(pk)) => <[u8; 32]>::try_from(pk)
.map_err(|_| crypto_err("public key must be exactly 32 bytes"))?,
(Some(addr), Some(pk)) => {
let from_addr = public_key_from_ss58(addr)?;
let from_pk = <[u8; 32]>::try_from(pk)
.map_err(|_| crypto_err("public key must be exactly 32 bytes"))?;
if from_addr != from_pk {
return Err(crypto_err(
"ss58 address and public key refer to different accounts",
));
}
from_pk
}
(None, None) => {
return Err(crypto_err(
"no ss58 formatted address or public key provided",
));
}
};
Ok(Self {
inner: KeypairInner::PublicOnly {
public_key,
crypto_type,
},
ss58_format,
mnemonic: None,
seed: None,
})
}
pub fn from_mnemonic(
mnemonic: &str,
crypto_type: u8,
password: Option<&str>,
) -> Result<Self, CoreError> {
let (inner, seed) = match crypto_type {
crypto if is_pq_crypto(crypto) => {
let phrase = bip39::Mnemonic::parse_in(bip39::Language::English, mnemonic)
.map_err(|_| crypto_err("invalid post-quantum mnemonic"))?;
if phrase.word_count() != 24 {
return Err(crypto_err(
"post-quantum wallets require a 24-word mnemonic with 256-bit entropy",
));
}
let expanded = Zeroizing::new(phrase.to_seed(password.unwrap_or("")));
let seed = Zeroizing::new(expanded[..32].to_vec());
(
KeypairInner::Mldsa(MldsaKeypair::from_seed(&seed, crypto_type)?),
seed,
)
}
CRYPTO_SR25519 => {
let (pair, seed) = sr25519::Pair::from_phrase(mnemonic, password)
.map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
(KeypairInner::Sr25519(pair), Zeroizing::new(seed.to_vec()))
}
CRYPTO_ED25519 => {
let (pair, seed) = WipingEd25519Pair::from_phrase(mnemonic, password)
.map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
(
KeypairInner::Ed25519(pair.into_secret()),
Zeroizing::new(seed.as_ref().to_vec()),
)
}
CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
let (_, seed) = sr25519::Pair::from_phrase(mnemonic, password)
.map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
(
KeypairInner::Hashed(HashedKeypair::from_seed(&seed, crypto_type)?),
Zeroizing::new(seed.to_vec()),
)
}
other => return Err(crypto_err(format!("unknown crypto type {other}"))),
};
Ok(Self {
inner,
ss58_format: DEFAULT_SS58_FORMAT,
mnemonic: password
.is_none()
.then(|| Zeroizing::new(mnemonic.to_string())),
seed: Some(seed),
})
}
pub fn from_seed(seed: &[u8], crypto_type: u8) -> Result<Self, CoreError> {
let inner = match crypto_type {
crypto if is_pq_crypto(crypto) => {
KeypairInner::Mldsa(MldsaKeypair::from_seed(seed, crypto_type)?)
}
CRYPTO_SR25519 => KeypairInner::Sr25519(
sr25519::Pair::from_seed_slice(seed)
.map_err(|e| crypto_err(format!("invalid seed: {e:?}")))?,
),
CRYPTO_ED25519 => KeypairInner::Ed25519(
WipingEd25519Pair::from_seed_slice(seed)
.map_err(|e| crypto_err(format!("invalid seed: {e:?}")))?
.into_secret(),
),
CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
KeypairInner::Hashed(HashedKeypair::from_seed(seed, crypto_type)?)
}
other => return Err(crypto_err(format!("unknown crypto type {other}"))),
};
Ok(Self {
inner,
ss58_format: DEFAULT_SS58_FORMAT,
mnemonic: None,
seed: Some(Zeroizing::new(seed.to_vec())),
})
}
pub fn from_uri(uri: &str, crypto_type: u8) -> Result<Self, CoreError> {
if is_hashed_crypto(crypto_type) || is_pq_crypto(crypto_type) {
return Err(crypto_err(
"hashed and post-quantum wallets require a mnemonic or a 32-byte master seed; secret URIs are unsupported",
));
}
let inner = match crypto_type {
CRYPTO_SR25519 => KeypairInner::Sr25519(
sr25519::Pair::from_string(uri, None)
.map_err(|e| crypto_err(format!("invalid secret uri: {e:?}")))?,
),
CRYPTO_ED25519 => KeypairInner::Ed25519(
WipingEd25519Pair::from_string(uri, None)
.map_err(|e| crypto_err(format!("invalid secret uri: {e:?}")))?
.into_secret(),
),
other => return Err(crypto_err(format!("unknown crypto type {other}"))),
};
Ok(Self {
inner,
ss58_format: DEFAULT_SS58_FORMAT,
mnemonic: None,
seed: None,
})
}
pub fn from_private_key(private_key: &str, crypto_type: u8) -> Result<Self, CoreError> {
let private_key_vec = Zeroizing::new(
hex::decode(private_key.trim_start_matches("0x"))
.map_err(|_| crypto_err("invalid private_key hex string"))?,
);
if is_hashed_crypto(crypto_type) || is_pq_crypto(crypto_type) {
return Self::from_seed(&private_key_vec, crypto_type);
}
let inner = match crypto_type {
CRYPTO_SR25519 => {
KeypairInner::Sr25519(sr25519::Pair::from_seed_slice(&private_key_vec).map_err(
|error| crypto_err(format!("invalid sr25519 private key: {error:?}")),
)?)
}
CRYPTO_ED25519 => {
let seed = if private_key_vec.len() >= 32 {
&private_key_vec[..32]
} else {
return Err(crypto_err("ed25519 private key must be at least 32 bytes"));
};
KeypairInner::Ed25519(
WipingEd25519Pair::from_seed_slice(seed)
.map_err(|error| {
crypto_err(format!("invalid ed25519 private key: {error:?}"))
})?
.into_secret(),
)
}
other => return Err(crypto_err(format!("unknown crypto type {other}"))),
};
Ok(Self {
inner,
ss58_format: DEFAULT_SS58_FORMAT,
mnemonic: None,
seed: None,
})
}
pub fn generate_mnemonic(n_words: usize) -> Result<String, CoreError> {
use bip39::{Language, Mnemonic};
if !matches!(n_words, 12 | 15 | 18 | 21 | 24) {
return Err(crypto_err(format!(
"unsupported mnemonic length {n_words}; expected 12, 15, 18, 21, or 24"
)));
}
Mnemonic::generate_in(Language::English, n_words)
.map(|mnemonic| mnemonic.to_string())
.map_err(|error| crypto_err(format!("failed to generate mnemonic: {error}")))
}
#[cfg(feature = "host")]
pub fn from_encrypted_json(json_data: &str, passphrase: &str) -> Result<Self, CoreError> {
encrypted_json::create_from_encrypted_json(json_data, passphrase)
}
pub fn crypto_type(&self) -> u8 {
match &self.inner {
KeypairInner::Ed25519(_) => CRYPTO_ED25519,
KeypairInner::Sr25519(_) => CRYPTO_SR25519,
KeypairInner::Hashed(key) => key.crypto_type(),
KeypairInner::Mldsa(key) => match key.descriptor.algorithm {
4 => CRYPTO_MLDSA65,
5 => CRYPTO_MLDSA65_FIXED,
6 => CRYPTO_FALCON512_HASHED,
7 => CRYPTO_FALCON512,
8 => CRYPTO_FALCON1024_HASHED,
9 => CRYPTO_FALCON1024,
_ => unreachable!("validated post-quantum profile"),
},
KeypairInner::PublicOnly { crypto_type, .. } => *crypto_type,
}
}
pub fn public_key_bytes(&self) -> [u8; 32] {
match &self.inner {
KeypairInner::Ed25519(pair) => ed25519_public(pair),
KeypairInner::Sr25519(pair) => pair.public().0,
KeypairInner::Hashed(pair) => pair.account_id(),
KeypairInner::Mldsa(pair) => pair.account_id(),
KeypairInner::PublicOnly { public_key, .. } => *public_key,
}
}
pub fn ss58_address(&self) -> String {
ss58_from_public(self.public_key_bytes(), self.ss58_format)
}
pub fn ss58_format(&self) -> u16 {
self.ss58_format
}
pub fn private_key_bytes(&self) -> Option<Vec<u8>> {
self.inner.private_key_bytes()
}
pub fn mnemonic(&self) -> Option<&str> {
self.mnemonic.as_deref().map(String::as_str)
}
pub fn seed_bytes(&self) -> Option<&[u8]> {
self.seed.as_deref().map(Vec::as_slice)
}
#[cfg(feature = "host")]
pub(crate) fn from_inner(inner: KeypairInner, ss58_format: u16) -> Self {
Self {
inner,
ss58_format,
mnemonic: None,
seed: None,
}
}
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
match &self.inner {
KeypairInner::Ed25519(pair) => Ok(pair.sign(message).to_bytes().to_vec()),
KeypairInner::Sr25519(pair) => Ok(as_bytes(&pair.sign(message))),
KeypairInner::Hashed(_) => self.sign_hashed_message(message),
KeypairInner::Mldsa(_) => Err(crypto_err(
"post-quantum arbitrary message signing is not supported",
)),
KeypairInner::PublicOnly { .. } => {
Err(crypto_err("no private key set to create signatures"))
}
}
}
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, CoreError> {
let public_key = self.public_key_bytes();
let crypto_type = self.crypto_type();
if verify_with_crypto(crypto_type, &public_key, message, signature)? {
return Ok(true);
}
let wrapped = [b"<Bytes>", message, b"</Bytes>"].concat();
verify_with_crypto(crypto_type, &public_key, &wrapped, signature)
}
#[cfg(feature = "host")]
pub fn encrypt(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
if self.crypto_type() != CRYPTO_ED25519 {
return Err(crypto_err(
"encrypt/decrypt is only supported for ed25519 keypairs",
));
}
ensure_sodium()?;
let x25519_pk = ed25519_to_x25519_pk(&self.public_key_bytes())?;
Ok(sealedbox::seal(message, &x25519_pk))
}
#[cfg(feature = "host")]
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CoreError> {
if self.crypto_type() != CRYPTO_ED25519 {
return Err(crypto_err(
"encrypt/decrypt is only supported for ed25519 keypairs",
));
}
let KeypairInner::Ed25519(pair) = &self.inner else {
return Err(crypto_err(
"decryption requires a keypair with a private key",
));
};
ensure_sodium()?;
let (x25519_pk, x25519_sk) = ed25519_x25519_from_pair(pair)?;
sealedbox::open(ciphertext, &x25519_pk, &x25519_sk)
.map_err(|_| crypto_err("decryption failed: invalid ciphertext or wrong key"))
}
#[cfg(feature = "host")]
pub fn encrypt_for(
ss58_address: &str,
message: &[u8],
crypto_type: u8,
) -> Result<Vec<u8>, CoreError> {
let recipient = Self::new(Some(ss58_address), None, crypto_type, DEFAULT_SS58_FORMAT)?;
recipient.encrypt(message)
}
}
pub fn verify_strict(
message: &[u8],
signature: &[u8],
ss58_address: &str,
crypto_type: u8,
) -> Result<bool, CoreError> {
let public_key = public_key_from_ss58(ss58_address)?;
verify_with_crypto(crypto_type, &public_key, message, signature)
}
pub fn verify(
message: &[u8],
signature: &[u8],
ss58_address: &str,
crypto_type: u8,
) -> Result<bool, CoreError> {
let public_key = public_key_from_ss58(ss58_address)?;
if verify_with_crypto(crypto_type, &public_key, message, signature)? {
return Ok(true);
}
let wrapped = [b"<Bytes>", message, b"</Bytes>"].concat();
verify_with_crypto(crypto_type, &public_key, &wrapped, signature)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use sp_core::crypto::Ss58AddressFormat;
use super::*;
#[test]
fn ss58_rendering_matches_sp_core() {
let mut state = 0x243f_6a88_85a3_08d3u64;
for format in [0u16, 2, 42, 63, 64, 255, 4096, 16383] {
for i in 0..50u32 {
let mut key = [0u8; 32];
key[..4].copy_from_slice(&i.to_le_bytes());
for b in &mut key[4..] {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
*b = (state >> 33) as u8;
}
assert_eq!(
ss58_from_public(key, format),
AccountId32::from(key)
.to_ss58check_with_version(Ss58AddressFormat::custom(format)),
"diverged for format {format}"
);
}
}
}
#[test]
fn alice_uri_matches_known_address() {
let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
assert_eq!(
kp.ss58_address(),
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
);
}
#[test]
fn public_only_from_ss58_matches_full_key() {
let full = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
let public = Keypair::new(Some(&full.ss58_address()), None, CRYPTO_SR25519, 42).unwrap();
assert_eq!(public.ss58_address(), full.ss58_address());
assert_eq!(public.public_key_bytes(), full.public_key_bytes());
}
#[test]
fn sr25519_verify_roundtrip() {
let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
let message = b"hello";
let sig = kp.sign(message).unwrap();
assert!(kp.verify(message, &sig).unwrap());
}
#[test]
fn ecdsa_ss58_matches_sp_core() {
use sp_core::ecdsa;
let pair = ecdsa::Pair::from_string("//Alice", None).unwrap();
let public = pair.public();
let compressed: &[u8] = public.as_ref();
assert_eq!(compressed.len(), ECDSA_PUBLIC_KEY_LEN);
let expected = AccountId32::from(sp_core::hashing::blake2_256(compressed));
let account = account_id_from_ecdsa_public(compressed).unwrap();
assert_eq!(account, <[u8; 32]>::from(expected.clone()));
for format in [0u16, 42, 4096] {
assert_eq!(
ss58_from_ecdsa_public(compressed, format).unwrap(),
expected.to_ss58check_with_version(Ss58AddressFormat::custom(format))
);
}
}
#[test]
fn ecdsa_ss58_rejects_non_compressed_keys() {
assert!(account_id_from_ecdsa_public(&[0x02; 32]).is_err());
assert!(account_id_from_ecdsa_public(&[0x04; 33]).is_err());
assert!(account_id_from_ecdsa_public(&[0x04; 65]).is_err());
}
#[test]
fn sr25519_verify_bytes_wrapping() {
let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
let sig = kp.sign(b"<Bytes>hello</Bytes>").unwrap();
assert!(kp.verify(b"hello", &sig).unwrap());
}
#[test]
fn ed25519_signing_object_has_a_wiping_owner_and_keeps_chain_bytes() {
fn stored(inner: &KeypairInner) -> Option<&Zeroizing<SigningKey>> {
match inner {
KeypairInner::Ed25519(secret) => Some(secret),
_ => None,
}
}
let keypair = Keypair::from_uri("//Alice", CRYPTO_ED25519).unwrap();
assert!(stored(&keypair.inner).is_some());
let seed = hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
.unwrap();
let keypair = Keypair::from_seed(&seed, CRYPTO_ED25519).unwrap();
assert_eq!(
hex::encode(keypair.public_key_bytes()),
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
);
assert_eq!(
hex::encode(keypair.sign(b"").unwrap()),
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
);
}
#[test]
fn zeroize_clears_every_signing_key_secret() {
use zeroize::Zeroize;
let mut key = SigningKey::from([7u8; 32]);
key.zeroize();
assert_eq!(<[u8; 32]>::from(key), [0u8; 32]);
}
#[test]
fn hard_derivation_keeps_substrate_vectors() {
let alice = Keypair::from_uri("//Alice", CRYPTO_ED25519).unwrap();
assert_eq!(
hex::encode(alice.public_key_bytes()),
"88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee"
);
let bob = Keypair::from_uri("//Bob", CRYPTO_ED25519).unwrap();
assert_eq!(
hex::encode(bob.public_key_bytes()),
"d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae69"
);
}
}