use zeroize::Zeroizing;
use crate::CryptoError;
pub const SHARED_SECRET_LEN: usize = 32;
#[derive(Clone)]
pub struct SharedSecret(Zeroizing<[u8; SHARED_SECRET_LEN]>);
impl SharedSecret {
#[must_use]
pub fn from_be_bytes(bytes: [u8; SHARED_SECRET_LEN]) -> Self {
Self(Zeroizing::new(bytes))
}
pub(crate) fn expose(&self) -> &[u8; SHARED_SECRET_LEN] {
&self.0
}
#[must_use]
pub fn ct_eq(&self, other: &Self) -> bool {
use subtle::ConstantTimeEq as _;
bool::from(self.0.ct_eq(&*other.0))
}
}
impl core::fmt::Debug for SharedSecret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("SharedSecret(<скрыт>)")
}
}
pub trait KeyAgreement {
fn public_key(&self) -> Vec<u8>;
fn agree(&self, peer_public: &[u8]) -> Result<SharedSecret, CryptoError>;
}
pub struct X25519Agreement<'a> {
secret: &'a crate::secret::X25519Secret,
}
impl core::fmt::Debug for X25519Agreement<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("X25519Agreement(<ключ скрыт>)")
}
}
impl<'a> X25519Agreement<'a> {
#[must_use]
pub fn new(secret: &'a crate::secret::X25519Secret) -> Self {
Self { secret }
}
}
impl KeyAgreement for X25519Agreement<'_> {
fn public_key(&self) -> Vec<u8> {
let sk = x25519_dalek::StaticSecret::from(*self.secret.expose());
x25519_dalek::PublicKey::from(&sk).to_bytes().to_vec()
}
fn agree(&self, peer_public: &[u8]) -> Result<SharedSecret, CryptoError> {
let peer: [u8; 32] = peer_public.try_into().map_err(|_| CryptoError::BadLength)?;
let sk = x25519_dalek::StaticSecret::from(*self.secret.expose());
let shared = sk.diffie_hellman(&x25519_dalek::PublicKey::from(peer));
Ok(SharedSecret::from_be_bytes(*shared.as_bytes()))
}
}
pub struct P256Agreement {
secret: p256::SecretKey,
}
impl core::fmt::Debug for P256Agreement {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("P256Agreement(<ключ скрыт>)")
}
}
impl P256Agreement {
pub fn generate<R: rand_core::CryptoRng + ?Sized>(rng: &mut R) -> Self {
use p256::elliptic_curve::Generate as _;
Self { secret: p256::SecretKey::generate_from_rng(rng) }
}
pub fn from_be_bytes(bytes: &[u8; 32]) -> Result<Self, CryptoError> {
let secret = p256::SecretKey::from_slice(bytes).map_err(|_| CryptoError::BadKey)?;
Ok(Self { secret })
}
}
impl KeyAgreement for P256Agreement {
fn public_key(&self) -> Vec<u8> {
use p256::elliptic_curve::sec1::ToSec1Point as _;
self.secret.public_key().to_sec1_point(false).to_bytes().to_vec()
}
fn agree(&self, peer_public: &[u8]) -> Result<SharedSecret, CryptoError> {
let peer =
p256::PublicKey::from_sec1_bytes(peer_public).map_err(|_| CryptoError::BadKey)?;
let shared = p256::ecdh::diffie_hellman(self.secret.to_nonzero_scalar(), peer.as_affine());
let bytes: [u8; SHARED_SECRET_LEN] =
shared.raw_secret_bytes().as_slice().try_into().map_err(|_| CryptoError::BadLength)?;
Ok(SharedSecret::from_be_bytes(bytes))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)]
mod tests {
use super::*;
fn p256_pair(byte: u8) -> P256Agreement {
P256Agreement::from_be_bytes(&[byte; 32]).expect("скаляр в диапазоне")
}
#[test]
fn a_p256_public_key_is_sixty_five_bytes_of_uncompressed_point() {
let pk = p256_pair(0x11).public_key();
assert_eq!(pk.len(), 65, "форма на проводе задана форматом: несжатая точка");
assert_eq!(pk.first(), Some(&0x04), "префикс несжатой точки SEC1");
}
#[test]
fn both_sides_of_a_p256_agreement_reach_the_same_secret() {
let a = p256_pair(0x11);
let b = p256_pair(0x22);
let from_a = a.agree(&b.public_key()).unwrap();
let from_b = b.agree(&a.public_key()).unwrap();
assert_eq!(from_a.expose(), from_b.expose());
}
#[test]
fn both_sides_of_an_x25519_agreement_reach_the_same_secret() {
let sa = crate::secret::X25519Secret::from_bytes([0x33; 32]);
let sb = crate::secret::X25519Secret::from_bytes([0x44; 32]);
let a = X25519Agreement::new(&sa);
let b = X25519Agreement::new(&sb);
let from_a = a.agree(&b.public_key()).unwrap();
let from_b = b.agree(&a.public_key()).unwrap();
assert_eq!(from_a.expose(), from_b.expose());
}
#[test]
fn a_point_off_the_curve_is_refused_rather_than_multiplied() {
let a = p256_pair(0x11);
let mut bogus = vec![0x04u8];
bogus.extend_from_slice(&[0xab; 64]);
assert_eq!(bogus.len(), 65, "длина верна: отказ обязан быть по кривой");
assert!(a.agree(&bogus).is_err(), "точка вне кривой принята");
assert!(a.agree(&[0x00]).is_err(), "точка в бесконечности принята");
}
#[test]
fn a_public_key_of_the_wrong_length_is_refused() {
let p = p256_pair(0x11);
assert!(p.agree(&[0x04; 33]).is_err(), "P-256 принял 33 байта с префиксом 0x04");
assert!(p.agree(&[0x00; 32]).is_err(), "P-256 принял 32 байта");
let s = crate::secret::X25519Secret::from_bytes([0x55; 32]);
let x = X25519Agreement::new(&s);
assert!(x.agree(&[0x00; 65]).is_err(), "X25519 принял 65 байт");
}
#[test]
fn a_compressed_p256_point_is_accepted_here_and_yields_the_same_secret() {
use p256::elliptic_curve::sec1::ToSec1Point as _;
let a = p256_pair(0x11);
let b = p256_pair(0x22);
let uncompressed = b.public_key();
let compressed = b.secret.public_key().to_sec1_point(true).to_bytes().to_vec();
assert_eq!(compressed.len(), 33, "сжатая точка SEC1 — 33 байта");
assert!(
matches!(compressed.first(), Some(0x02 | 0x03)),
"префикс сжатой точки: {:?}",
compressed.first()
);
let from_uncompressed = a.agree(&uncompressed).expect("несжатая форма отвергнута");
let from_compressed =
a.agree(&compressed).expect("сжатая форма отвергнута: поведение слоя изменилось");
assert!(
from_compressed.ct_eq(&from_uncompressed),
"сжатая и несжатая формы одного ключа дали разные секреты"
);
}
#[test]
fn a_shared_secret_never_prints_itself() {
let secret = SharedSecret::from_be_bytes([0xab; 32]);
let shown = format!("{secret:?}");
assert!(!shown.contains("ab"), "секрет попал в Debug: {shown}");
assert!(shown.contains("скрыт"));
}
}