1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
43use hkdf::Hkdf;
44use p256::elliptic_curve::ops::{MulByGenerator, Reduce};
45use p256::elliptic_curve::sec1::ToEncodedPoint;
46use sha2::{Digest, Sha256};
47use zeroize::Zeroizing;
48
49const DOMAIN_STRING: &[u8] = b"atlas:ecdh:p256:ed25519:derivation:v1";
56
57const HKDF_INFO: &[u8] = b"ed25519-signing-key";
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub enum Chain {
70 Solana,
71 Sui,
72 Aptos,
73 Sei,
74 Stellar,
75 Near,
76 Cosmos,
77 Polkadot,
78 Cardano,
79 Ton,
80 Custom(String),
83}
84
85impl Chain {
86 pub fn salt(&self) -> Vec<u8> {
88 match self {
89 Chain::Solana => b"atlas:ecdh:solana:ed25519:v1".to_vec(),
90 Chain::Sui => b"atlas:ecdh:sui:ed25519:v1".to_vec(),
91 Chain::Aptos => b"atlas:ecdh:aptos:ed25519:v1".to_vec(),
92 Chain::Sei => b"atlas:ecdh:sei:ed25519:v1".to_vec(),
93 Chain::Stellar => b"atlas:ecdh:stellar:ed25519:v1".to_vec(),
94 Chain::Near => b"atlas:ecdh:near:ed25519:v1".to_vec(),
95 Chain::Cosmos => b"atlas:ecdh:cosmos:ed25519:v1".to_vec(),
96 Chain::Polkadot => b"atlas:ecdh:polkadot:ed25519:v1".to_vec(),
97 Chain::Cardano => b"atlas:ecdh:cardano:ed25519:v1".to_vec(),
98 Chain::Ton => b"atlas:ecdh:ton:ed25519:v1".to_vec(),
99 Chain::Custom(s) => s.as_bytes().to_vec(),
100 }
101 }
102
103 pub fn name(&self) -> &str {
105 match self {
106 Chain::Solana => "Solana",
107 Chain::Sui => "Sui",
108 Chain::Aptos => "Aptos",
109 Chain::Sei => "Sei",
110 Chain::Stellar => "Stellar",
111 Chain::Near => "NEAR",
112 Chain::Cosmos => "Cosmos",
113 Chain::Polkadot => "Polkadot",
114 Chain::Cardano => "Cardano",
115 Chain::Ton => "TON",
116 Chain::Custom(s) => s.as_str(),
117 }
118 }
119
120 pub fn all_builtins() -> &'static [Chain] {
122 &[
123 Chain::Solana,
124 Chain::Sui,
125 Chain::Aptos,
126 Chain::Sei,
127 Chain::Stellar,
128 Chain::Near,
129 Chain::Cosmos,
130 Chain::Polkadot,
131 Chain::Cardano,
132 Chain::Ton,
133 ]
134 }
135}
136
137impl std::fmt::Display for Chain {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(f, "{}", self.name())
140 }
141}
142
143pub fn fixed_point_uncompressed() -> Vec<u8> {
167 let scalar = domain_scalar();
168 let point = p256::ProjectivePoint::mul_by_generator(&scalar);
169 let affine = p256::AffinePoint::from(point);
170 affine.to_encoded_point(false).as_bytes().to_vec()
171}
172
173pub fn fixed_point_xy() -> Vec<u8> {
184 fixed_point_uncompressed()[1..].to_vec()
185}
186
187pub fn fixed_point_compressed() -> Vec<u8> {
189 let scalar = domain_scalar();
190 let point = p256::ProjectivePoint::mul_by_generator(&scalar);
191 let affine = p256::AffinePoint::from(point);
192 affine.to_encoded_point(true).as_bytes().to_vec()
193}
194
195fn domain_scalar() -> p256::Scalar {
197 let hash = Sha256::digest(DOMAIN_STRING);
198 <p256::Scalar as Reduce<p256::U256>>::reduce_bytes(&hash)
199}
200
201fn derive_seed(ecdh_secret: &[u8], chain: &Chain) -> Result<Zeroizing<[u8; 32]>, String> {
212 if ecdh_secret.len() != 32 {
213 return Err(format!(
214 "ECDH secret must be exactly 32 bytes, got {}",
215 ecdh_secret.len()
216 ));
217 }
218
219 let salt = chain.salt();
220 let hk = Hkdf::<Sha256>::new(Some(&salt), ecdh_secret);
221 let mut okm = Zeroizing::new([0u8; 32]);
222 hk.expand(HKDF_INFO, okm.as_mut())
223 .map_err(|e| format!("HKDF expand failed: {}", e))?;
224 Ok(okm)
225}
226
227pub fn derive_public_key(ecdh_secret: &[u8], chain: &Chain) -> Result<Vec<u8>, String> {
241 let seed = derive_seed(ecdh_secret, chain)?;
242 let signing_key = SigningKey::from_bytes(&seed);
243 let verifying_key: VerifyingKey = (&signing_key).into();
244 Ok(verifying_key.to_bytes().to_vec())
245}
246
247pub fn derive_public_key_base58(ecdh_secret: &[u8], chain: &Chain) -> Result<String, String> {
257 let pubkey = derive_public_key(ecdh_secret, chain)?;
258 Ok(bs58::encode(&pubkey).into_string())
259}
260
261pub fn derive_public_key_hex(ecdh_secret: &[u8], chain: &Chain) -> Result<String, String> {
263 let pubkey = derive_public_key(ecdh_secret, chain)?;
264 Ok(hex::encode(&pubkey))
265}
266
267pub fn derive_all_builtin_keys(
278 ecdh_secret: &[u8],
279) -> Result<Vec<(Chain, String)>, String> {
280 Chain::all_builtins()
281 .iter()
282 .map(|chain| {
283 let addr = derive_public_key_base58(ecdh_secret, chain)?;
284 Ok((chain.clone(), addr))
285 })
286 .collect()
287}
288
289pub fn sign(ecdh_secret: &[u8], message: &[u8], chain: &Chain) -> Result<Vec<u8>, String> {
308 let seed = derive_seed(ecdh_secret, chain)?;
309 let signing_key = SigningKey::from_bytes(&seed);
310 let signature = signing_key.sign(message);
311 Ok(signature.to_bytes().to_vec())
313}
314
315pub fn verify(pubkey: &[u8], message: &[u8], signature: &[u8]) -> Result<(), String> {
320 if pubkey.len() != 32 {
321 return Err(format!("Public key must be 32 bytes, got {}", pubkey.len()));
322 }
323 if signature.len() != 64 {
324 return Err(format!("Signature must be 64 bytes, got {}", signature.len()));
325 }
326
327 let vk_bytes: [u8; 32] = pubkey.try_into().map_err(|_| "Invalid pubkey length")?;
328 let verifying_key =
329 VerifyingKey::from_bytes(&vk_bytes).map_err(|e| format!("Invalid public key: {}", e))?;
330
331 let sig_bytes: [u8; 64] = signature.try_into().map_err(|_| "Invalid signature length")?;
332 let sig = Signature::from_bytes(&sig_bytes);
333
334 verifying_key
335 .verify(message, &sig)
336 .map_err(|e| format!("Signature verification failed: {}", e))
337}
338
339pub fn print_fixed_point_info() {
345 let uncompressed = fixed_point_uncompressed();
346 let xy = fixed_point_xy();
347 let compressed = fixed_point_compressed();
348
349 println!("═══ atlas-ecdh-bridge Fixed Point ═══");
350 println!();
351 println!("Domain: {}", std::str::from_utf8(DOMAIN_STRING).unwrap());
352 println!();
353 println!(
354 "Uncompressed (65 bytes): {}",
355 hex::encode(&uncompressed)
356 );
357 println!("X (32 bytes): {}", hex::encode(&xy[..32]));
358 println!("Y (32 bytes): {}", hex::encode(&xy[32..]));
359 println!(
360 "Compressed (33 bytes): {}",
361 hex::encode(&compressed)
362 );
363 println!();
364 println!("── Android (Kotlin) ──");
365 println!("val fixedPointXY = byteArrayOf(");
366 for (i, chunk) in xy.chunks(16).enumerate() {
367 let hex_str: Vec<String> = chunk.iter().map(|b| format!("0x{:02X}.toByte()", b)).collect();
368 let comma = if i < (xy.len() + 15) / 16 - 1 { "," } else { "" };
369 println!(" {}{}", hex_str.join(", "), comma);
370 }
371 println!(")");
372 println!();
373 println!("── iOS (Swift) ──");
374 println!(
375 "let fixedPoint = Data(hex: \"{}\")",
376 hex::encode(&uncompressed)
377 );
378 println!();
379 println!("── Web (JavaScript) ──");
380 println!(
381 "const fixedPoint = new Uint8Array([{}]);",
382 uncompressed
383 .iter()
384 .map(|b| format!("0x{:02X}", b))
385 .collect::<Vec<_>>()
386 .join(", ")
387 );
388}
389
390#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn fixed_point_is_valid_p256() {
400 let point = fixed_point_uncompressed();
401 assert_eq!(point.len(), 65);
402 assert_eq!(point[0], 0x04);
403
404 let encoded =
406 p256::EncodedPoint::from_bytes(&point).expect("Must be valid SEC1 encoding");
407 assert!(!encoded.is_identity());
408 }
409
410 #[test]
411 fn fixed_point_is_deterministic() {
412 assert_eq!(fixed_point_uncompressed(), fixed_point_uncompressed());
413 }
414
415 #[test]
416 fn fixed_point_xy_matches_uncompressed() {
417 let full = fixed_point_uncompressed();
418 let xy = fixed_point_xy();
419 assert_eq!(xy.len(), 64);
420 assert_eq!(&full[1..], &xy[..]);
421 }
422
423 #[test]
424 fn fixed_point_compressed_matches() {
425 let compressed = fixed_point_compressed();
426 assert_eq!(compressed.len(), 33);
427 assert!(compressed[0] == 0x02 || compressed[0] == 0x03);
428 }
429
430 #[test]
431 fn derive_pubkey_is_32_bytes() {
432 let secret = [0xAB_u8; 32];
433 let pubkey = derive_public_key(&secret, &Chain::Solana).unwrap();
434 assert_eq!(pubkey.len(), 32);
435 }
436
437 #[test]
438 fn derive_pubkey_is_deterministic() {
439 let secret = [0xAB_u8; 32];
440 let pk1 = derive_public_key(&secret, &Chain::Solana).unwrap();
441 let pk2 = derive_public_key(&secret, &Chain::Solana).unwrap();
442 assert_eq!(pk1, pk2);
443 }
444
445 #[test]
446 fn different_chains_produce_different_keys() {
447 let secret = [0xCD_u8; 32];
448 let sol = derive_public_key(&secret, &Chain::Solana).unwrap();
449 let sui = derive_public_key(&secret, &Chain::Sui).unwrap();
450 let apt = derive_public_key(&secret, &Chain::Aptos).unwrap();
451 let sei = derive_public_key(&secret, &Chain::Sei).unwrap();
452 assert_ne!(sol, sui);
453 assert_ne!(sol, apt);
454 assert_ne!(sol, sei);
455 assert_ne!(sui, apt);
456 }
457
458 #[test]
459 fn different_secrets_produce_different_keys() {
460 let pk1 = derive_public_key(&[0xAA_u8; 32], &Chain::Solana).unwrap();
461 let pk2 = derive_public_key(&[0xBB_u8; 32], &Chain::Solana).unwrap();
462 assert_ne!(pk1, pk2);
463 }
464
465 #[test]
466 fn base58_address_format() {
467 let secret = [0x42_u8; 32];
468 let addr = derive_public_key_base58(&secret, &Chain::Solana).unwrap();
469 assert!(addr.len() >= 32 && addr.len() <= 44);
470 }
471
472 #[test]
473 fn hex_address_format() {
474 let secret = [0x42_u8; 32];
475 let addr = derive_public_key_hex(&secret, &Chain::Sui).unwrap();
476 assert_eq!(addr.len(), 64); }
478
479 #[test]
480 fn custom_chain_works() {
481 let secret = [0xEE_u8; 32];
482 let custom = Chain::Custom("my-app:my-chain:ed25519:v1".into());
483 let pk = derive_public_key(&secret, &custom).unwrap();
484 assert_eq!(pk.len(), 32);
485
486 let sol = derive_public_key(&secret, &Chain::Solana).unwrap();
488 assert_ne!(pk, sol);
489 }
490
491 #[test]
492 fn derive_all_builtin() {
493 let secret = [0xAB_u8; 32];
494 let keys = derive_all_builtin_keys(&secret).unwrap();
495 assert_eq!(keys.len(), 10);
496
497 let addrs: Vec<&String> = keys.iter().map(|(_, a)| a).collect();
499 for i in 0..addrs.len() {
500 for j in (i + 1)..addrs.len() {
501 assert_ne!(addrs[i], addrs[j]);
502 }
503 }
504 }
505
506 #[test]
507 fn sign_verify_roundtrip() {
508 let secret = [0xEF_u8; 32];
509 let message = b"hello solana";
510
511 let sig = sign(&secret, message, &Chain::Solana).unwrap();
512 assert_eq!(sig.len(), 64);
513
514 let pubkey = derive_public_key(&secret, &Chain::Solana).unwrap();
515 verify(&pubkey, message, &sig).expect("Signature must verify");
516 }
517
518 #[test]
519 fn wrong_message_fails_verification() {
520 let secret = [0xEF_u8; 32];
521 let sig = sign(&secret, b"correct", &Chain::Solana).unwrap();
522 let pubkey = derive_public_key(&secret, &Chain::Solana).unwrap();
523 assert!(verify(&pubkey, b"wrong", &sig).is_err());
524 }
525
526 #[test]
527 fn wrong_chain_fails_verification() {
528 let secret = [0xEF_u8; 32];
529 let sig = sign(&secret, b"msg", &Chain::Solana).unwrap();
530 let sui_pk = derive_public_key(&secret, &Chain::Sui).unwrap();
531 assert!(verify(&sui_pk, b"msg", &sig).is_err());
532 }
533
534 #[test]
535 fn sign_verify_all_builtin_chains() {
536 let secret = [0xDD_u8; 32];
537 let msg = b"test all chains";
538
539 for chain in Chain::all_builtins() {
540 let sig = sign(&secret, msg, chain).unwrap();
541 let pk = derive_public_key(&secret, chain).unwrap();
542 verify(&pk, msg, &sig)
543 .unwrap_or_else(|e| panic!("Failed for {}: {}", chain.name(), e));
544 }
545 }
546
547 #[test]
548 fn verify_invalid_pubkey() {
549 assert!(verify(&[0u8; 16], b"msg", &[0u8; 64]).is_err());
550 }
551
552 #[test]
553 fn verify_invalid_signature_length() {
554 let secret = [0xAA_u8; 32];
555 let pk = derive_public_key(&secret, &Chain::Solana).unwrap();
556 assert!(verify(&pk, b"msg", &[0u8; 32]).is_err());
557 }
558
559 #[test]
560 fn invalid_secret_length_0() {
561 assert!(derive_public_key(&[], &Chain::Solana).is_err());
562 }
563
564 #[test]
565 fn invalid_secret_length_16() {
566 assert!(derive_public_key(&[0u8; 16], &Chain::Solana).is_err());
567 }
568
569 #[test]
570 fn invalid_secret_length_48() {
571 assert!(sign(&[0u8; 48], b"msg", &Chain::Solana).is_err());
572 }
573}