use super::{KeyPair, SeaError};
use base64::prelude::*;
pub async fn secret(their_epub: &str, pair: &KeyPair) -> Result<String, SeaError> {
let their = their_epub.to_string();
let pair = pair.clone();
tokio::task::spawn_blocking(move || secret_sync(&their, &pair))
.await
.map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?
}
pub fn secret_sync(their_epub: &str, pair: &KeyPair) -> Result<String, SeaError> {
let their_pub = parse_epub(their_epub)?;
let our_epriv = pair
.epriv_key
.as_ref()
.ok_or_else(|| SeaError::Crypto("missing epriv key".to_string()))?;
let our_priv_bytes = BASE64_URL_SAFE_NO_PAD
.decode(our_epriv)
.map_err(|_| SeaError::InvalidKey)?;
if our_priv_bytes.len() != 32 {
return Err(SeaError::InvalidKey);
}
let mut priv_array = [0u8; 32];
priv_array.copy_from_slice(&our_priv_bytes);
let our_secret =
p256::SecretKey::from_bytes(&priv_array.into()).map_err(|_| SeaError::InvalidKey)?;
let shared_secret =
p256::ecdh::diffie_hellman(our_secret.to_nonzero_scalar(), their_pub.as_affine());
let shared_bytes = shared_secret.raw_secret_bytes();
Ok(BASE64_URL_SAFE_NO_PAD.encode(&shared_bytes[..]))
}
fn parse_epub(epub: &str) -> Result<p256::PublicKey, SeaError> {
let parts: Vec<&str> = epub.split('.').collect();
if parts.len() != 2 {
return Err(SeaError::InvalidKey);
}
let x = BASE64_URL_SAFE_NO_PAD
.decode(parts[0])
.map_err(|_| SeaError::InvalidKey)?;
let y = BASE64_URL_SAFE_NO_PAD
.decode(parts[1])
.map_err(|_| SeaError::InvalidKey)?;
let mut pub_bytes = Vec::with_capacity(65);
pub_bytes.push(0x04u8);
pub_bytes.extend_from_slice(&x);
pub_bytes.extend_from_slice(&y);
p256::PublicKey::from_sec1_bytes(&pub_bytes).map_err(|_| SeaError::InvalidKey)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sea::generate_pair;
#[tokio::test]
async fn test_secret_symmetric() {
let alice = generate_pair().await.unwrap();
let bob = generate_pair().await.unwrap();
let alice_secret = secret(bob.epub_key.as_ref().unwrap(), &alice)
.await
.unwrap();
let bob_secret = secret(alice.epub_key.as_ref().unwrap(), &bob)
.await
.unwrap();
assert_eq!(alice_secret, bob_secret, "ECDH shared secrets must match");
}
#[tokio::test]
async fn test_secret_sync_matches_async() {
let alice = generate_pair().await.unwrap();
let bob = generate_pair().await.unwrap();
let async_result = secret(bob.epub_key.as_ref().unwrap(), &alice)
.await
.unwrap();
let sync_result = secret_sync(bob.epub_key.as_ref().unwrap(), &alice).unwrap();
assert_eq!(async_result, sync_result);
}
#[tokio::test]
async fn test_secret_invalid_epub_format() {
let pair = generate_pair().await.unwrap();
assert!(secret_sync("invalid_key", &pair).is_err());
}
#[tokio::test]
async fn test_secret_missing_epriv() {
let mut pair = generate_pair().await.unwrap();
pair.epriv_key = None;
let bob = generate_pair().await.unwrap();
assert!(secret_sync(bob.epub_key.as_ref().unwrap(), &pair).is_err());
}
}