use std::fmt;
use packtool::Packed;
use rand_core::{CryptoRng, RngCore};
use super::{Curve, DhCurve, SharedSecret};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid public key length: expected 32 bytes, got {0}")]
InvalidPublicKeyLength(usize),
}
#[derive(Debug, Clone, Copy, Default)]
pub struct X25519;
impl Curve for X25519 {
const NAME: &'static str = "25519";
const PUBLIC_KEY_SIZE: usize = 32;
const PRIVATE_KEY_SIZE: usize = 32;
type Error = Error;
type PublicKey = X25519PublicKey;
fn public_key_from_bytes(bytes: &[u8]) -> Result<Self::PublicKey, Self::Error> {
X25519PublicKey::from_bytes(bytes)
}
}
impl DhCurve for X25519 {
const DHLEN: usize = 32;
type SharedSecret = SharedSecret<32>;
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Packed)]
pub struct X25519PublicKey(#[packed(accessor = false)] [u8; 32]);
impl X25519PublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| Error::InvalidPublicKeyLength(bytes.len()))?;
Ok(Self(arr))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl AsRef<[u8]> for X25519PublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl fmt::Display for X25519PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::encode(self.0))
}
}
impl fmt::Debug for X25519PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::encode(self.0))
}
}
mod backend {
#[cfg(feature = "x25519-cryptoxide")]
pub(super) use cryptoxide_backend::{dh, public_key};
#[cfg(not(feature = "x25519-cryptoxide"))]
pub(super) use eccoxide_backend::{dh, public_key};
#[cfg(not(feature = "x25519-cryptoxide"))]
mod eccoxide_backend {
use eccoxide::protocol::x25519;
pub fn public_key(secret: [u8; 32]) -> [u8; 32] {
x25519::SecretKey::from_bytes(secret)
.public_key()
.to_bytes()
}
pub fn dh(secret: [u8; 32], peer: [u8; 32]) -> [u8; 32] {
x25519::SecretKey::from_bytes(secret)
.diffie_hellman(&x25519::PublicKey::from_bytes(peer))
.to_bytes()
}
}
#[cfg(feature = "x25519-cryptoxide")]
mod cryptoxide_backend {
use cryptoxide::x25519;
pub fn public_key(secret: [u8; 32]) -> [u8; 32] {
x25519::base(&x25519::SecretKey::from(secret)).into()
}
pub fn dh(secret: [u8; 32], peer: [u8; 32]) -> [u8; 32] {
x25519::dh(
&x25519::SecretKey::from(secret),
&x25519::PublicKey::from(peer),
)
.into()
}
}
}
pub struct SoftwareX25519PrivateKey {
secret: [u8; 32],
}
impl SoftwareX25519PrivateKey {
pub fn generate<R: RngCore + CryptoRng>(mut rng: R) -> Self {
let mut secret = [0u8; 32];
rng.fill_bytes(&mut secret);
Self { secret }
}
pub fn from_bytes(secret: [u8; 32]) -> Self {
Self { secret }
}
pub fn public_key(&self) -> X25519PublicKey {
X25519PublicKey(backend::public_key(self.secret))
}
pub fn dh(&self, peer: &X25519PublicKey) -> SharedSecret<32> {
SharedSecret::new(backend::dh(self.secret, peer.0))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.secret
}
}
impl Drop for SoftwareX25519PrivateKey {
fn drop(&mut self) {
crate::zeroize::zeroize_array(&mut self.secret);
}
}
#[cfg(not(test))]
impl fmt::Debug for SoftwareX25519PrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SoftwareX25519PrivateKey")
.finish_non_exhaustive()
}
}
#[cfg(test)]
impl fmt::Debug for SoftwareX25519PrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SoftwareX25519PrivateKey")
.field("secret", &hex::encode(self.secret))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn h32(s: &str) -> [u8; 32] {
hex::decode(s).unwrap().try_into().unwrap()
}
#[test]
fn rfc7748_section_6_1_ecdh_known_answer() {
let alice_sk = h32("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a");
let alice_pk = h32("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a");
let bob_sk = h32("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb");
let bob_pk = h32("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");
let shared = h32("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");
let alice = SoftwareX25519PrivateKey::from_bytes(alice_sk);
let bob = SoftwareX25519PrivateKey::from_bytes(bob_sk);
assert_eq!(alice.public_key().as_bytes(), &alice_pk);
assert_eq!(bob.public_key().as_bytes(), &bob_pk);
let ss_ab = alice.dh(&X25519PublicKey::from_bytes(&bob_pk).unwrap());
let ss_ba = bob.dh(&X25519PublicKey::from_bytes(&alice_pk).unwrap());
assert_eq!(ss_ab.as_bytes(), &shared);
assert_eq!(ss_ba.as_bytes(), &shared);
}
#[test]
fn rfc7748_section_5_2_scalarmult_known_answer() {
let scalar = h32("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
let u = h32("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
let expected = h32("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552");
let sk = SoftwareX25519PrivateKey::from_bytes(scalar);
let out = sk.dh(&X25519PublicKey::from_bytes(&u).unwrap());
assert_eq!(out.as_bytes(), &expected);
}
#[test]
fn dh_is_symmetric() {
let sk1 = SoftwareX25519PrivateKey::generate(rand::rng());
let pk1 = sk1.public_key();
let sk2 = SoftwareX25519PrivateKey::generate(rand::rng());
let pk2 = sk2.public_key();
assert_eq!(sk1.dh(&pk2), sk2.dh(&pk1));
}
#[test]
fn dh_different_peers_produce_different_secrets() {
let sk = SoftwareX25519PrivateKey::generate(rand::rng());
let peer1 = SoftwareX25519PrivateKey::generate(rand::rng()).public_key();
let peer2 = SoftwareX25519PrivateKey::generate(rand::rng()).public_key();
assert_ne!(sk.dh(&peer1), sk.dh(&peer2));
}
#[test]
fn public_key_round_trip() {
let sk = SoftwareX25519PrivateKey::generate(rand::rng());
let pk = sk.public_key();
let pk2 = X25519PublicKey::from_bytes(pk.as_bytes()).unwrap();
assert_eq!(pk, pk2);
}
#[test]
fn public_key_wrong_length_rejected() {
let err = X25519PublicKey::from_bytes(&[0u8; 31]).unwrap_err();
assert!(matches!(err, Error::InvalidPublicKeyLength(31)));
let err = X25519PublicKey::from_bytes(&[0u8; 33]).unwrap_err();
assert!(matches!(err, Error::InvalidPublicKeyLength(33)));
}
#[test]
fn low_order_point_yields_zero_secret_per_spec() {
let sk = SoftwareX25519PrivateKey::generate(rand::rng());
let low_order = X25519PublicKey::from_bytes(&[0u8; 32]).unwrap();
assert_eq!(sk.dh(&low_order).as_bytes(), &[0u8; 32]);
}
proptest! {
#[test]
fn dh_never_panics(peer_bytes in any::<[u8; 32]>(), secret in any::<[u8; 32]>()) {
let sk = SoftwareX25519PrivateKey::from_bytes(secret);
let peer = X25519PublicKey::from_bytes(&peer_bytes).unwrap();
let _ = sk.dh(&peer);
}
}
}