use {
base64::{engine::general_purpose, Engine as _},
ed25519_dalek::SigningKey,
rand::RngCore as _,
thiserror::Error,
};
#[derive(Clone)]
pub struct Ed25519Keypair {
signing: SigningKey,
}
impl Ed25519Keypair {
pub fn generate() -> Self {
let mut sk = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut sk);
Self {
signing: SigningKey::from_bytes(&sk),
}
}
pub fn from_signing_key(signing: SigningKey) -> Self {
Self { signing }
}
pub fn from_private_key_hex_or_base64(raw: &str) -> Result<Self, KeyParseError> {
parse_ed25519_signing_key(raw).map(Self::from_signing_key)
}
pub fn signing_key(&self) -> &SigningKey {
&self.signing
}
pub fn private_key_bytes(&self) -> [u8; 32] {
self.signing.to_bytes()
}
pub fn public_key_bytes(&self) -> [u8; 32] {
self.signing.verifying_key().to_bytes()
}
pub fn private_key_hex(&self) -> String {
hex::encode(self.private_key_bytes())
}
pub fn public_key_hex(&self) -> String {
hex::encode(self.public_key_bytes())
}
}
#[derive(Debug, Error)]
pub enum KeyParseError {
#[error("invalid hex private key: {0}")]
InvalidHex(String),
#[error("invalid base64/base64url private key: {0}")]
InvalidBase64(String),
#[error("unsupported key scheme flag 0x{flag:02x} (expected 0x00 for ed25519)")]
UnsupportedSuiKeySchemeFlag { flag: u8 },
#[error(
"invalid private key length {len}, expected 32 bytes (raw ed25519) or 33 bytes (0x00 + key)"
)]
InvalidLength { len: usize },
}
pub fn parse_ed25519_signing_key(raw: &str) -> Result<SigningKey, KeyParseError> {
let raw = raw.trim();
let raw_no_0x = raw.strip_prefix("0x").unwrap_or(raw);
let looks_like_hex = raw.starts_with("0x")
|| ((raw_no_0x.len() == 64 || raw_no_0x.len() == 66)
&& raw_no_0x.chars().all(|c| c.is_ascii_hexdigit()));
if looks_like_hex {
let bytes = hex::decode(raw_no_0x).map_err(|e| KeyParseError::InvalidHex(e.to_string()))?;
return match bytes.len() {
32 => Ok(SigningKey::from_bytes(
&<[u8; 32]>::try_from(bytes.as_slice()).expect("length checked"),
)),
33 => signing_key_from_sui_flagged_bytes(
&<[u8; 33]>::try_from(bytes.as_slice()).expect("length checked"),
),
len => Err(KeyParseError::InvalidLength { len }),
};
}
let try_b64 = |engine: &general_purpose::GeneralPurpose| -> Option<Vec<u8>> {
engine.decode(raw.as_bytes()).ok()
};
let bytes = try_b64(&general_purpose::STANDARD)
.or_else(|| try_b64(&general_purpose::STANDARD_NO_PAD))
.or_else(|| try_b64(&general_purpose::URL_SAFE))
.or_else(|| try_b64(&general_purpose::URL_SAFE_NO_PAD))
.ok_or_else(|| {
KeyParseError::InvalidBase64("expected base64/base64url data".to_string())
})?;
match bytes.len() {
32 => Ok(SigningKey::from_bytes(
&<[u8; 32]>::try_from(bytes.as_slice()).expect("length checked"),
)),
33 => signing_key_from_sui_flagged_bytes(
&<[u8; 33]>::try_from(bytes.as_slice()).expect("length checked"),
),
len => Err(KeyParseError::InvalidLength { len }),
}
}
fn signing_key_from_sui_flagged_bytes(bytes: &[u8; 33]) -> Result<SigningKey, KeyParseError> {
const ED25519_FLAG: u8 = 0x00;
if bytes[0] != ED25519_FLAG {
return Err(KeyParseError::UnsupportedSuiKeySchemeFlag { flag: bytes[0] });
}
let mut sk = [0u8; 32];
sk.copy_from_slice(&bytes[1..]);
Ok(SigningKey::from_bytes(&sk))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_key_bytes(byte: u8) -> [u8; 32] {
[byte; 32]
}
#[test]
fn parse_hex_roundtrip() {
let keypair = Ed25519Keypair::generate();
let hex = keypair.private_key_hex();
let parsed = Ed25519Keypair::from_private_key_hex_or_base64(&hex).unwrap();
assert_eq!(parsed.public_key_bytes(), keypair.public_key_bytes());
}
#[test]
fn parse_base64_roundtrip() {
let bytes = make_key_bytes(7);
let b64 = general_purpose::STANDARD.encode(bytes);
let parsed = Ed25519Keypair::from_private_key_hex_or_base64(&b64).unwrap();
assert_eq!(parsed.private_key_bytes(), bytes);
}
#[test]
fn parse_sui_keytool_base64_format() {
let bytes = make_key_bytes(9);
let mut flagged = [0u8; 33];
flagged[0] = 0x00;
flagged[1..].copy_from_slice(&bytes);
let b64 = general_purpose::STANDARD.encode(flagged);
let parsed = Ed25519Keypair::from_private_key_hex_or_base64(&b64).unwrap();
assert_eq!(parsed.private_key_bytes(), bytes);
}
#[test]
fn parse_invalid_hex_reports_error() {
assert!(matches!(
Ed25519Keypair::from_private_key_hex_or_base64("0xzzzz"),
Err(KeyParseError::InvalidHex(_))
));
}
#[test]
fn parse_invalid_length_reports_error() {
let bytes = vec![0u8; 31];
let b64 = general_purpose::STANDARD.encode(bytes);
assert!(matches!(
Ed25519Keypair::from_private_key_hex_or_base64(&b64),
Err(KeyParseError::InvalidLength { len: 31 })
));
}
#[test]
fn parse_unsupported_sui_flag_reports_error() {
let mut flagged = [0u8; 33];
flagged[0] = 0x01;
let b64 = general_purpose::STANDARD.encode(flagged);
assert!(matches!(
Ed25519Keypair::from_private_key_hex_or_base64(&b64),
Err(KeyParseError::UnsupportedSuiKeySchemeFlag { flag: 0x01 })
));
}
}