diff --git a/server/crates/arbiter-server/src/actors/keyholder/encryption.rs b/server/crates/arbiter-server/src/actors/keyholder/encryption.rs deleted file mode 100644 index a3a6d96..0000000 --- a/server/crates/arbiter-server/src/actors/keyholder/encryption.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod v1; diff --git a/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs b/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs deleted file mode 100644 index 6972c54..0000000 --- a/server/crates/arbiter-server/src/actors/keyholder/encryption/v1.rs +++ /dev/null @@ -1,329 +0,0 @@ -use std::ops::Deref as _; - -use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt}; -use chacha20poly1305::{ - AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce, - aead::{AeadMut, Error, Payload}, -}; -use hmac::Mac as _; -use rand::{ - Rng as _, SeedableRng, - rngs::{StdRng, SysRng}, -}; - -use crate::safe_cell::{SafeCell, SafeCellHandle as _}; - -pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes(); -pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes(); -pub const USERAGENT_INTEGRITY_DERIVE_TAG: &[u8] = "arbiter/useragent/integrity-key/v1".as_bytes(); -pub const USERAGENT_INTEGRITY_TAG: &[u8] = "arbiter/useragent/pubkey-entry/v1".as_bytes(); - -pub const NONCE_LENGTH: usize = 24; - -#[derive(Default)] -pub struct Nonce([u8; NONCE_LENGTH]); -impl Nonce { - pub fn increment(&mut self) { - for i in (0..self.0.len()).rev() { - if self.0[i] == 0xFF { - self.0[i] = 0; - } else { - self.0[i] += 1; - break; - } - } - } - - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } -} -impl<'a> TryFrom<&'a [u8]> for Nonce { - type Error = (); - - fn try_from(value: &'a [u8]) -> Result { - if value.len() != NONCE_LENGTH { - return Err(()); - } - let mut nonce = [0u8; NONCE_LENGTH]; - nonce.copy_from_slice(value); - Ok(Self(nonce)) - } -} - -pub struct KeyCell(pub SafeCell); -impl From> for KeyCell { - fn from(value: SafeCell) -> Self { - Self(value) - } -} -impl TryFrom>> for KeyCell { - type Error = (); - - fn try_from(mut value: SafeCell>) -> Result { - let value = value.read(); - if value.len() != size_of::() { - return Err(()); - } - let cell = SafeCell::new_inline(|cell_write: &mut Key| { - cell_write.copy_from_slice(&value); - }); - Ok(Self(cell)) - } -} - -impl KeyCell { - pub fn new_secure_random() -> Self { - let key = SafeCell::new_inline(|key_buffer: &mut Key| { - #[allow( - clippy::unwrap_used, - reason = "Rng failure is unrecoverable and should panic" - )] - let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); - rng.fill_bytes(key_buffer); - }); - - key.into() - } - - pub fn encrypt_in_place( - &mut self, - nonce: &Nonce, - associated_data: &[u8], - mut buffer: impl AsMut>, - ) -> Result<(), Error> { - let key_reader = self.0.read(); - let key_ref = key_reader.deref(); - let cipher = XChaCha20Poly1305::new(key_ref); - let nonce = XNonce::from_slice(nonce.0.as_ref()); - let buffer = buffer.as_mut(); - cipher.encrypt_in_place(nonce, associated_data, buffer) - } - pub fn decrypt_in_place( - &mut self, - nonce: &Nonce, - associated_data: &[u8], - buffer: &mut SafeCell>, - ) -> Result<(), Error> { - let key_reader = self.0.read(); - let key_ref = key_reader.deref(); - let cipher = XChaCha20Poly1305::new(key_ref); - let nonce = XNonce::from_slice(nonce.0.as_ref()); - let mut buffer = buffer.write(); - let buffer: &mut Vec = buffer.as_mut(); - cipher.decrypt_in_place(nonce, associated_data, buffer) - } - - pub fn encrypt( - &mut self, - nonce: &Nonce, - associated_data: &[u8], - plaintext: impl AsRef<[u8]>, - ) -> Result, Error> { - let key_reader = self.0.read(); - let key_ref = key_reader.deref(); - let mut cipher = XChaCha20Poly1305::new(key_ref); - let nonce = XNonce::from_slice(nonce.0.as_ref()); - - let ciphertext = cipher.encrypt( - nonce, - Payload { - msg: plaintext.as_ref(), - aad: associated_data, - }, - )?; - Ok(ciphertext) - } -} - -pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH]; - -pub fn generate_salt() -> Salt { - let mut salt = Salt::default(); - #[allow( - clippy::unwrap_used, - reason = "Rng failure is unrecoverable and should panic" - )] - let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); - rng.fill_bytes(&mut salt); - salt -} - -/// User password might be of different length, have not enough entropy, etc... -/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation. -pub fn derive_seal_key(mut password: SafeCell>, salt: &Salt) -> KeyCell { - #[allow(clippy::unwrap_used)] - let params = argon2::Params::new(262_144, 3, 4, None).unwrap(); - let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params); - let mut key = SafeCell::new(Key::default()); - password.read_inline(|password_source| { - let mut key_buffer = key.write(); - let key_buffer: &mut [u8] = key_buffer.as_mut(); - - #[allow( - clippy::unwrap_used, - reason = "Better fail completely than return a weak key" - )] - hasher - .hash_password_into(password_source.deref(), salt, key_buffer) - .unwrap(); - }); - - key.into() -} - -/// Derives a dedicated key used for integrity tags within a specific domain. -pub fn derive_integrity_key(seal_key: &mut KeyCell, derive_tag: &[u8]) -> KeyCell { - type HmacSha256 = hmac::Hmac; - - let mut derived = SafeCell::new(Key::default()); - seal_key.0.read_inline(|seal_key_bytes| { - let mut mac = ::new_from_slice(seal_key_bytes.as_ref()) - .expect("HMAC key initialization must not fail for 32-byte key"); - mac.update(derive_tag); - let output = mac.finalize().into_bytes(); - - let mut writer = derived.write(); - let writer: &mut [u8] = writer.as_mut(); - writer.copy_from_slice(&output); - }); - - derived.into() -} - -/// Computes an integrity tag for a specific domain and payload shape. -pub fn compute_integrity_tag<'a, I>( - integrity_key: &mut KeyCell, - purpose_tag: &[u8], - data_parts: I, -) -> [u8; 32] -where - I: IntoIterator, -{ - type HmacSha256 = hmac::Hmac; - - let mut output_tag = [0u8; 32]; - integrity_key.0.read_inline(|integrity_key_bytes| { - let mut mac = ::new_from_slice(integrity_key_bytes.as_ref()) - .expect("HMAC key initialization must not fail for 32-byte key"); - mac.update(purpose_tag); - for data_part in data_parts { - mac.update(data_part); - } - output_tag.copy_from_slice(&mac.finalize().into_bytes()); - }); - - output_tag -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::safe_cell::SafeCell; - - #[test] - pub fn derive_seal_key_deterministic() { - static PASSWORD: &[u8] = b"password"; - let password = SafeCell::new(PASSWORD.to_vec()); - let password2 = SafeCell::new(PASSWORD.to_vec()); - let salt = generate_salt(); - - let mut key1 = derive_seal_key(password, &salt); - let mut key2 = derive_seal_key(password2, &salt); - - let key1_reader = key1.0.read(); - let key2_reader = key2.0.read(); - - assert_eq!(key1_reader.deref(), key2_reader.deref()); - } - - #[test] - pub fn successful_derive() { - static PASSWORD: &[u8] = b"password"; - let password = SafeCell::new(PASSWORD.to_vec()); - let salt = generate_salt(); - - let mut key = derive_seal_key(password, &salt); - let key_reader = key.0.read(); - let key_ref = key_reader.deref(); - - assert_ne!(key_ref.as_slice(), &[0u8; 32][..]); - } - - #[test] - pub fn encrypt_decrypt() { - static PASSWORD: &[u8] = b"password"; - let password = SafeCell::new(PASSWORD.to_vec()); - let salt = generate_salt(); - - let mut key = derive_seal_key(password, &salt); - let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305 - let associated_data = b"associated data"; - let mut buffer = b"secret data".to_vec(); - - key.encrypt_in_place(&nonce, associated_data, &mut buffer) - .unwrap(); - assert_ne!(buffer, b"secret data"); - - let mut buffer = SafeCell::new(buffer); - - key.decrypt_in_place(&nonce, associated_data, &mut buffer) - .unwrap(); - - let buffer = buffer.read(); - assert_eq!(*buffer, b"secret data"); - } - - #[test] - // We should fuzz this - pub fn test_nonce_increment() { - let mut nonce = Nonce([0u8; NONCE_LENGTH]); - nonce.increment(); - - assert_eq!( - nonce.0, - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 - ] - ); - } - - #[test] - pub fn integrity_tag_deterministic() { - let salt = generate_salt(); - let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt); - let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG); - let key_type = 1i32.to_be_bytes(); - let t1 = compute_integrity_tag( - &mut integrity_key, - USERAGENT_INTEGRITY_TAG, - [key_type.as_slice(), b"pubkey".as_ref()], - ); - let t2 = compute_integrity_tag( - &mut integrity_key, - USERAGENT_INTEGRITY_TAG, - [key_type.as_slice(), b"pubkey".as_ref()], - ); - assert_eq!(t1, t2); - } - - #[test] - pub fn integrity_tag_changes_with_payload() { - let salt = generate_salt(); - let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt); - let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG); - let key_type_1 = 1i32.to_be_bytes(); - let key_type_2 = 2i32.to_be_bytes(); - let t1 = compute_integrity_tag( - &mut integrity_key, - USERAGENT_INTEGRITY_TAG, - [key_type_1.as_slice(), b"pubkey".as_ref()], - ); - let t2 = compute_integrity_tag( - &mut integrity_key, - USERAGENT_INTEGRITY_TAG, - [key_type_2.as_slice(), b"pubkey".as_ref()], - ); - assert_ne!(t1, t2); - } -} diff --git a/server/crates/arbiter-server/src/actors/keyholder/mod.rs b/server/crates/arbiter-server/src/actors/keyholder/mod.rs index f43a621..584ec4d 100644 --- a/server/crates/arbiter-server/src/actors/keyholder/mod.rs +++ b/server/crates/arbiter-server/src/actors/keyholder/mod.rs @@ -8,7 +8,7 @@ use kameo::{Actor, Reply, messages}; use strum::{EnumDiscriminants, IntoDiscriminant}; use tracing::{error, info}; -use crate::safe_cell::SafeCell; +use crate::{crypto::{KeyCell, derive_key, encryption::v1::{self, Nonce}, integrity::v1::compute_integrity_tag}, safe_cell::SafeCell}; use crate::{ db::{ self, @@ -17,9 +17,7 @@ use crate::{ }, safe_cell::SafeCellHandle as _, }; -use encryption::v1::{self, KeyCell, Nonce}; -pub mod encryption; #[derive(Default, EnumDiscriminants)] #[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))] @@ -32,7 +30,6 @@ enum State { Unsealed { root_key_history_id: i32, root_key: KeyCell, - integrity_key: KeyCell, }, } @@ -116,7 +113,7 @@ impl KeyHolder { .await?; let mut nonce = - v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| { + Nonce::try_from(current_nonce.as_slice()).map_err(|_| { error!( "Broken database: invalid nonce for root key history id={}", root_key_id @@ -145,14 +142,12 @@ impl KeyHolder { return Err(Error::AlreadyBootstrapped); } let salt = v1::generate_salt(); - let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); - let integrity_key = - v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG); + let mut seal_key = derive_key(seal_key_raw, &salt); let mut root_key = KeyCell::new_secure_random(); // Zero nonces are fine because they are one-time - let root_key_nonce = v1::Nonce::default(); - let data_encryption_nonce = v1::Nonce::default(); + let root_key_nonce = Nonce::default(); + let data_encryption_nonce = Nonce::default(); let root_key_ciphertext: Vec = root_key.0.read_inline(|reader| { let root_key_reader = reader.as_slice(); @@ -196,7 +191,6 @@ impl KeyHolder { self.state = State::Unsealed { root_key, root_key_history_id, - integrity_key, }; info!("Keyholder bootstrapped successfully"); @@ -229,9 +223,7 @@ impl KeyHolder { error!("Broken database: invalid salt for root key"); Error::BrokenDatabase })?; - let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); - let integrity_key = - v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG); + let mut seal_key = derive_key(seal_key_raw, &salt); let mut root_key = SafeCell::new(current_key.ciphertext.clone()); @@ -251,11 +243,10 @@ impl KeyHolder { self.state = State::Unsealed { root_key_history_id: current_key.id, - root_key: v1::KeyCell::try_from(root_key).map_err(|err| { + root_key: KeyCell::try_from(root_key).map_err(|err| { error!(?err, "Broken database: invalid encryption key size"); Error::BrokenDatabase })?, - integrity_key, }; info!("Keyholder unsealed successfully"); @@ -270,12 +261,12 @@ impl KeyHolder { purpose_tag: Vec, data_parts: Vec>, ) -> Result, Error> { - let State::Unsealed { integrity_key, .. } = &mut self.state else { + let State::Unsealed { root_key, .. } = &mut self.state else { return Err(Error::NotBootstrapped); }; - let tag = v1::compute_integrity_tag( - integrity_key, + let tag = compute_integrity_tag( + root_key, &purpose_tag, data_parts.iter().map(Vec::as_slice), ); diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index 60ad26c..140d481 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -10,8 +10,7 @@ use crate::{ bootstrap::ConsumeToken, keyholder::{self, SignIntegrityTag}, user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound}, - }, - db::schema, + }, crypto::integrity::v1::USERAGENT_INTEGRITY_TAG, db::schema }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -269,7 +268,7 @@ where .actors .key_holder .ask(SignIntegrityTag { - purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(), + purpose_tag: USERAGENT_INTEGRITY_TAG.to_vec(), data_parts: vec![ (pubkey.key_type() as i32).to_be_bytes().to_vec(), pubkey.to_stored_bytes(), diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs index 3243996..90cd1d6 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -13,6 +13,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer; use crate::actors::keyholder::KeyHolderState; use crate::actors::user_agent::session::Error; +use crate::crypto::integrity::v1::USERAGENT_INTEGRITY_TAG; use crate::db::models::{ EvmWalletAccess, KeyType, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, }; @@ -112,7 +113,7 @@ impl UserAgentSession { .actors .key_holder .ask(SignIntegrityTag { - purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(), + purpose_tag: USERAGENT_INTEGRITY_TAG.to_vec(), data_parts: vec![(key_type as i32).to_be_bytes().to_vec(), public_key], }) .await diff --git a/server/crates/arbiter-server/src/crypto/encryption/mod.rs b/server/crates/arbiter-server/src/crypto/encryption/mod.rs new file mode 100644 index 0000000..5dd9fd0 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/encryption/mod.rs @@ -0,0 +1 @@ +pub mod v1; \ No newline at end of file diff --git a/server/crates/arbiter-server/src/crypto/encryption/v1.rs b/server/crates/arbiter-server/src/crypto/encryption/v1.rs new file mode 100644 index 0000000..8872f76 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/encryption/v1.rs @@ -0,0 +1,111 @@ +use argon2::password_hash::Salt as ArgonSalt; + +use rand::{ + Rng as _, SeedableRng, + rngs::{StdRng, SysRng}, +}; + + +pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes(); +pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes(); + +pub const NONCE_LENGTH: usize = 24; + +#[derive(Default)] +pub struct Nonce(pub [u8; NONCE_LENGTH]); +impl Nonce { + pub fn increment(&mut self) { + for i in (0..self.0.len()).rev() { + if self.0[i] == 0xFF { + self.0[i] = 0; + } else { + self.0[i] += 1; + break; + } + } + } + + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} +impl<'a> TryFrom<&'a [u8]> for Nonce { + type Error = (); + + fn try_from(value: &'a [u8]) -> Result { + if value.len() != NONCE_LENGTH { + return Err(()); + } + let mut nonce = [0u8; NONCE_LENGTH]; + nonce.copy_from_slice(value); + Ok(Self(nonce)) + } +} + + + +pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH]; + +pub fn generate_salt() -> Salt { + let mut salt = Salt::default(); + #[allow( + clippy::unwrap_used, + reason = "Rng failure is unrecoverable and should panic" + )] + let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); + rng.fill_bytes(&mut salt); + salt +} + +#[cfg(test)] +mod tests { + use std::ops::Deref as _; + + use super::*; + use crate::{crypto::derive_key, safe_cell::{SafeCell, SafeCellHandle as _}}; + + #[test] + pub fn derive_seal_key_deterministic() { + static PASSWORD: &[u8] = b"password"; + let password = SafeCell::new(PASSWORD.to_vec()); + let password2 = SafeCell::new(PASSWORD.to_vec()); + let salt = generate_salt(); + + let mut key1 = derive_key(password, &salt); + let mut key2 = derive_key(password2, &salt); + + let key1_reader = key1.0.read(); + let key2_reader = key2.0.read(); + + assert_eq!(key1_reader.deref(), key2_reader.deref()); + } + + #[test] + pub fn successful_derive() { + static PASSWORD: &[u8] = b"password"; + let password = SafeCell::new(PASSWORD.to_vec()); + let salt = generate_salt(); + + let mut key = derive_key(password, &salt); + let key_reader = key.0.read(); + let key_ref = key_reader.deref(); + + assert_ne!(key_ref.as_slice(), &[0u8; 32][..]); + } + + + + #[test] + // We should fuzz this + pub fn test_nonce_increment() { + let mut nonce = Nonce([0u8; NONCE_LENGTH]); + nonce.increment(); + + assert_eq!( + nonce.0, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + ] + ); + } +} diff --git a/server/crates/arbiter-server/src/crypto/integrity/mod.rs b/server/crates/arbiter-server/src/crypto/integrity/mod.rs new file mode 100644 index 0000000..5dd9fd0 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/integrity/mod.rs @@ -0,0 +1 @@ +pub mod v1; \ No newline at end of file diff --git a/server/crates/arbiter-server/src/crypto/integrity/v1.rs b/server/crates/arbiter-server/src/crypto/integrity/v1.rs new file mode 100644 index 0000000..21a0788 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/integrity/v1.rs @@ -0,0 +1,77 @@ +use crate::{crypto::KeyCell, safe_cell::SafeCellHandle as _}; +use chacha20poly1305::Key; +use hmac::Mac as _; + + pub const USERAGENT_INTEGRITY_DERIVE_TAG: &[u8] = "arbiter/useragent/integrity-key/v1".as_bytes(); + pub const USERAGENT_INTEGRITY_TAG: &[u8] = "arbiter/useragent/pubkey-entry/v1".as_bytes(); + + +/// Computes an integrity tag for a specific domain and payload shape. +pub fn compute_integrity_tag<'a, I>( + integrity_key: &mut KeyCell, + purpose_tag: &[u8], + data_parts: I, +) -> [u8; 32] +where + I: IntoIterator, +{ + type HmacSha256 = hmac::Hmac; + + let mut output_tag = [0u8; 32]; + integrity_key.0.read_inline(|integrity_key_bytes: &Key| { + let mut mac = ::new_from_slice(integrity_key_bytes.as_ref()) + .expect("HMAC key initialization must not fail for 32-byte key"); + mac.update(purpose_tag); + for data_part in data_parts { + mac.update(data_part); + } + output_tag.copy_from_slice(&mac.finalize().into_bytes()); + }); + + output_tag +} + +#[cfg(test)] +mod tests { + use crate::{crypto::{derive_key, encryption::v1::generate_salt}, safe_cell::{SafeCell, SafeCellHandle as _}}; + + use super::{compute_integrity_tag, USERAGENT_INTEGRITY_TAG}; + + + #[test] + pub fn integrity_tag_deterministic() { + let salt = generate_salt(); + let mut integrity_key = derive_key(SafeCell::new(b"password".to_vec()), &salt); + let key_type = 1i32.to_be_bytes(); + let t1 = compute_integrity_tag( + &mut integrity_key, + USERAGENT_INTEGRITY_TAG, + [key_type.as_slice(), b"pubkey".as_ref()], + ); + let t2 = compute_integrity_tag( + &mut integrity_key, + USERAGENT_INTEGRITY_TAG, + [key_type.as_slice(), b"pubkey".as_ref()], + ); + assert_eq!(t1, t2); + } + + #[test] + pub fn integrity_tag_changes_with_payload() { + let salt = generate_salt(); + let mut integrity_key = derive_key(SafeCell::new(b"password".to_vec()), &salt); + let key_type_1 = 1i32.to_be_bytes(); + let key_type_2 = 2i32.to_be_bytes(); + let t1 = compute_integrity_tag( + &mut integrity_key, + USERAGENT_INTEGRITY_TAG, + [key_type_1.as_slice(), b"pubkey".as_ref()], + ); + let t2 = compute_integrity_tag( + &mut integrity_key, + USERAGENT_INTEGRITY_TAG, + [key_type_2.as_slice(), b"pubkey".as_ref()], + ); + assert_ne!(t1, t2); + } +} \ No newline at end of file diff --git a/server/crates/arbiter-server/src/crypto/mod.rs b/server/crates/arbiter-server/src/crypto/mod.rs new file mode 100644 index 0000000..6bf2931 --- /dev/null +++ b/server/crates/arbiter-server/src/crypto/mod.rs @@ -0,0 +1,153 @@ +use std::ops::Deref as _; + +use argon2::{Algorithm, Argon2}; +use chacha20poly1305::{ + AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce, + aead::{AeadMut, Error, Payload}, +}; +use rand::{Rng as _, SeedableRng as _, rngs::{StdRng, SysRng}}; + +use crate::{safe_cell::{SafeCell, SafeCellHandle as _}}; + +pub mod encryption; +pub mod integrity; + +use encryption::v1::{Nonce, Salt}; + +pub struct KeyCell(pub SafeCell); +impl From> for KeyCell { + fn from(value: SafeCell) -> Self { + Self(value) + } +} +impl TryFrom>> for KeyCell { + type Error = (); + + fn try_from(mut value: SafeCell>) -> Result { + let value = value.read(); + if value.len() != size_of::() { + return Err(()); + } + let cell = SafeCell::new_inline(|cell_write: &mut Key| { + cell_write.copy_from_slice(&value); + }); + Ok(Self(cell)) + } +} + +impl KeyCell { + pub fn new_secure_random() -> Self { + let key = SafeCell::new_inline(|key_buffer: &mut Key| { + #[allow( + clippy::unwrap_used, + reason = "Rng failure is unrecoverable and should panic" + )] + let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); + rng.fill_bytes(key_buffer); + }); + + key.into() + } + + pub fn encrypt_in_place( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + mut buffer: impl AsMut>, + ) -> Result<(), Error> { + let key_reader = self.0.read(); + let key_ref = key_reader.deref(); + let cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + let buffer = buffer.as_mut(); + cipher.encrypt_in_place(nonce, associated_data, buffer) + } + pub fn decrypt_in_place( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut SafeCell>, + ) -> Result<(), Error> { + let key_reader = self.0.read(); + let key_ref = key_reader.deref(); + let cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + let mut buffer = buffer.write(); + let buffer: &mut Vec = buffer.as_mut(); + cipher.decrypt_in_place(nonce, associated_data, buffer) + } + + pub fn encrypt( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + plaintext: impl AsRef<[u8]>, + ) -> Result, Error> { + let key_reader = self.0.read(); + let key_ref = key_reader.deref(); + let mut cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + + let ciphertext = cipher.encrypt( + nonce, + Payload { + msg: plaintext.as_ref(), + aad: associated_data, + }, + )?; + Ok(ciphertext) + } +} + +/// User password might be of different length, have not enough entropy, etc... +/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation. +pub fn derive_key(mut password: SafeCell>, salt: &Salt) -> KeyCell { + #[allow(clippy::unwrap_used)] + let params = argon2::Params::new(262_144, 3, 4, None).unwrap(); + let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params); + let mut key = SafeCell::new(Key::default()); + password.read_inline(|password_source| { + let mut key_buffer = key.write(); + let key_buffer: &mut [u8] = key_buffer.as_mut(); + + #[allow( + clippy::unwrap_used, + reason = "Better fail completely than return a weak key" + )] + hasher + .hash_password_into(password_source.deref(), salt, key_buffer) + .unwrap(); + }); + + key.into() +} + +#[cfg(test)] +mod tests { + use crate::{safe_cell::{SafeCell, SafeCellHandle as _}}; + use super::{derive_key, encryption::v1::{Nonce, generate_salt}}; + + #[test] + pub fn encrypt_decrypt() { + static PASSWORD: &[u8] = b"password"; + let password = SafeCell::new(PASSWORD.to_vec()); + let salt = generate_salt(); + + let mut key = derive_key(password, &salt); + let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305 + let associated_data = b"associated data"; + let mut buffer = b"secret data".to_vec(); + + key.encrypt_in_place(&nonce, associated_data, &mut buffer) + .unwrap(); + assert_ne!(buffer, b"secret data"); + + let mut buffer = SafeCell::new(buffer); + + key.decrypt_in_place(&nonce, associated_data, &mut buffer) + .unwrap(); + + let buffer = buffer.read(); + assert_eq!(*buffer, b"secret data"); + } +} \ No newline at end of file diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 9b2695e..a25ed97 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] use crate::context::ServerContext; +pub mod crypto; pub mod actors; pub mod context; pub mod db; diff --git a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs b/server/crates/arbiter-server/tests/keyholder/lifecycle.rs index e0023d3..1ca348a 100644 --- a/server/crates/arbiter-server/tests/keyholder/lifecycle.rs +++ b/server/crates/arbiter-server/tests/keyholder/lifecycle.rs @@ -1,7 +1,5 @@ use arbiter_server::{ - actors::keyholder::{Error, KeyHolder}, - db::{self, models, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, + actors::keyholder::{Error, KeyHolder}, crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, db::{self, models, schema}, safe_cell::{SafeCell, SafeCellHandle as _} }; use diesel::{QueryDsl, SelectableHelper}; use diesel_async::RunQueryDsl; @@ -27,13 +25,13 @@ async fn test_bootstrap() { assert_eq!(row.schema_version, 1); assert_eq!( row.tag, - arbiter_server::actors::keyholder::encryption::v1::ROOT_KEY_TAG + ROOT_KEY_TAG ); assert!(!row.ciphertext.is_empty()); assert!(!row.salt.is_empty()); assert_eq!( row.data_encryption_nonce, - arbiter_server::actors::keyholder::encryption::v1::Nonce::default().to_vec() + Nonce::default().to_vec() ); } diff --git a/server/crates/arbiter-server/tests/keyholder/storage.rs b/server/crates/arbiter-server/tests/keyholder/storage.rs index 74a67cc..b0ecd33 100644 --- a/server/crates/arbiter-server/tests/keyholder/storage.rs +++ b/server/crates/arbiter-server/tests/keyholder/storage.rs @@ -1,9 +1,7 @@ use std::collections::HashSet; use arbiter_server::{ - actors::keyholder::{Error, encryption::v1}, - db::{self, models, schema}, - safe_cell::{SafeCell, SafeCellHandle as _}, + actors::keyholder::Error, crypto::encryption::v1::Nonce, db::{self, models, schema}, safe_cell::{SafeCell, SafeCellHandle as _} }; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update}; use diesel_async::RunQueryDsl; @@ -102,7 +100,7 @@ async fn test_nonce_never_reused() { assert_eq!(nonces.len(), unique.len(), "all nonces must be unique"); for (i, row) in rows.iter().enumerate() { - let mut expected = v1::Nonce::default(); + let mut expected = Nonce::default(); for _ in 0..=i { expected.increment(); } diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index f130999..2cc2fa6 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -279,8 +279,12 @@ pub async fn test_challenge_auth_rejects_invalid_signature() { .await .unwrap(); + let expected_err = task.await.unwrap(); + + println!("Received expected error: {expected_err:#?}"); + assert!(matches!( - task.await.unwrap(), + expected_err, Err(auth::Error::InvalidChallengeSolution) )); }