use x25519_dalek::{PublicKey, StaticSecret};
pub struct EphemeralKeypair {
secret: StaticSecret,
public: [u8; 32],
}
impl EphemeralKeypair {
#[must_use]
pub fn generate() -> Self {
let secret = StaticSecret::random();
let public = PublicKey::from(&secret).to_bytes();
Self { secret, public }
}
#[must_use]
pub fn from_secret(scalar: [u8; 32]) -> Self {
let secret = StaticSecret::from(scalar);
let public = PublicKey::from(&secret).to_bytes();
Self { secret, public }
}
#[must_use]
pub fn public(&self) -> [u8; 32] {
self.public
}
#[must_use]
pub fn diffie_hellman(&self, peer_public: &[u8; 32]) -> [u8; 32] {
let peer = PublicKey::from(*peer_public);
self.secret.diffie_hellman(&peer).to_bytes()
}
}
#[must_use]
pub fn x25519_shared(secret: &[u8; 32], peer_public: &[u8; 32]) -> [u8; 32] {
let secret = StaticSecret::from(*secret);
let peer = PublicKey::from(*peer_public);
secret.diffie_hellman(&peer).to_bytes()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn h(s: &str) -> [u8; 32] {
hex::decode(s).unwrap().try_into().unwrap()
}
const ALICE_PRIV: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
const ALICE_PUB: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
const BOB_PRIV: &str = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
const BOB_PUB: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
const SHARED_K: &str = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
#[test]
fn alice_public_matches_rfc7748() {
assert_eq!(
EphemeralKeypair::from_secret(h(ALICE_PRIV)).public(),
h(ALICE_PUB)
);
}
#[test]
fn bob_public_matches_rfc7748() {
assert_eq!(
EphemeralKeypair::from_secret(h(BOB_PRIV)).public(),
h(BOB_PUB)
);
}
#[test]
fn alice_dh_bob_pub_matches_rfc7748_k() {
let alice = EphemeralKeypair::from_secret(h(ALICE_PRIV));
assert_eq!(alice.diffie_hellman(&h(BOB_PUB)), h(SHARED_K));
}
#[test]
fn bob_dh_alice_pub_matches_rfc7748_k() {
let bob = EphemeralKeypair::from_secret(h(BOB_PRIV));
assert_eq!(bob.diffie_hellman(&h(ALICE_PUB)), h(SHARED_K));
}
#[test]
fn dh_is_symmetric() {
let alice = EphemeralKeypair::from_secret(h(ALICE_PRIV));
let bob = EphemeralKeypair::from_secret(h(BOB_PRIV));
assert_eq!(
alice.diffie_hellman(&bob.public()),
bob.diffie_hellman(&alice.public())
);
}
#[test]
fn free_function_matches_rfc7748_k() {
assert_eq!(x25519_shared(&h(ALICE_PRIV), &h(BOB_PUB)), h(SHARED_K));
assert_eq!(x25519_shared(&h(BOB_PRIV), &h(ALICE_PUB)), h(SHARED_K));
}
#[test]
fn generate_then_exchange_roundtrips() {
let alice = EphemeralKeypair::generate();
let bob = EphemeralKeypair::generate();
assert_eq!(alice.public().len(), 32);
assert_eq!(
alice.diffie_hellman(&bob.public()),
bob.diffie_hellman(&alice.public())
);
}
}