use crate::core::error::{PaserkError, PaserkResult};
pub const EPHEMERAL_PK_SIZE: usize = 32;
pub const SEAL_CIPHERTEXT_SIZE: usize = 32;
pub const SEAL_TAG_SIZE: usize = 32;
const SEAL_NONCE_SIZE: usize = 24;
pub const SEAL_DATA_SIZE: usize = SEAL_TAG_SIZE + EPHEMERAL_PK_SIZE + SEAL_CIPHERTEXT_SIZE;
pub type SealOutput = (
[u8; SEAL_TAG_SIZE],
[u8; EPHEMERAL_PK_SIZE],
[u8; SEAL_CIPHERTEXT_SIZE],
);
const SEAL_EK_DOMAIN_BYTE: u8 = 0x01;
const SEAL_AK_DOMAIN_BYTE: u8 = 0x02;
#[cfg(any(feature = "k2", feature = "k4"))]
pub fn seal_k2k4(
plaintext_key: &[u8; 32],
recipient_pk: &[u8; 32],
header: &str,
) -> PaserkResult<SealOutput> {
use blake2::digest::{FixedOutput, KeyInit, Update};
use blake2::{Blake2b, Blake2bMac};
use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::XChaCha20;
use rand_core::{OsRng, TryRngCore};
use x25519_dalek::{PublicKey, StaticSecret};
type Blake2b32 = Blake2b<blake2::digest::consts::U32>;
type Blake2b24 = Blake2b<blake2::digest::consts::U24>;
type Blake2bMac32 = Blake2bMac<blake2::digest::consts::U32>;
let mut ephemeral_secret_bytes = [0u8; 32];
OsRng
.try_fill_bytes(&mut ephemeral_secret_bytes)
.map_err(|_| PaserkError::CryptoError)?;
let ephemeral_secret = StaticSecret::from(ephemeral_secret_bytes);
let ephemeral_pk: [u8; 32] = PublicKey::from(&ephemeral_secret).to_bytes();
let recipient_public = PublicKey::from(*recipient_pk);
let shared_secret = ephemeral_secret.diffie_hellman(&recipient_public);
let mut ek_hasher = <Blake2b32 as Default>::default();
<Blake2b32 as Update>::update(&mut ek_hasher, &[SEAL_EK_DOMAIN_BYTE]);
<Blake2b32 as Update>::update(&mut ek_hasher, header.as_bytes());
<Blake2b32 as Update>::update(&mut ek_hasher, shared_secret.as_bytes());
<Blake2b32 as Update>::update(&mut ek_hasher, &ephemeral_pk);
<Blake2b32 as Update>::update(&mut ek_hasher, recipient_pk);
let mut encryption_key: [u8; 32] = <Blake2b32 as FixedOutput>::finalize_fixed(ek_hasher).into();
let mut ak_hasher = <Blake2b32 as Default>::default();
<Blake2b32 as Update>::update(&mut ak_hasher, &[SEAL_AK_DOMAIN_BYTE]);
<Blake2b32 as Update>::update(&mut ak_hasher, header.as_bytes());
<Blake2b32 as Update>::update(&mut ak_hasher, shared_secret.as_bytes());
<Blake2b32 as Update>::update(&mut ak_hasher, &ephemeral_pk);
<Blake2b32 as Update>::update(&mut ak_hasher, recipient_pk);
let mut auth_key: [u8; 32] = <Blake2b32 as FixedOutput>::finalize_fixed(ak_hasher).into();
let mut n_hasher = <Blake2b24 as Default>::default();
<Blake2b24 as Update>::update(&mut n_hasher, &ephemeral_pk);
<Blake2b24 as Update>::update(&mut n_hasher, recipient_pk);
let nonce: [u8; SEAL_NONCE_SIZE] = <Blake2b24 as FixedOutput>::finalize_fixed(n_hasher).into();
let mut ciphertext = *plaintext_key;
let mut cipher = XChaCha20::new(&encryption_key.into(), &nonce.into());
cipher.apply_keystream(&mut ciphertext);
let mut tag_mac = <Blake2bMac32 as KeyInit>::new_from_slice(&auth_key)
.map_err(|_| PaserkError::CryptoError)?;
<Blake2bMac32 as Update>::update(&mut tag_mac, header.as_bytes());
<Blake2bMac32 as Update>::update(&mut tag_mac, &ephemeral_pk);
<Blake2bMac32 as Update>::update(&mut tag_mac, &ciphertext);
let tag: [u8; SEAL_TAG_SIZE] = <Blake2bMac32 as FixedOutput>::finalize_fixed(tag_mac).into();
zeroize::Zeroize::zeroize(&mut ephemeral_secret_bytes);
zeroize::Zeroize::zeroize(&mut encryption_key);
zeroize::Zeroize::zeroize(&mut auth_key);
Ok((tag, ephemeral_pk, ciphertext))
}
#[cfg(any(feature = "k2", feature = "k4"))]
pub fn unseal_k2k4(
tag: &[u8; SEAL_TAG_SIZE],
ephemeral_pk: &[u8; EPHEMERAL_PK_SIZE],
ciphertext: &[u8; SEAL_CIPHERTEXT_SIZE],
recipient_sk: &[u8; 64],
header: &str,
) -> PaserkResult<[u8; 32]> {
use blake2::digest::{FixedOutput, KeyInit, Update};
use blake2::{Blake2b, Blake2bMac};
use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::XChaCha20;
use ed25519_dalek::SigningKey;
use subtle::ConstantTimeEq;
use x25519_dalek::{PublicKey, StaticSecret};
type Blake2b32 = Blake2b<blake2::digest::consts::U32>;
type Blake2b24 = Blake2b<blake2::digest::consts::U24>;
type Blake2bMac32 = Blake2bMac<blake2::digest::consts::U32>;
let ed_secret =
SigningKey::from_keypair_bytes(recipient_sk).map_err(|_| PaserkError::InvalidKey)?;
let x25519_secret = StaticSecret::from(ed_secret.to_scalar_bytes());
let x25519_recipient_pk: [u8; 32] = PublicKey::from(&x25519_secret).to_bytes();
let ephemeral_public = PublicKey::from(*ephemeral_pk);
let shared_secret = x25519_secret.diffie_hellman(&ephemeral_public);
let mut ek_hasher = <Blake2b32 as Default>::default();
<Blake2b32 as Update>::update(&mut ek_hasher, &[SEAL_EK_DOMAIN_BYTE]);
<Blake2b32 as Update>::update(&mut ek_hasher, header.as_bytes());
<Blake2b32 as Update>::update(&mut ek_hasher, shared_secret.as_bytes());
<Blake2b32 as Update>::update(&mut ek_hasher, ephemeral_pk);
<Blake2b32 as Update>::update(&mut ek_hasher, &x25519_recipient_pk);
let mut encryption_key: [u8; 32] = <Blake2b32 as FixedOutput>::finalize_fixed(ek_hasher).into();
let mut ak_hasher = <Blake2b32 as Default>::default();
<Blake2b32 as Update>::update(&mut ak_hasher, &[SEAL_AK_DOMAIN_BYTE]);
<Blake2b32 as Update>::update(&mut ak_hasher, header.as_bytes());
<Blake2b32 as Update>::update(&mut ak_hasher, shared_secret.as_bytes());
<Blake2b32 as Update>::update(&mut ak_hasher, ephemeral_pk);
<Blake2b32 as Update>::update(&mut ak_hasher, &x25519_recipient_pk);
let mut auth_key: [u8; 32] = <Blake2b32 as FixedOutput>::finalize_fixed(ak_hasher).into();
let mut tag_mac = <Blake2bMac32 as KeyInit>::new_from_slice(&auth_key)
.map_err(|_| PaserkError::CryptoError)?;
<Blake2bMac32 as Update>::update(&mut tag_mac, header.as_bytes());
<Blake2bMac32 as Update>::update(&mut tag_mac, ephemeral_pk);
<Blake2bMac32 as Update>::update(&mut tag_mac, ciphertext);
let computed_tag: [u8; SEAL_TAG_SIZE] =
<Blake2bMac32 as FixedOutput>::finalize_fixed(tag_mac).into();
if computed_tag.ct_eq(tag).into() {
let mut n_hasher = <Blake2b24 as Default>::default();
<Blake2b24 as Update>::update(&mut n_hasher, ephemeral_pk);
<Blake2b24 as Update>::update(&mut n_hasher, &x25519_recipient_pk);
let nonce: [u8; SEAL_NONCE_SIZE] =
<Blake2b24 as FixedOutput>::finalize_fixed(n_hasher).into();
let mut plaintext = *ciphertext;
let mut cipher = XChaCha20::new(&encryption_key.into(), &nonce.into());
cipher.apply_keystream(&mut plaintext);
zeroize::Zeroize::zeroize(&mut encryption_key);
zeroize::Zeroize::zeroize(&mut auth_key);
Ok(plaintext)
} else {
zeroize::Zeroize::zeroize(&mut encryption_key);
zeroize::Zeroize::zeroize(&mut auth_key);
Err(PaserkError::AuthenticationFailed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "k4")]
fn generate_test_keypair() -> PaserkResult<(ed25519_dalek::SigningKey, [u8; 64], [u8; 32])> {
use ed25519_dalek::SigningKey;
use rand_core::{OsRng, TryRngCore};
use x25519_dalek::{PublicKey, StaticSecret};
let mut seed = [0u8; 32];
OsRng
.try_fill_bytes(&mut seed)
.map_err(|_| PaserkError::CryptoError)?;
let signing_key = SigningKey::from_bytes(&seed);
let keypair_bytes = signing_key.to_keypair_bytes();
let x25519_secret = StaticSecret::from(signing_key.to_scalar_bytes());
let x25519_public = PublicKey::from(&x25519_secret).to_bytes();
Ok((signing_key, keypair_bytes, x25519_public))
}
#[test]
#[cfg(feature = "k4")]
fn test_seal_unseal_roundtrip() -> PaserkResult<()> {
let (signing_key, secret_key_bytes, x25519_public) = generate_test_keypair()?;
let _ = signing_key;
let plaintext_key = [0x42u8; 32];
let header = "k4.seal.";
let (tag, ephemeral_pk, ciphertext) = seal_k2k4(&plaintext_key, &x25519_public, header)?;
assert_ne!(ciphertext, plaintext_key);
let unsealed = unseal_k2k4(&tag, &ephemeral_pk, &ciphertext, &secret_key_bytes, header)?;
assert_eq!(unsealed, plaintext_key);
Ok(())
}
#[test]
#[cfg(feature = "k4")]
fn test_seal_produces_different_output() -> PaserkResult<()> {
let (_, _, x25519_public) = generate_test_keypair()?;
let plaintext_key = [0x42u8; 32];
let header = "k4.seal.";
let (tag1, epk1, ct1) = seal_k2k4(&plaintext_key, &x25519_public, header)?;
let (tag2, epk2, ct2) = seal_k2k4(&plaintext_key, &x25519_public, header)?;
assert_ne!(epk1, epk2);
assert_ne!(ct1, ct2);
assert_ne!(tag1, tag2);
Ok(())
}
#[test]
#[cfg(feature = "k4")]
fn test_unseal_wrong_key() -> PaserkResult<()> {
let (_, _, x25519_public1) = generate_test_keypair()?;
let (_, secret_key2_bytes, _) = generate_test_keypair()?;
let plaintext_key = [0x42u8; 32];
let header = "k4.seal.";
let (tag, ephemeral_pk, ciphertext) = seal_k2k4(&plaintext_key, &x25519_public1, header)?;
let result = unseal_k2k4(&tag, &ephemeral_pk, &ciphertext, &secret_key2_bytes, header);
assert!(matches!(result, Err(PaserkError::AuthenticationFailed)));
Ok(())
}
#[test]
#[cfg(feature = "k4")]
fn test_unseal_modified_tag() -> PaserkResult<()> {
let (_, secret_key_bytes, x25519_public) = generate_test_keypair()?;
let plaintext_key = [0x42u8; 32];
let header = "k4.seal.";
let (mut tag, ephemeral_pk, ciphertext) =
seal_k2k4(&plaintext_key, &x25519_public, header)?;
tag[0] ^= 0xff;
let result = unseal_k2k4(&tag, &ephemeral_pk, &ciphertext, &secret_key_bytes, header);
assert!(matches!(result, Err(PaserkError::AuthenticationFailed)));
Ok(())
}
}