1use crate::error::TaiError;
29use crate::ids::{SuiAddress, SUI_ADDR_LEN};
30use async_trait::async_trait;
31use blake2::digest::consts::U32;
32use blake2::{Blake2b, Digest};
33use ed25519_dalek::{Signature as EdSignature, Signer as EdSigner, SigningKey, VerifyingKey};
34use std::path::Path;
35
36pub const SCHEME_ED25519: u8 = 0x00;
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct SuiSignature {
42 pub bytes: Vec<u8>,
44}
45
46impl SuiSignature {
47 pub fn from_ed25519(sig: EdSignature, pubkey: VerifyingKey) -> Self {
49 let mut out = Vec::with_capacity(97);
50 out.push(SCHEME_ED25519);
51 out.extend_from_slice(&sig.to_bytes());
52 out.extend_from_slice(pubkey.as_bytes());
53 SuiSignature { bytes: out }
54 }
55
56 pub fn to_base64(&self) -> String {
58 use base64ct::{Base64, Encoding};
59 Base64::encode_string(&self.bytes)
60 }
61}
62
63#[async_trait]
67pub trait Signer: Send + Sync {
68 fn address(&self) -> SuiAddress;
70
71 async fn sign(&self, digest: &[u8; 32]) -> Result<SuiSignature, TaiError>;
73}
74
75pub struct Ed25519FileSigner {
88 key: SigningKey,
89 pubkey: VerifyingKey,
90 address: SuiAddress,
91}
92
93impl Ed25519FileSigner {
94 pub fn from_seed(seed: [u8; 32]) -> Self {
96 let key = SigningKey::from_bytes(&seed);
97 let pubkey = key.verifying_key();
98 let address = address_from_ed25519_pubkey(&pubkey);
99 Ed25519FileSigner {
100 key,
101 pubkey,
102 address,
103 }
104 }
105
106 pub async fn load_from_file(path: impl AsRef<Path>) -> Result<Self, TaiError> {
115 let path_ref = path.as_ref();
116 check_key_file_permissions(path_ref).await;
117 let raw = tokio::fs::read(path_ref).await?;
118 if raw.is_empty() {
119 return Err(TaiError::Signer(format!(
120 "key file is empty: {} — place a 32-byte seed (raw or hex) there",
121 path_ref.display()
122 )));
123 }
124 let seed = parse_seed_bytes(&raw)?;
125 Ok(Self::from_seed(seed))
126 }
127
128 pub fn public_key(&self) -> &VerifyingKey {
130 &self.pubkey
131 }
132}
133
134#[async_trait]
135impl Signer for Ed25519FileSigner {
136 fn address(&self) -> SuiAddress {
137 self.address
138 }
139
140 async fn sign(&self, digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
141 let sig = self.key.sign(digest);
142 Ok(SuiSignature::from_ed25519(sig, self.pubkey))
143 }
144}
145
146pub fn address_from_ed25519_pubkey(pk: &VerifyingKey) -> SuiAddress {
151 let mut hasher = Blake2b::<U32>::new();
152 hasher.update([SCHEME_ED25519]);
153 hasher.update(pk.as_bytes());
154 let out = hasher.finalize();
155 let mut bytes = [0u8; SUI_ADDR_LEN];
156 bytes.copy_from_slice(&out);
157 SuiAddress::from_bytes(bytes)
158}
159
160pub async fn save_seed_to_file(seed: &[u8; 32], path: impl AsRef<Path>) -> Result<(), TaiError> {
166 let path_ref = path.as_ref();
167 if path_ref.exists() {
168 return Err(TaiError::Signer(format!(
169 "refusing to overwrite existing key file at {}",
170 path_ref.display()
171 )));
172 }
173 if let Some(parent) = path_ref.parent() {
174 tokio::fs::create_dir_all(parent).await?;
175 }
176 let hex_str = hex::encode(seed);
177 tokio::fs::write(path_ref, &hex_str).await?;
178 set_owner_only_perms(path_ref).await;
179 Ok(())
180}
181
182#[cfg(unix)]
183async fn check_key_file_permissions(path: &Path) {
184 use std::os::unix::fs::PermissionsExt;
185 let Ok(meta) = tokio::fs::metadata(path).await else {
186 return;
187 };
188 let mode = meta.permissions().mode() & 0o777;
189 if mode & 0o077 != 0 {
190 eprintln!(
191 "[tai] warning: key file {} has mode {:o} — group/world bits set. \
192 Recommended: chmod 600 {}",
193 path.display(),
194 mode,
195 path.display(),
196 );
197 }
198}
199
200#[cfg(not(unix))]
201async fn check_key_file_permissions(_path: &Path) {
202 }
204
205#[cfg(unix)]
206async fn set_owner_only_perms(path: &Path) {
207 use std::os::unix::fs::PermissionsExt;
208 let perms = std::fs::Permissions::from_mode(0o600);
209 let _ = tokio::fs::set_permissions(path, perms).await;
210}
211
212#[cfg(not(unix))]
213async fn set_owner_only_perms(_path: &Path) {
214 }
216
217fn parse_seed_bytes(raw: &[u8]) -> Result<[u8; 32], TaiError> {
218 let looks_hex = !raw.is_empty()
222 && raw.iter().all(|b| {
223 matches!(
224 b,
225 b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' | b'x' | b'\n' | b'\r' | b' ' | b'\t'
226 )
227 });
228
229 if looks_hex {
230 let s = std::str::from_utf8(raw)
231 .map_err(|e| TaiError::Signer(format!("key file not utf8: {e}")))?
232 .trim();
233 let s = s.strip_prefix("0x").unwrap_or(s);
234 let bytes = hex::decode(s)?;
235 if bytes.len() != 32 {
236 return Err(TaiError::Signer(format!(
237 "expected 32-byte seed, got {} bytes",
238 bytes.len()
239 )));
240 }
241 let mut out = [0u8; 32];
242 out.copy_from_slice(&bytes);
243 return Ok(out);
244 }
245
246 if raw.len() < 32 {
247 return Err(TaiError::Signer(format!(
248 "key file too short: {} bytes (need at least 32)",
249 raw.len()
250 )));
251 }
252 let mut out = [0u8; 32];
253 out.copy_from_slice(&raw[..32]);
254 Ok(out)
255}
256
257pub struct SuiKeystoreSigner;
267
268const SUI_KEYSTORE_UNIMPL: &str =
269 "SuiKeystoreSigner is not implemented in this version; use Ed25519FileSigner";
270
271#[async_trait]
272impl Signer for SuiKeystoreSigner {
273 fn address(&self) -> SuiAddress {
274 SuiAddress::from_bytes([0u8; SUI_ADDR_LEN])
278 }
279 async fn sign(&self, _digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
280 Err(TaiError::Signer(SUI_KEYSTORE_UNIMPL.into()))
281 }
282}
283
284pub struct TurnkeySigner;
288
289const TURNKEY_UNIMPL: &str =
290 "TurnkeySigner is not implemented in this version; use Ed25519FileSigner";
291
292#[async_trait]
293impl Signer for TurnkeySigner {
294 fn address(&self) -> SuiAddress {
295 SuiAddress::from_bytes([0u8; SUI_ADDR_LEN])
296 }
297 async fn sign(&self, _digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
298 Err(TaiError::Signer(TURNKEY_UNIMPL.into()))
299 }
300}
301
302pub struct TeeSigner;
306
307const TEE_UNIMPL: &str = "TeeSigner is not implemented in this version; use Ed25519FileSigner";
308
309#[async_trait]
310impl Signer for TeeSigner {
311 fn address(&self) -> SuiAddress {
312 SuiAddress::from_bytes([0u8; SUI_ADDR_LEN])
313 }
314 async fn sign(&self, _digest: &[u8; 32]) -> Result<SuiSignature, TaiError> {
315 Err(TaiError::Signer(TEE_UNIMPL.into()))
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn ed25519_signer_derives_a_well_formed_address() {
325 let signer = Ed25519FileSigner::from_seed([7u8; 32]);
326 let addr = signer.address();
327 let s = addr.to_string();
328 assert!(s.starts_with("0x"));
330 assert_eq!(s.len(), 66);
331 }
332
333 #[tokio::test]
334 async fn ed25519_signer_produces_97_byte_wire_signature() {
335 let signer = Ed25519FileSigner::from_seed([7u8; 32]);
336 let digest = [0u8; 32];
337 let sig = signer.sign(&digest).await.unwrap();
338 assert_eq!(sig.bytes.len(), 97);
339 assert_eq!(sig.bytes[0], SCHEME_ED25519);
340 }
341
342 #[test]
343 fn deterministic_address_for_known_seed() {
344 let a = Ed25519FileSigner::from_seed([42u8; 32]).address();
346 let b = Ed25519FileSigner::from_seed([42u8; 32]).address();
347 assert_eq!(a, b);
348 }
349}