Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/nym-kkt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ tokio-util = { workspace = true, features = ["codec"] }

# internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]}
nym-sphinx = { path = "../nymsphinx" }

libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"]}
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }

rand = "0.9.2"
curve25519-dalek = {version = "4.1.3", features = ["rand_core", "serde"] }
Expand Down
8 changes: 4 additions & 4 deletions common/nym-kkt/src/ciphersuite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use nym_crypto::asymmetric::ed25519;

use crate::error::KKTError;

pub const HASH_LEN_256: u8 = 32;
pub const HASH_LEN_256: usize = 32;
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;

pub const CURVE25519_KEY_LEN: usize = 32;
Expand Down Expand Up @@ -172,7 +172,7 @@ impl Ciphersuite {
l
}
}
None => HASH_LEN_256,
None => HASH_LEN_256 as u8,
};
Ok(Self {
hash_function,
Expand Down Expand Up @@ -218,8 +218,8 @@ impl Ciphersuite {
HashFunction::SHAKE128 => 2,
HashFunction::SHA256 => 3,
},
match self.hash_length {
HASH_LEN_256 => 0,
match self.hash_length as usize {
HASH_LEN_256 => 0u8,
_ => self.hash_length,
},
match self.signature_scheme {
Expand Down
253 changes: 190 additions & 63 deletions common/nym-kkt/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -1,95 +1,222 @@
use core::hash;

use blake3::{Hash, Hasher};
use curve25519_dalek::digest::DynDigest;
use libcrux_psq::traits::Ciphertext;
use nym_crypto::symmetric::aead::{AeadKey, Nonce};
use nym_crypto::{
aes::Aes256,
asymmetric::x25519::{self, PrivateKey, PublicKey},
generic_array::GenericArray,
Aes256GcmSiv,
};
// use rand::{CryptoRng, RngCore};
use blake3::Hasher;

use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};

use nym_sphinx::{PrivateKey, PublicKey};

use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;

use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
use crate::{
ciphersuite::{CURVE25519_KEY_LEN, HASH_LEN_256},
context::KKTContext,
error::KKTError,
frame::KKTFrame,
};

#[derive(Clone, Copy, Zeroize)]
pub struct KKTSessionSecret([u8; 32]);

impl KKTSessionSecret {
pub fn new(remote_public_key: &PublicKey) -> (Self, PublicKey) {
// this doesn't use the newer rand crate
let ephemeral_private_key = PrivateKey::random();
let ephemeral_public_key = PublicKey::from(&ephemeral_private_key);

(
Self::derive(&ephemeral_private_key, &remote_public_key),
ephemeral_public_key,
)
}
pub fn from_bytes(secret: [u8; 32]) -> Self {
Self(secret)
}
pub fn try_derive(private_key: &PrivateKey, public_key: &[u8]) -> Result<Self, KKTError> {
let mut pub_key: [u8; 32] = [0u8; 32];
pub_key.copy_from_slice(&public_key[0..CURVE25519_KEY_LEN]);

// Todo: check validity of pk...
let pk = PublicKey::from(pub_key);
Ok(Self::derive(private_key, &pk))
}

pub fn derive(private_key: &PrivateKey, public_key: &PublicKey) -> Self {
let mut shared_secret = private_key.diffie_hellman(&public_key);

let mut hasher = Hasher::new();

hasher.update(shared_secret.as_bytes());
shared_secret.zeroize();

use crate::error::KKTError;
Self(hasher.finalize().as_bytes().to_owned())
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}

fn generate_round_trip_symmetric_key<R>(
pub fn encrypt_initial_kkt_frame<R>(
rng: &mut R,
remote_public_key: &PublicKey,
) -> ([u8; 64], [u8; 32])
kkt_frame: &KKTFrame,
) -> Result<(KKTSessionSecret, Vec<u8>), KKTError>
where
R: CryptoRng + RngCore,
{
let (session_secret_key, ephemeral_public_key) = KKTSessionSecret::new(remote_public_key);

let mut encrypted_frame =
encrypt_kkt_frame(rng, &session_secret_key, &kkt_frame, b"KKT_INITIAL_FRAME")?;

let mut output_buffer = Vec::with_capacity(encrypted_frame.len() + CURVE25519_KEY_LEN);
output_buffer.extend_from_slice(ephemeral_public_key.as_bytes());
output_buffer.append(&mut encrypted_frame);

// [ 32 | 12 | ciphertext | 16];
// [eph_pub_key | nonce | ciphertext | tag];
Ok((session_secret_key, output_buffer))
}

pub fn decrypt_initial_kkt_frame(
responder_private_key: &PrivateKey,
encrypted_frame_bytes: &[u8],
) -> Result<(KKTSessionSecret, KKTFrame, KKTContext), KKTError> {
if encrypted_frame_bytes.len() < CURVE25519_KEY_LEN + TAG_LEN + NONCE_LEN {
return Err(KKTError::AEADError {
info: "Encrypted KKT Frame is too short.",
});
} else {
let shared_secret = KKTSessionSecret::try_derive(
responder_private_key,
&encrypted_frame_bytes[0..CURVE25519_KEY_LEN],
)?;

let (kkt_frame, kkt_context) = decrypt_kkt_frame(
&shared_secret,
&encrypted_frame_bytes[CURVE25519_KEY_LEN..],
b"KKT_INITIAL_FRAME",
)?;
Ok((shared_secret, kkt_frame, kkt_context))
}
}

pub fn encrypt_kkt_frame<R>(
rng: &mut R,
secret_key: &KKTSessionSecret,
kkt_frame: &KKTFrame,
aad: &[u8],
) -> Result<Vec<u8>, KKTError>
where
R: CryptoRng + RngCore,
{
let mut s = x25519::PrivateKey::new(rng);
let gs = s.public_key();
let kkt_frame_bytes = kkt_frame.to_bytes();

let mut gbs = s.diffie_hellman(remote_public_key);
s.zeroize();
// generate nonce
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
rng.fill_bytes(&mut nonce);

let mut message: [u8; 64] = [0u8; 64];
message[0..32].clone_from_slice(gs.as_bytes());
let mut ciphertext = encrypt(&secret_key.as_bytes(), &kkt_frame_bytes, &aad, &nonce)?;

let mut hasher = Hasher::new();
// [ 12 | ciphertext | 16];
// [nonce | ciphertext | tag];
let mut output_buffer: Vec<u8> =
Vec::with_capacity(NONCE_LEN + kkt_frame_bytes.len() + TAG_LEN);

hasher.update(&gbs);
gbs.zeroize();
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
output_buffer.extend_from_slice(&nonce);
output_buffer.append(&mut ciphertext);

hasher.update(remote_public_key.as_bytes());
hasher.update(gs.as_bytes());
Ok(output_buffer)
}

hasher.finalize_into_reset(&mut message[32..64]);
// kkt_frame_bytes should look like this
// [ 12 | ciphertext | 16];
// [nonce | ciphertext | tag];
pub fn decrypt_kkt_frame(
secret_key: &KKTSessionSecret,
kkt_frame_bytes: &[u8],
aad: &[u8],
) -> Result<(KKTFrame, KKTContext), KKTError> {
let mut nonce: [u8; NONCE_LEN] = [0u8; NONCE_LEN];
nonce.copy_from_slice(&kkt_frame_bytes[0..NONCE_LEN]);

let plaintext = decrypt(
secret_key.as_bytes(),
&kkt_frame_bytes[NONCE_LEN..],
aad,
&nonce,
)?;

KKTFrame::from_bytes(&plaintext)
}

(message, key)
fn encrypt(
secret_key: &[u8; 32],
plaintext: &[u8],
aad: &[u8],
nonce: &[u8; NONCE_LEN],
) -> Result<Vec<u8>, KKTError> {
let mut output_buffer = vec![0; plaintext.len() + TAG_LEN];
libcrux_chacha20poly1305::encrypt(&secret_key, &plaintext, &mut output_buffer, &aad, &nonce)?;
Ok(output_buffer)
}

fn extract_shared_secret(b: &PrivateKey, message: &[u8; 64]) -> Result<[u8; 32], KKTError> {
let gs = PublicKey::from_bytes(&message[0..32])?;
fn decrypt(
secret_key: &[u8; 32],
ciphertext: &[u8],
aad: &[u8],
nonce: &[u8; NONCE_LEN],
) -> Result<Vec<u8>, KKTError> {
let mut output_buffer = vec![0; ciphertext.len() - TAG_LEN];
libcrux_chacha20poly1305::decrypt(&secret_key, &mut output_buffer, &ciphertext, &aad, &nonce)?;
Ok(output_buffer)
}

let mut gsb = b.diffie_hellman(&gs);
#[cfg(test)]
mod test {
use rand::{RngCore, rng};

let mut hasher = Hasher::new();
hasher.update(&gsb);
gsb.zeroize();
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
use crate::{
ciphersuite::HASH_LEN_256,
encryption::{KKTSessionSecret, decrypt, encrypt},
key_utils::generate_keypair_x25519,
};

hasher.update(b.public_key().as_bytes());
hasher.update(gs.as_bytes());
#[test]
fn test_keygen() {
let responder_x25519_keypair = generate_keypair_x25519();

// This runs in constant time
if hasher.finalize() == message[32..64] {
Ok(key)
} else {
Err(KKTError::X25519Error {
info: format!("Symmetric Key Hash Validation Error"),
})
let (session_secret_key, ephemeral_public_key) =
KKTSessionSecret::new(&responder_x25519_keypair.1);

let shared_secret = KKTSessionSecret::try_derive(
&responder_x25519_keypair.0,
&ephemeral_public_key.as_bytes().as_slice(),
)
.unwrap();

assert_eq!(shared_secret.as_bytes(), session_secret_key.as_bytes())
}
}

fn encrypt(mut key: [u8; 32], message: &[u8]) -> Result<Vec<u8>, KKTError> {
// The empty nonce is fine since we use the key once.
let nonce = Nonce::<Aes256GcmSiv>::from_slice(&[]);
#[test]
fn test_encryption() {
let mut rng = rng();

let ciphertext =
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
let mut secret_key = [0u8; HASH_LEN_256];
rng.fill_bytes(&mut secret_key);

key.zeroize();
let mut plaintext = vec![0; 100];
rng.fill_bytes(&mut plaintext);

Ok(ciphertext)
}
let mut nonce = [0; 12];
rng.fill_bytes(&mut nonce);

fn decrypt(key: [u8; 32], ciphertext: Vec<u8>) -> Vec<u8> {
// The empty nonce is fine since we use the key once.
let nonce = Nonce::<Aes256>::from_slice(&[]);
let mut aad = vec![0; 124];
rng.fill_bytes(&mut aad);

let ciphertext =
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
let ciphertext = encrypt(&secret_key, &plaintext, &aad, &nonce).unwrap();

key.zeroize();
let o_plaintext = decrypt(&secret_key, &ciphertext, &aad, &nonce).unwrap();

Ok(ciphertext)
assert_eq!(o_plaintext, plaintext)
}
}
36 changes: 36 additions & 0 deletions common/nym-kkt/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: Apache-2.0

use std::fmt::Debug;

use nym_crypto::asymmetric::x25519::KeyRecoveryError;
use thiserror::Error;

use crate::context::KKTStatus;
Expand Down Expand Up @@ -40,10 +43,19 @@ pub enum KKTError {
#[error("{}", info)]
X25519Error { info: &'static str },

#[error("{}", info)]
AEADError { info: &'static str },

#[error("Generic libcrux error")]
LibcruxError,
}

impl From<KeyRecoveryError> for KKTError {
fn from(err: KeyRecoveryError) -> Self {
err.into()
}
}

impl From<libcrux_kem::Error> for KKTError {
fn from(err: libcrux_kem::Error) -> Self {
match err {
Expand Down Expand Up @@ -83,3 +95,27 @@ impl From<libcrux_ecdh::Error> for KKTError {
}
}
}
impl From<libcrux_chacha20poly1305::AeadError> for KKTError {
fn from(err: libcrux_chacha20poly1305::AeadError) -> Self {
KKTError::KEMError {
info: match err {
libcrux_chacha20poly1305::AeadError::PlaintextTooLarge => {
"Plaintext is longer than u32::MAX"
}
libcrux_chacha20poly1305::AeadError::CiphertextTooLarge => {
"Ciphertext is longer than u32::MAX"
}
libcrux_chacha20poly1305::AeadError::AadTooLarge => "Aad is longer than u32::MAX",
libcrux_chacha20poly1305::AeadError::CiphertextTooShort => {
"The provided destination ciphertext does not fit the ciphertext and tag"
}
libcrux_chacha20poly1305::AeadError::PlaintextTooShort => {
"The provided destination plaintext is too short to fit the decrypted plaintext"
}
libcrux_chacha20poly1305::AeadError::InvalidCiphertext => {
"The ciphertext is not a valid encryption under the given key and nonce."
}
},
}
}
}
Loading
Loading