use ed25519_dalek::{SigningKey, VerifyingKey};
use hkdf::Hkdf;
use sha2::Sha256;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
use crate::e2e::error::{E2eError, Result};
pub struct EphemeralKeypair {
secret: StaticSecret,
public: PublicKey,
}
impl std::fmt::Debug for EphemeralKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EphemeralKeypair")
.field("public", &hex::encode(self.public.to_bytes()))
.field("secret", &"<redacted>")
.finish()
}
}
impl EphemeralKeypair {
pub fn generate() -> Result<Self> {
let mut seed = Zeroizing::new([0u8; 32]);
rand::fill(seed.as_mut_slice());
let secret = StaticSecret::from(*seed);
let public = PublicKey::from(&secret);
Ok(Self { secret, public })
}
#[must_use]
pub fn public_bytes(&self) -> [u8; 32] {
self.public.to_bytes()
}
#[must_use]
pub fn derive_wrap_key(&self, peer_pub: &[u8; 32], info: &[u8]) -> [u8; 32] {
let peer = PublicKey::from(*peer_pub);
let shared = self.secret.diffie_hellman(&peer);
let hk = Hkdf::<Sha256>::new(Some(b"RPE2E01-WRAP"), shared.as_bytes());
let mut okm = [0u8; 32];
hk.expand(info, &mut okm).expect("hkdf expand 32 bytes");
okm
}
}
#[allow(dead_code)]
#[must_use]
pub fn static_derive_wrap_key(
my_secret: &[u8; 32],
peer_public: &[u8; 32],
info: &[u8],
) -> [u8; 32] {
let secret = StaticSecret::from(*my_secret);
let shared = secret.diffie_hellman(&PublicKey::from(*peer_public));
let hk = Hkdf::<Sha256>::new(Some(b"RPE2E01-WRAP"), shared.as_bytes());
let mut okm = [0u8; 32];
hk.expand(info, &mut okm).expect("hkdf expand 32 bytes");
okm
}
pub fn ed25519_pub_to_x25519(ed_pub: &[u8; 32]) -> Result<[u8; 32]> {
let vk = VerifyingKey::from_bytes(ed_pub)
.map_err(|e| E2eError::Crypto(format!("invalid ed25519 pub: {e}")))?;
Ok(vk.to_montgomery().to_bytes())
}
#[must_use]
pub fn ed25519_seed_to_x25519(ed_seed: &[u8; 32]) -> [u8; 32] {
let signing = SigningKey::from_bytes(ed_seed);
signing.to_scalar_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
fn h32(s: &str) -> [u8; 32] {
let v = hex::decode(s).expect("bad hex");
assert_eq!(v.len(), 32);
let mut out = [0u8; 32];
out.copy_from_slice(&v);
out
}
#[test]
fn ecdh_roundtrip_yields_same_shared() {
let alice = EphemeralKeypair::generate().unwrap();
let bob = EphemeralKeypair::generate().unwrap();
let info = b"test-context";
let k_ab = alice.derive_wrap_key(&bob.public_bytes(), info);
let k_ba = bob.derive_wrap_key(&alice.public_bytes(), info);
assert_eq!(k_ab, k_ba);
assert_eq!(k_ab.len(), 32);
}
#[test]
fn ecdh_different_info_yields_different_keys() {
let alice = EphemeralKeypair::generate().unwrap();
let bob = EphemeralKeypair::generate().unwrap();
let k1 = alice.derive_wrap_key(&bob.public_bytes(), b"ctx-1");
let k2 = alice.derive_wrap_key(&bob.public_bytes(), b"ctx-2");
assert_ne!(k1, k2);
}
#[test]
fn ed25519_to_x25519_rfc8032_vectors() {
let seed_a = h32("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
let seed_b = h32("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb");
let signing_a = SigningKey::from_bytes(&seed_a);
let signing_b = SigningKey::from_bytes(&seed_b);
let scalar_a = ed25519_seed_to_x25519(&seed_a);
let scalar_b = ed25519_seed_to_x25519(&seed_b);
let x_sk_a = StaticSecret::from(scalar_a);
let x_sk_b = StaticSecret::from(scalar_b);
assert_eq!(
PublicKey::from(&x_sk_a).to_bytes(),
h32("d85e07ec22b0ad881537c2f44d662d1a143cf830c57aca4305d85c7a90f6b62e")
);
assert_eq!(
PublicKey::from(&x_sk_b).to_bytes(),
h32("25c704c594b88afc00a76b69d1ed2b984d7e22550f3ed0802d04fbcd07d38d47")
);
let pub_from_ed_a = ed25519_pub_to_x25519(&signing_a.verifying_key().to_bytes()).unwrap();
let pub_from_ed_b = ed25519_pub_to_x25519(&signing_b.verifying_key().to_bytes()).unwrap();
assert_eq!(pub_from_ed_a, PublicKey::from(&x_sk_a).to_bytes());
assert_eq!(pub_from_ed_b, PublicKey::from(&x_sk_b).to_bytes());
let expected_shared =
h32("5166f24a6918368e2af831a4affadd97af0ac326bdf143596c045967cc00230e");
assert_eq!(
x_sk_a
.diffie_hellman(&PublicKey::from(pub_from_ed_b))
.to_bytes(),
expected_shared,
);
assert_eq!(
x_sk_b
.diffie_hellman(&PublicKey::from(pub_from_ed_a))
.to_bytes(),
expected_shared,
);
}
#[test]
fn ed25519_to_x25519_roundtrip_via_identity() {
use crate::e2e::crypto::identity::Identity;
let id = Identity::generate().unwrap();
let seed = id.secret_bytes();
let pub_ed = id.public_bytes();
let scalar = ed25519_seed_to_x25519(&seed);
let pub_from_secret = PublicKey::from(&StaticSecret::from(scalar)).to_bytes();
let pub_from_ed = ed25519_pub_to_x25519(&pub_ed).unwrap();
assert_eq!(pub_from_secret, pub_from_ed);
}
}