-
Notifications
You must be signed in to change notification settings - Fork 0
feat: added bincode serialization for the EncryptedMessage type
#8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
df3931e
692d8b9
ff3db96
98d099a
a8efebb
757035b
7f836b9
d31ecb4
ab5bd9e
75893b2
8413c22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,16 @@ | ||
| [package] | ||
| name = "ntor" | ||
| version = "0.1.0" | ||
| version = "0.1.2" | ||
| edition = "2024" | ||
|
|
||
| [dependencies] | ||
| ring = "0.17.8" | ||
| aes-gcm = "0.10" | ||
| bincode = "2.0.1" | ||
| curve25519-dalek = "4.1.1" | ||
| getrandom = { version = "0.2", features = ["js"] } | ||
| hex = "0.4.3" | ||
| hmac = "0.12" | ||
| log = "0.4.26" | ||
| serde = { version = "1.0.219", features = ["derive"] } | ||
| sha2 = "0.10" | ||
| curve25519-dalek = "4.1.1" | ||
| x25519-dalek = {version="^2.0.1", features = ["static_secrets"] } | ||
| rand_core = {version="0.6.4", features = ["std"]} | ||
| rand = "0.9.1" | ||
|
|
||
| x25519-dalek = { version = "^2.0.1", features = ["static_secrets"] } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| use crate::common::{ | ||
| InitSessionMessage, InitSessionResponse, NTorCertificate, NTorParty, PrivatePublicKeyPair, | ||
| }; | ||
| use crate::helpers::generate_private_public_key_pair; | ||
| use hmac::digest::Digest; | ||
| use hmac::{Hmac, Mac}; | ||
| use sha2::Sha256; | ||
| use x25519_dalek::PublicKey; | ||
|
|
||
| #[derive(Clone)] | ||
| pub struct NTorClient { | ||
| ephemeral_key_pair: PrivatePublicKeyPair, | ||
| pub(crate) shared_secret: Option<Vec<u8>>, | ||
| } | ||
|
|
||
| impl NTorClient { | ||
| pub fn new() -> Self { | ||
| Self { | ||
| ephemeral_key_pair: PrivatePublicKeyPair { | ||
| private_key: None, | ||
| public_key: PublicKey::from([0; 32]), | ||
| }, | ||
| shared_secret: None, | ||
| } | ||
| } | ||
|
|
||
| pub fn initialise_session(&mut self) -> InitSessionMessage { | ||
| self.ephemeral_key_pair = generate_private_public_key_pair(); | ||
|
|
||
| InitSessionMessage { | ||
| client_ephemeral_public_key: self.ephemeral_key_pair.public_key, | ||
| } | ||
| } | ||
|
|
||
| // Steps 15 - 20 of the Goldberg 2012 paper. | ||
| pub fn handle_response_from_server( | ||
| &mut self, | ||
| server_certificate: &NTorCertificate, | ||
| msg: &InitSessionResponse, | ||
| ) -> bool { | ||
| println!("Client:"); | ||
|
|
||
| // Step 18: Compute the shared secret. | ||
| let mut buffer: Vec<u8> = Vec::new(); | ||
|
|
||
| // ECDH Client private ephemeral * server static public key | ||
| let taken_private_key = self.ephemeral_key_pair.private_key.take().unwrap(); | ||
| let mut ecdh_result_1 = taken_private_key | ||
| .diffie_hellman(&msg.server_ephemeral_public_key) | ||
| .to_bytes() | ||
| .to_vec(); | ||
| println!("[Debug] ECDH result 1: {:?}", ecdh_result_1); | ||
| buffer.append(&mut ecdh_result_1); | ||
|
|
||
| // ECDH Client private ephemeral * server ephemeral public Key | ||
| let mut ecdh_result_2 = taken_private_key | ||
| .diffie_hellman(&server_certificate.public_key) | ||
| .to_bytes() | ||
| .to_vec(); | ||
| println!("[Debug] ECDH result 2: {:?}", ecdh_result_2); | ||
| buffer.append(&mut ecdh_result_2); | ||
|
|
||
| // Server id | ||
| buffer.append(&mut server_certificate.server_id.as_bytes().to_vec()); | ||
|
|
||
| // Client ephemeral public | ||
| buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec()); | ||
|
|
||
| // Server ephemeral public | ||
| buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec()); | ||
|
|
||
| // "ntor" string identifier | ||
| buffer.append(&mut "ntor".as_bytes().to_vec()); | ||
|
|
||
| // Instantiate and run hashing function | ||
| let mut hasher = Sha256::new(); | ||
| hasher.update(buffer); | ||
| let sha256_hash = hasher.finalize(); | ||
| let sha256_hash: &[u8; 32] = match sha256_hash.as_slice().try_into() { | ||
| Ok(array_ref) => array_ref, | ||
| Err(_) => { | ||
| panic!("Invalid sha256 hash length"); | ||
| } | ||
| }; | ||
|
|
||
| let secret_key_prime = &sha256_hash[0..16]; | ||
| println!("[Debug] Client secret key prime: {:?}", secret_key_prime); | ||
|
|
||
| let secret_key = &sha256_hash[16..]; | ||
|
|
||
| // Step 19: Compute HMAC (t_b in the paper) | ||
|
|
||
| let mut buffer: Vec<u8> = Vec::new(); | ||
| buffer.append(&mut server_certificate.server_id.as_bytes().to_vec()); | ||
| buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec()); | ||
| buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec()); | ||
| buffer.append(&mut "ntor".as_bytes().to_vec()); | ||
| buffer.append(&mut "server".as_bytes().to_vec()); | ||
|
|
||
| let mut hmac_hash = Hmac::<Sha256>::new_from_slice(&buffer).unwrap(); | ||
| hmac_hash.update(secret_key_prime); | ||
| let computed_t_b_hash = hmac_hash.finalize().into_bytes().to_vec(); | ||
|
|
||
| // assert that computed_t_b_hash equals t_hash generated by server | ||
| if computed_t_b_hash == msg.t_b_hash { | ||
| self.shared_secret = Some(secret_key.to_vec()); | ||
|
|
||
| println!("Shared secret:"); | ||
| println!("{:?}\n", secret_key); | ||
| true | ||
| } else { | ||
| println!("Failed to verify the shared secret: try again bro."); | ||
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Default for NTorClient { | ||
| fn default() -> Self { | ||
| Self::new() | ||
| } | ||
| } | ||
|
|
||
| impl NTorParty for NTorClient { | ||
| fn get_shared_secret(&self) -> Option<&[u8]> { | ||
| self.shared_secret.as_deref() | ||
| } | ||
|
|
||
| fn set_shared_secret(&mut self, shared_secret: Vec<u8>) { | ||
| self.shared_secret = Some(shared_secret); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| use x25519_dalek::{PublicKey, StaticSecret}; | ||
|
|
||
| use crate::helpers; | ||
| use crate::helpers::{key_derivation, vec_to_array32}; | ||
|
|
||
| #[derive(Clone)] | ||
| pub struct PrivatePublicKeyPair { | ||
| // In the future, type StaticSecret should be reserved for the server's static and the EphemeralSecret reserved for the ephemeral private key. | ||
| // However, as a quirk of the nTOR protocol, we also need to use StaticSecret for the client's ephemeral private key hence why it is adopted here. | ||
| pub(crate) private_key: Option<StaticSecret>, | ||
| pub(crate) public_key: PublicKey, | ||
| } | ||
|
|
||
| impl PrivatePublicKeyPair { | ||
| pub fn get_public_key(&self) -> PublicKey { | ||
| self.public_key | ||
| } | ||
|
|
||
| pub fn get_private_key(&self) -> Option<StaticSecret> { | ||
| self.private_key.clone() | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone)] | ||
| pub struct NTorCertificate { | ||
| pub(crate) public_key: PublicKey, | ||
| pub(crate) server_id: String, | ||
| } | ||
|
|
||
| impl NTorCertificate { | ||
| pub fn new(public_key: Vec<u8>, server_id: String) -> Self { | ||
| let pub_key = TryInto::<[u8; 32]>::try_into(public_key).unwrap(); | ||
| NTorCertificate { | ||
| public_key: PublicKey::from(pub_key), | ||
| server_id, | ||
| } | ||
| } | ||
|
|
||
| pub fn public_key(&self) -> Vec<u8> { | ||
| self.public_key.to_bytes().to_vec() | ||
| } | ||
| } | ||
|
|
||
| // In the paper, the outgoing message is ("ntor", B_id, client_ephemeral_public_key). | ||
| pub struct InitSessionMessage { | ||
| pub(crate) client_ephemeral_public_key: PublicKey, | ||
| } | ||
|
|
||
| impl InitSessionMessage { | ||
| pub fn from(bytes: Vec<u8>) -> Self { | ||
| let u8_array = vec_to_array32(bytes); | ||
| InitSessionMessage { | ||
| client_ephemeral_public_key: PublicKey::from(u8_array), | ||
| } | ||
| } | ||
|
|
||
| pub fn public_key(&self) -> &[u8; 32] { | ||
| self.client_ephemeral_public_key.as_bytes() | ||
| } | ||
| } | ||
|
|
||
| // In the paper, the return message is ("ntor", server_ephemeral_public_key, t_b_hash). | ||
| pub struct InitSessionResponse { | ||
| pub(crate) server_ephemeral_public_key: PublicKey, | ||
| pub(crate) t_b_hash: Vec<u8>, | ||
| } | ||
|
|
||
| impl InitSessionResponse { | ||
| pub fn new(public_key: Vec<u8>, t_b_hash: Vec<u8>) -> Self { | ||
| let pub_key = TryInto::<[u8; 32]>::try_into(public_key).unwrap(); | ||
| InitSessionResponse { | ||
| server_ephemeral_public_key: PublicKey::from(pub_key), | ||
| t_b_hash, | ||
| } | ||
| } | ||
|
|
||
| pub fn public_key(&self) -> &[u8; 32] { | ||
| self.server_ephemeral_public_key.as_bytes() | ||
| } | ||
|
|
||
| pub fn t_b_hash(&self) -> &[u8] { | ||
| &self.t_b_hash | ||
| } | ||
| } | ||
|
|
||
| #[derive(bincode::Encode, bincode::Decode)] | ||
| pub struct EncryptedMessage { | ||
| pub nonce: [u8; 12], | ||
| pub data: Vec<u8>, | ||
| } | ||
|
|
||
| pub trait NTorParty { | ||
| fn get_shared_secret(&self) -> Option<&[u8]>; | ||
|
|
||
| fn set_shared_secret(&mut self, shared_secret: Vec<u8>); | ||
|
|
||
| fn encrypt(&self, data: &[u8]) -> Result<EncryptedMessage, &'static str> { | ||
| let Some(key) = self.get_shared_secret() else { | ||
| return Err("no encryption key found"); | ||
| }; | ||
|
|
||
| let encrypt_key = key_derivation(key)?; | ||
| helpers::encrypt(&encrypt_key, data).map(|(nonce, encrypted_message)| EncryptedMessage { | ||
| nonce, | ||
| data: encrypted_message, | ||
| }) | ||
| } | ||
|
|
||
| fn decrypt(&self, encrypted_message: EncryptedMessage) -> Result<Vec<u8>, &'static str> { | ||
| let Some(key) = self.get_shared_secret() else { | ||
| return Err("no decryption key found"); | ||
| }; | ||
|
|
||
| let decrypt_key = key_derivation(key)?; | ||
| helpers::decrypt( | ||
| &encrypted_message.nonce, | ||
| &decrypt_key, | ||
| &encrypted_message.data, | ||
| ) | ||
| } | ||
|
|
||
| fn wasm_encrypt(&self, data: &[u8]) -> Result<([u8; 12], Vec<u8>), &'static str> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remember struggling to make [u8; 32] work in WASM, which is why I originally used Vec. I tested your updates in the interceptor and they compiled successfully, I’m not sure why it didn’t work before. Since [u8; 32] is now accepted, have you tried removing the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, still prepping the layer8-backbone PR. Should be out sometime today
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ahh, have you try to remove wasm_encrypt/decrypt function? |
||
| let Some(key) = self.get_shared_secret() else { | ||
| return Err("no encryption key found"); | ||
| }; | ||
|
|
||
| let encrypt_key = key_derivation(key)?; | ||
| helpers::encrypt(&encrypt_key, data) | ||
| } | ||
|
|
||
| fn wasm_decrypt(&self, nonce: &[u8; 12], data: &[u8]) -> Result<Vec<u8>, &'static str> { | ||
| let Some(key) = self.get_shared_secret() else { | ||
| return Err("no decryption key found"); | ||
| }; | ||
|
|
||
| let decrypt_key = key_derivation(key)?; | ||
| helpers::decrypt(nonce, &decrypt_key, data) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| use crate::common::PrivatePublicKeyPair; | ||
| use aes_gcm::aead::Aead; | ||
| use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; | ||
| use std::convert::TryInto; | ||
| use x25519_dalek::{PublicKey, StaticSecret}; | ||
|
|
||
| /// Returns an array of zeros if conversion fails. | ||
| pub fn vec_to_array32(vec: Vec<u8>) -> [u8; 32] { | ||
| if vec.len() == 32 { | ||
| vec.try_into().unwrap() | ||
| } else { | ||
| [0u8; 32] | ||
| } | ||
| } | ||
|
|
||
| pub fn generate_private_public_key_pair() -> PrivatePublicKeyPair { | ||
| let mut buf = [0u8; 32]; | ||
| getrandom::getrandom(&mut buf).expect("generate random failed"); | ||
| let private_key = StaticSecret::from(buf); | ||
| let public_key = PublicKey::from(&private_key); | ||
|
|
||
| PrivatePublicKeyPair { | ||
| private_key: Some(private_key), | ||
| public_key, | ||
| } | ||
| } | ||
|
|
||
| pub fn key_derivation(shared_secret: &[u8]) -> Result<[u8; 32], &'static str> { | ||
| let mut encrypt_key = shared_secret.to_vec(); // fixme use a reliable kdf | ||
| encrypt_key.extend(shared_secret.to_vec()); | ||
| TryInto::<[u8; 32]>::try_into(encrypt_key).map_err(|_| "Invalid key length") | ||
| } | ||
|
|
||
| pub(crate) fn encrypt(key: &[u8; 32], data: &[u8]) -> Result<([u8; 12], Vec<u8>), &'static str> { | ||
| let key = Key::<Aes256Gcm>::from_slice(&key[..]); | ||
| let cipher = Aes256Gcm::new(key); | ||
|
|
||
| let mut nonce_bytes = [0u8; 12]; | ||
| getrandom::getrandom(&mut nonce_bytes).map_err(|_| "Random generation failed")?; | ||
| let nonce = Nonce::from_slice(&nonce_bytes); // 96-bits; unique per message | ||
|
|
||
| let ciphertext = cipher | ||
| .encrypt(nonce, data) | ||
| .map_err(|_| "Encryption failed")?; | ||
|
|
||
| Ok((nonce_bytes, ciphertext)) | ||
| } | ||
|
|
||
| pub(crate) fn decrypt( | ||
| nonce_bytes: &[u8; 12], | ||
| key: &[u8], | ||
| ciphertext: &[u8], | ||
| ) -> Result<Vec<u8>, &'static str> { | ||
| let key = Key::<Aes256Gcm>::from_slice(key); | ||
| let cipher = Aes256Gcm::new(key); | ||
| let nonce = Nonce::from_slice(nonce_bytes); | ||
|
|
||
| let decrypted_data = cipher | ||
| .decrypt(nonce, ciphertext) | ||
| .map_err(|_| "Decryption failed")?; | ||
|
|
||
| Ok(decrypted_data) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| pub mod client; | ||
| pub mod common; | ||
| pub mod server; | ||
|
|
||
| pub mod helpers; | ||
| mod test; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just remembered why I used Vec here. We’re using a single cryptography algorithm right now, but that may change later, and different algorithms may use public keys of different sizes. In that case, we need to update every usage of [u8;32]. How is [u8; 32] better in this context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, we can revert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we version bump when that happens, I'm assuming we will support multiple encryption schemes and not just replace the ntor?