#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{
error,
io::{Read, Write},
path::Path,
};
use ed25519_dalek::Signer as DalekSigner;
use rand::{rngs::OsRng, CryptoRng, RngCore};
use rialo_s_pubkey::Pubkey;
use rialo_s_seed_phrase::generate_seed_from_seed_phrase_and_passphrase;
use rialo_s_signature::Signature;
use rialo_s_signer::{EncodableKey, EncodableKeypair, Signer, SignerError};
#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use wasm_bindgen::prelude::*;
#[cfg(feature = "seed-derivable")]
pub mod seed_derivable;
pub mod signable;
const KEYPAIR_LENGTH: usize = 64;
#[cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), wasm_bindgen)]
#[derive(Debug)]
pub struct Keypair(ed25519_dalek::SigningKey);
impl Keypair {
pub const SECRET_KEY_LENGTH: usize = 32;
pub fn generate<R>(csprng: &mut R) -> Self
where
R: CryptoRng + RngCore,
{
Self(ed25519_dalek::SigningKey::generate(csprng))
}
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let mut rng = OsRng;
Self::generate(&mut rng)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
let bytes: &[u8; KEYPAIR_LENGTH] = bytes
.try_into()
.map_err(ed25519_dalek::SignatureError::from_source)?;
ed25519_dalek::SigningKey::from_keypair_bytes(bytes).map(Self)
}
pub fn to_bytes(&self) -> [u8; 64] {
self.0.to_keypair_bytes()
}
pub fn from_base58_string(s: &str) -> Self {
Self::try_from_base58_string(s).unwrap()
}
pub fn try_from_base58_string(s: &str) -> Result<Self, TryFromBase58Error> {
let mut buf = [0u8; KEYPAIR_LENGTH];
bs58::decode(s).onto(&mut buf)?;
Ok(Self::from_bytes(&buf)?)
}
pub fn to_base58_string(&self) -> String {
bs58::encode(&self.to_bytes()).into_string()
}
pub fn secret(&self) -> &ed25519_dalek::SigningKey {
&self.0
}
pub fn insecure_clone(&self) -> Self {
Self(ed25519_dalek::SigningKey::from_bytes(&self.0.to_bytes()))
}
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#[allow(non_snake_case)]
#[wasm_bindgen]
impl Keypair {
#[wasm_bindgen(constructor)]
pub fn constructor() -> Keypair {
Keypair::new()
}
pub fn toBytes(&self) -> Box<[u8]> {
self.to_bytes().into()
}
pub fn fromBytes(bytes: &[u8]) -> Result<Keypair, JsValue> {
Keypair::from_bytes(bytes).map_err(|e| e.to_string().into())
}
#[wasm_bindgen(js_name = pubkey)]
pub fn js_pubkey(&self) -> Pubkey {
self.pubkey()
}
}
impl From<ed25519_dalek::SigningKey> for Keypair {
fn from(value: ed25519_dalek::SigningKey) -> Self {
Self(value)
}
}
#[cfg(test)]
static_assertions::const_assert_eq!(Keypair::SECRET_KEY_LENGTH, ed25519_dalek::SECRET_KEY_LENGTH);
impl Signer for Keypair {
#[inline]
fn pubkey(&self) -> Pubkey {
Pubkey::from(self.0.verifying_key().to_bytes())
}
fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
Ok(self.pubkey())
}
fn sign_message(&self, message: &[u8]) -> Signature {
Signature::from(DalekSigner::sign(&self.0, message).to_bytes())
}
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
Ok(self.sign_message(message))
}
fn is_interactive(&self) -> bool {
false
}
}
impl<T> PartialEq<T> for Keypair
where
T: Signer,
{
fn eq(&self, other: &T) -> bool {
self.pubkey() == other.pubkey()
}
}
impl EncodableKey for Keypair {
fn read<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
read_keypair(reader)
}
fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
write_keypair(self, writer)
}
}
impl EncodableKeypair for Keypair {
type Pubkey = Pubkey;
fn encodable_pubkey(&self) -> Self::Pubkey {
self.pubkey()
}
}
#[cfg(feature = "serde")]
impl Serialize for Keypair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.to_base58_string().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Keypair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let base58_str = String::deserialize(deserializer)?;
Keypair::try_from_base58_string(&base58_str)
.map_err(|error| <D::Error as de::Error>::custom(format!("invalid `Keypair`: {error}")))
}
}
pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> {
let mut buffer = String::new();
reader.read_to_string(&mut buffer)?;
let trimmed = buffer.trim();
if !trimmed.starts_with('[') {
match Keypair::try_from_base58_string(trimmed) {
Ok(keypair) => return Ok(keypair),
Err(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Input is not valid base58 or JSON array format",
)
.into());
}
}
}
if !trimmed.ends_with(']') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Input must be a JSON array (starting with '[' and ending with ']')",
)
.into());
}
#[allow(clippy::arithmetic_side_effects)]
let contents = &trimmed[1..trimmed.len() - 1];
let elements_vec: Vec<&str> = contents.split(',').map(|s| s.trim()).collect();
let len = elements_vec.len();
let elements: [&str; KEYPAIR_LENGTH] = elements_vec.try_into().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Expected {} elements, found {}", KEYPAIR_LENGTH, len),
)
})?;
let mut out = [0u8; KEYPAIR_LENGTH];
for (idx, element) in elements.into_iter().enumerate() {
let parsed: u8 = element.parse()?;
out[idx] = parsed;
}
Keypair::from_bytes(&out).map_err(|e| std::io::Error::other(e.to_string()).into())
}
pub fn read_keypair_file<F: AsRef<Path>>(path: F) -> Result<Keypair, Box<dyn error::Error>> {
Keypair::read_from_file(path)
}
pub fn write_keypair<W: Write>(
keypair: &Keypair,
writer: &mut W,
) -> Result<String, Box<dyn error::Error>> {
let keypair_bytes = keypair.to_bytes();
let mut result = Vec::with_capacity(64 * 4 + 2);
result.push(b'[');
for (i, &num) in keypair_bytes.iter().enumerate() {
if i > 0 {
result.push(b','); }
let num_str = num.to_string();
result.extend_from_slice(num_str.as_bytes());
}
result.push(b']'); writer.write_all(&result)?;
let as_string = String::from_utf8(result)?;
Ok(as_string)
}
pub fn write_keypair_file<F: AsRef<Path>>(
keypair: &Keypair,
outfile: F,
) -> Result<String, Box<dyn error::Error>> {
keypair.write_to_file(outfile)
}
pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>> {
if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH {
return Err("Seed is too short".into());
}
let secret = ed25519_dalek::SigningKey::from_bytes(
seed[..ed25519_dalek::SECRET_KEY_LENGTH].try_into().unwrap(),
);
Ok(Keypair(secret))
}
pub fn keypair_from_seed_phrase_and_passphrase(
seed_phrase: &str,
passphrase: &str,
) -> Result<Keypair, Box<dyn std::error::Error>> {
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
seed_phrase,
passphrase,
))
}
#[derive(Debug, Error)]
pub enum TryFromBase58Error {
#[error("Failed to decode base58 string")]
IncorrectEncoding(#[from] bs58::decode::Error),
#[error("Decoded base58 string does not represent a valid keypair")]
InvalidKeypair(#[from] ed25519_dalek::SignatureError),
}
#[cfg(test)]
mod tests {
use std::{
fs::{self, File},
mem,
};
use assert_matches::assert_matches;
use bip39::{Language, Mnemonic, MnemonicType, Seed};
use rialo_s_signer::unique_signers;
use super::*;
fn tmp_file_path(name: &str) -> String {
use std::env;
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
let keypair = Keypair::new();
format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey())
}
#[test]
fn test_write_keypair_file() {
let outfile = tmp_file_path("test_write_keypair_file.json");
let serialized_keypair = write_keypair_file(&Keypair::new(), &outfile).unwrap();
let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
assert!(Path::new(&outfile).exists());
assert_eq!(
keypair_vec,
read_keypair_file(&outfile).unwrap().to_bytes().to_vec()
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
File::open(&outfile)
.expect("open")
.metadata()
.expect("metadata")
.permissions()
.mode()
& 0o777,
0o600
);
}
assert_eq!(
read_keypair_file(&outfile).unwrap().pubkey().as_ref().len(),
mem::size_of::<Pubkey>()
);
fs::remove_file(&outfile).unwrap();
assert!(!Path::new(&outfile).exists());
}
#[test]
fn test_invalid_base58_string() {
let result = Keypair::try_from_base58_string("l");
assert_matches!(result, Err(TryFromBase58Error::IncorrectEncoding(_)));
}
#[test]
fn test_base58_string_of_invalid_keypair() {
let result = Keypair::try_from_base58_string("1111");
assert_matches!(result, Err(TryFromBase58Error::InvalidKeypair(_)));
}
#[cfg(feature = "serde")]
#[test]
fn test_serialization_roundtrip() {
let original = Keypair::new();
let json = serde_json::to_string(&original).expect("Failed to serialize `Keypair`");
let recovered =
serde_json::from_str::<Keypair>(&json).expect("Failed to deserialize `Keypair`");
assert_eq!(original.to_bytes(), recovered.to_bytes());
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialization_from_wrong_json_representation() {
let keypair_base58 = Keypair::new().to_base58_string();
let json_object = format!(r#"{{ "key": "{keypair_base58}" }}"#);
let error = serde_json::from_str::<Keypair>(&json_object)
.expect_err("Failed to report error for incorrect serialized representation");
assert!(error.to_string().contains("expected a string"));
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialization_from_empty_string() {
let json = "\"\"";
let error = serde_json::from_str::<Keypair>(json)
.expect_err("Failed to report error for empty string");
assert_eq!(
error.to_string(),
"invalid `Keypair`: Decoded base58 string does not represent a valid keypair"
);
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialization_from_non_base58_string() {
let json = "\"ll\"";
let error = serde_json::from_str::<Keypair>(json)
.expect_err("Failed to report error for invalid base58 string");
assert_eq!(
error.to_string(),
"invalid `Keypair`: Failed to decode base58 string"
);
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialization_of_invalid_keypair() {
let invalid_keypair_bytes = [0_u8; 64];
let invalid_keypair_base58 = bs58::encode(&invalid_keypair_bytes).into_string();
let json = format!("\"{invalid_keypair_base58}\"");
let error = serde_json::from_str::<Keypair>(&json)
.expect_err("Failed to report error for invalid `Keypair`");
assert_eq!(
error.to_string(),
"invalid `Keypair`: Decoded base58 string does not represent a valid keypair"
);
}
#[test]
fn test_write_keypair_file_overwrite_ok() {
let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json");
write_keypair_file(&Keypair::new(), &outfile).unwrap();
write_keypair_file(&Keypair::new(), &outfile).unwrap();
}
#[test]
fn test_write_keypair_file_truncate() {
let outfile = tmp_file_path("test_write_keypair_file_truncate.json");
write_keypair_file(&Keypair::new(), &outfile).unwrap();
read_keypair_file(&outfile).unwrap();
{
let mut f = File::create(&outfile).unwrap();
f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes())
.unwrap();
}
write_keypair_file(&Keypair::new(), &outfile).unwrap();
read_keypair_file(&outfile).unwrap();
}
#[test]
fn test_keypair_from_seed() {
let good_seed = vec![0; 32];
assert!(keypair_from_seed(&good_seed).is_ok());
let too_short_seed = vec![0; 31];
assert!(keypair_from_seed(&too_short_seed).is_err());
}
#[test]
fn test_keypair() {
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
let pubkey = keypair.pubkey();
let data = [1u8];
let sig = keypair.sign_message(&data);
assert_eq!(keypair.try_pubkey().unwrap(), pubkey);
assert_eq!(keypair.pubkey(), pubkey);
assert_eq!(keypair.try_sign_message(&data).unwrap(), sig);
assert_eq!(keypair.sign_message(&data), sig);
let keypair2 = keypair_from_seed(&[0u8; 32]).unwrap();
assert_eq!(keypair, keypair2);
}
fn pubkeys(signers: &[&dyn Signer]) -> Vec<Pubkey> {
signers.iter().map(|x| x.pubkey()).collect()
}
#[test]
fn test_unique_signers() {
let alice = Keypair::new();
let bob = Keypair::new();
assert_eq!(
pubkeys(&unique_signers(vec![&alice, &bob, &alice])),
pubkeys(&[&alice, &bob])
);
}
#[test]
fn test_containers() {
use std::{rc::Rc, sync::Arc};
struct Foo<S: Signer> {
#[allow(unused)]
signer: S,
}
fn foo(_s: impl Signer) {}
let _arc_signer = Foo {
signer: Arc::new(Keypair::new()),
};
foo(Arc::new(Keypair::new()));
let _rc_signer = Foo {
signer: Rc::new(Keypair::new()),
};
foo(Rc::new(Keypair::new()));
let _ref_signer = Foo {
signer: &Keypair::new(),
};
foo(Keypair::new());
let _box_signer = Foo {
signer: Box::new(Keypair::new()),
};
foo(Box::new(Keypair::new()));
let _signer = Foo {
signer: Keypair::new(),
};
foo(Keypair::new());
}
#[test]
fn test_keypair_from_seed_phrase_and_passphrase() {
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
let passphrase = "42";
let seed = Seed::new(&mnemonic, passphrase);
let expected_keypair = keypair_from_seed(seed.as_bytes()).unwrap();
let keypair =
keypair_from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap();
assert_eq!(keypair.pubkey(), expected_keypair.pubkey());
}
}