use crate::error::TaiError;
use crate::ids::{SuiAddress, SUI_ADDR_LEN};
use async_trait::async_trait;
use blake2::digest::consts::U32;
use blake2::{Blake2b, Digest};
use ed25519_dalek::{Signature as EdSignature, Signer as EdSigner, SigningKey, VerifyingKey};
use std::path::Path;
pub const SCHEME_ED25519: u8 = 0x00;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SuiSignature {
pub bytes: Vec<u8>,
}
impl SuiSignature {
pub fn from_ed25519(sig: EdSignature, pubkey: VerifyingKey) -> Self {
let mut out = Vec::with_capacity(97);
out.push(SCHEME_ED25519);
out.extend_from_slice(&sig.to_bytes());
out.extend_from_slice(pubkey.as_bytes());
SuiSignature { bytes: out }
}
pub fn to_base64(&self) -> String {
use base64ct::{Base64, Encoding};
Base64::encode_string(&self.bytes)
}
}
#[async_trait]
pub trait Signer: Send + Sync {
fn address(&self) -> SuiAddress;
async fn sign(&self, digest: &[u8; 32]) -> Result<SuiSignature, TaiError>;
}
pub struct Ed25519FileSigner {
key: SigningKey,
pubkey: VerifyingKey,
address: SuiAddress,
}
impl Ed25519FileSigner {
pub fn from_seed(seed: [u8; 32]) -> Self {
let key = SigningKey::from_bytes(&seed);
let pubkey = key.verifying_key();
let address = address_from_ed25519_pubkey(&pubkey);
Ed25519FileSigner {
key,
pubkey,
address,
}
}
pub async fn load_from_file(path: impl AsRef<Path>) -> Result<Self, TaiError> {
let path_ref = path.as_ref();
check_key_file_permissions(path_ref).await;
let raw = tokio::fs::read(path_ref).await?;
if raw.is_empty() {
return Err(TaiError::Signer(format!(
"key file is empty: {} — place a 32-byte seed (raw or hex) there",
path_ref.display()
)));
}
let seed = parse_seed_bytes(&raw)?;
Ok(Self::from_seed(seed))
}
pub fn public_key(&self) -> &VerifyingKey {
&self.pubkey
}
}
#[async_trait]
impl Signer for Ed25519FileSigner {
fn address(&self) -> SuiAddress {
self.address
}
async fn sign(&self, digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
let sig = self.key.sign(digest);
Ok(SuiSignature::from_ed25519(sig, self.pubkey))
}
}
pub fn address_from_ed25519_pubkey(pk: &VerifyingKey) -> SuiAddress {
let mut hasher = Blake2b::<U32>::new();
hasher.update([SCHEME_ED25519]);
hasher.update(pk.as_bytes());
let out = hasher.finalize();
let mut bytes = [0u8; SUI_ADDR_LEN];
bytes.copy_from_slice(&out);
SuiAddress::from_bytes(bytes)
}
pub async fn save_seed_to_file(seed: &[u8; 32], path: impl AsRef<Path>) -> Result<(), TaiError> {
let path_ref = path.as_ref();
if path_ref.exists() {
return Err(TaiError::Signer(format!(
"refusing to overwrite existing key file at {}",
path_ref.display()
)));
}
if let Some(parent) = path_ref.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let hex_str = hex::encode(seed);
tokio::fs::write(path_ref, &hex_str).await?;
set_owner_only_perms(path_ref).await;
Ok(())
}
#[cfg(unix)]
async fn check_key_file_permissions(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let Ok(meta) = tokio::fs::metadata(path).await else {
return;
};
let mode = meta.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
eprintln!(
"[tai] warning: key file {} has mode {:o} — group/world bits set. \
Recommended: chmod 600 {}",
path.display(),
mode,
path.display(),
);
}
}
#[cfg(not(unix))]
async fn check_key_file_permissions(_path: &Path) {
}
#[cfg(unix)]
async fn set_owner_only_perms(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
let _ = tokio::fs::set_permissions(path, perms).await;
}
#[cfg(not(unix))]
async fn set_owner_only_perms(_path: &Path) {
}
fn parse_seed_bytes(raw: &[u8]) -> Result<[u8; 32], TaiError> {
let looks_hex = !raw.is_empty()
&& raw.iter().all(|b| {
matches!(
b,
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' | b'x' | b'\n' | b'\r' | b' ' | b'\t'
)
});
if looks_hex {
let s = std::str::from_utf8(raw)
.map_err(|e| TaiError::Signer(format!("key file not utf8: {e}")))?
.trim();
let s = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(s)?;
if bytes.len() != 32 {
return Err(TaiError::Signer(format!(
"expected 32-byte seed, got {} bytes",
bytes.len()
)));
}
let mut out = [0u8; 32];
out.copy_from_slice(&bytes);
return Ok(out);
}
if raw.len() < 32 {
return Err(TaiError::Signer(format!(
"key file too short: {} bytes (need at least 32)",
raw.len()
)));
}
let mut out = [0u8; 32];
out.copy_from_slice(&raw[..32]);
Ok(out)
}
pub struct SuiKeystoreSigner;
const SUI_KEYSTORE_UNIMPL: &str =
"SuiKeystoreSigner is not implemented in this version; use Ed25519FileSigner";
#[async_trait]
impl Signer for SuiKeystoreSigner {
fn address(&self) -> SuiAddress {
SuiAddress::from_bytes([0u8; SUI_ADDR_LEN])
}
async fn sign(&self, _digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
Err(TaiError::Signer(SUI_KEYSTORE_UNIMPL.into()))
}
}
pub struct TurnkeySigner;
const TURNKEY_UNIMPL: &str =
"TurnkeySigner is not implemented in this version; use Ed25519FileSigner";
#[async_trait]
impl Signer for TurnkeySigner {
fn address(&self) -> SuiAddress {
SuiAddress::from_bytes([0u8; SUI_ADDR_LEN])
}
async fn sign(&self, _digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
Err(TaiError::Signer(TURNKEY_UNIMPL.into()))
}
}
pub struct TeeSigner;
const TEE_UNIMPL: &str = "TeeSigner is not implemented in this version; use Ed25519FileSigner";
#[async_trait]
impl Signer for TeeSigner {
fn address(&self) -> SuiAddress {
SuiAddress::from_bytes([0u8; SUI_ADDR_LEN])
}
async fn sign(&self, _digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
Err(TaiError::Signer(TEE_UNIMPL.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ed25519_signer_derives_a_well_formed_address() {
let signer = Ed25519FileSigner::from_seed([7u8; 32]);
let addr = signer.address();
let s = addr.to_string();
assert!(s.starts_with("0x"));
assert_eq!(s.len(), 66);
}
#[tokio::test]
async fn ed25519_signer_produces_97_byte_wire_signature() {
let signer = Ed25519FileSigner::from_seed([7u8; 32]);
let digest = [0u8; 32];
let sig = signer.sign(&digest).await.unwrap();
assert_eq!(sig.bytes.len(), 97);
assert_eq!(sig.bytes[0], SCHEME_ED25519);
}
#[test]
fn deterministic_address_for_known_seed() {
let a = Ed25519FileSigner::from_seed([42u8; 32]).address();
let b = Ed25519FileSigner::from_seed([42u8; 32]).address();
assert_eq!(a, b);
}
}