use std::cmp::PartialEq;
use hkdf::Hkdf;
use log::debug;
use rand::{CryptoRng, Rng};
use sha2::Sha256;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use crate::pgp::{
crypto::{aes_kw, Decryptor},
errors::{ensure, Result},
ser::Serialize,
types::X25519PublicParams,
};
pub const KEY_LEN: usize = 32;
#[derive(Clone, derive_more::Debug, Zeroize, ZeroizeOnDrop)]
pub struct SecretKey {
#[debug("..")]
secret: StaticSecret,
}
impl From<&SecretKey> for X25519PublicParams {
fn from(value: &SecretKey) -> Self {
Self {
key: PublicKey::from(&value.secret),
}
}
}
impl PartialEq for SecretKey {
fn eq(&self, other: &Self) -> bool {
self.secret.as_bytes().eq(other.secret.as_bytes())
}
}
impl Eq for SecretKey {}
impl SecretKey {
pub fn generate<R: Rng + CryptoRng>(mut rng: R) -> Self {
let mut secret_key_bytes = Zeroizing::new([0u8; 32]);
rng.fill_bytes(&mut *secret_key_bytes);
let secret = StaticSecret::from(*secret_key_bytes);
SecretKey { secret }
}
pub fn try_from_bytes(secret: [u8; KEY_LEN]) -> Result<Self> {
let secret = x25519_dalek::StaticSecret::from(secret);
Ok(Self { secret })
}
pub fn as_bytes(&self) -> &[u8; KEY_LEN] {
self.secret.as_bytes()
}
}
impl Serialize for SecretKey {
fn to_writer<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
let x = self.as_bytes();
writer.write_all(x)?;
Ok(())
}
fn write_len(&self) -> usize {
KEY_LEN
}
}
pub struct EncryptionFields<'a> {
pub ephemeral_public_point: [u8; 32],
pub recipient_public: [u8; 32],
pub encrypted_session_key: &'a [u8],
}
impl Decryptor for SecretKey {
type EncryptionFields<'a> = EncryptionFields<'a>;
fn decrypt(&self, data: Self::EncryptionFields<'_>) -> Result<Vec<u8>> {
debug!("X25519 decrypt");
let shared_secret = {
let their_public = x25519_dalek::PublicKey::from(data.ephemeral_public_point);
let our_secret = &self.secret;
let shared_secret = our_secret.diffie_hellman(&their_public);
shared_secret.to_bytes()
};
derive_session_key(
data.ephemeral_public_point,
data.recipient_public,
shared_secret,
data.encrypted_session_key,
)
}
}
pub fn derive_session_key(
ephemeral: [u8; 32],
recipient_public: [u8; 32],
shared_secret: [u8; 32],
encrypted_session_key: &[u8],
) -> Result<Vec<u8>> {
let okm = hkdf(&ephemeral, &recipient_public, &shared_secret)?;
let decrypted_key = aes_kw::unwrap(&okm, encrypted_session_key)?;
ensure!(!decrypted_key.is_empty(), "empty key is not valid");
Ok(decrypted_key)
}
pub fn hkdf(
ephemeral: &[u8; 32],
recipient_public: &[u8; 32],
shared_secret: &[u8; 32],
) -> Result<[u8; 16]> {
const INFO: &[u8] = b"OpenPGP X25519";
let mut input = vec![];
input.extend_from_slice(ephemeral);
input.extend_from_slice(recipient_public);
input.extend_from_slice(shared_secret);
let hk = Hkdf::<Sha256>::new(None, &input);
let mut okm = [0u8; 16];
hk.expand(INFO, &mut okm)
.expect("16 is a valid length for Sha256 to output");
Ok(okm)
}
pub fn encrypt<R: CryptoRng + Rng>(
mut rng: R,
recipient_public: &x25519_dalek::PublicKey,
plain: &[u8],
) -> Result<([u8; 32], Vec<u8>)> {
debug!("X25519 encrypt");
const MAX_SIZE: usize = 255;
ensure!(
plain.len() <= MAX_SIZE,
"unable to encrypt larger than {} bytes",
MAX_SIZE
);
let (ephemeral_public, shared_secret) = {
let mut ephemeral_secret_key_bytes = Zeroizing::new([0u8; 32]);
rng.fill_bytes(&mut *ephemeral_secret_key_bytes);
let our_secret = StaticSecret::from(*ephemeral_secret_key_bytes);
let shared_secret = our_secret.diffie_hellman(recipient_public);
let ephemeral_public = x25519_dalek::PublicKey::from(&our_secret);
(ephemeral_public, shared_secret)
};
let okm = hkdf(
ephemeral_public.as_bytes(),
recipient_public.as_bytes(),
shared_secret.as_bytes(),
)?;
let wrapped = aes_kw::wrap(&okm, plain)?;
Ok((ephemeral_public.to_bytes(), wrapped))
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaChaRng;
use super::*;
#[test]
fn x25519_hkdf() {
let ephemeral_key = "87cf18d5f1b53f817cce5a004cf393cc8958bddc065f25f84af509b17dd36764";
let ephemeral_key: [u8; 32] = hex::decode(ephemeral_key).unwrap().try_into().unwrap();
let _ephemeral_secret = "af1e43c0d123efe893a7d4d390f3a761e3fac33dfc7f3edaa830c9011352c779";
let public_key = "8693248367f9e5015db922f8f48095dda784987f2d5985b12fbad16caf5e4435";
let public_key: [u8; 32] = hex::decode(public_key).unwrap().try_into().unwrap();
let long_lived_private = "4d600a4f794d44775c57a26e0feefed558e9afffd6ad0d582d57fb2ba2dcedb8";
let long_lived_private: [u8; 32] =
hex::decode(long_lived_private).unwrap().try_into().unwrap();
let shared_point = "67e30e69cdc7bab2a2680d78aca46a2f8b6e2ae44d398bdc6f92c5ad4a492514";
let shared_point: [u8; 32] = hex::decode(shared_point).unwrap().try_into().unwrap();
let hkdf = "f66dadcff64592239b254539b64ff607";
let hkdf: [u8; 16] = hex::decode(hkdf).unwrap().try_into().unwrap();
let decrypted = "dd708f6fa1ed65114d68d2343e7c2f1d";
let decrypted: [u8; 16] = hex::decode(decrypted).unwrap().try_into().unwrap();
let esk = "dea355437956617901e06957fbca8a6a47a5b5153e8d3ab7";
let esk = hex::decode(esk).unwrap();
let okm = super::hkdf(&ephemeral_key, &public_key, &shared_point).unwrap();
assert_eq!(okm, hkdf);
let decrypted_key = aes_kw::unwrap(&okm, &esk).unwrap();
assert_eq!(decrypted_key, decrypted);
let sk = SecretKey {
secret: long_lived_private.into(),
};
let decrypted2 = sk
.decrypt(EncryptionFields {
ephemeral_public_point: ephemeral_key,
recipient_public: public_key,
encrypted_session_key: &esk,
})
.unwrap();
assert_eq!(decrypted_key, decrypted2);
}
#[test]
fn test_encrypt_decrypt() {
let mut rng = ChaChaRng::from_seed([0u8; 32]);
let skey = SecretKey::generate(&mut rng);
let pub_params: X25519PublicParams = (&skey).into();
for text_size in (8..=248).step_by(8) {
for _i in 0..10 {
let mut plain = vec![0u8; text_size];
rng.fill_bytes(&mut plain);
let (ephemeral, enc_sk) = encrypt(&mut rng, &pub_params.key, &plain[..]).unwrap();
let data = EncryptionFields {
ephemeral_public_point: ephemeral,
recipient_public: pub_params.key.to_bytes(),
encrypted_session_key: &enc_sk,
};
let decrypted = skey.decrypt(data).unwrap();
assert_eq!(&plain[..], &decrypted[..]);
}
}
}
impl Arbitrary for SecretKey {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
any::<[u8; 32]>()
.prop_map(|b| SecretKey {
secret: StaticSecret::from(b),
})
.boxed()
}
}
}