Skip to content
Merged
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
220 changes: 171 additions & 49 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ dashmap = "6"
ttl_cache = "0.5"

# Database
rusqlite = { version = "0.40", features = ["bundled"] }
rusqlite = { version = "0.41", features = ["bundled"] }

# HTTP clients
reqwest = { version = "0.13", features = ["form", "json", "rustls"], default-features = false }
Expand All @@ -109,16 +109,16 @@ figment = { version = "0.10", features = ["toml"] }

# Utils
once_cell = "1"
parking_lot = "0.12"
parking_lot = "0.13"
dirs = "7"
rand = { version = "0.10", features = ["std_rng"] }

# CLI
clap = { version = "4", features = ["derive", "env"] }

# Encryption
aes-gcm = "0.10.3"
argon2 = "0.5.3"
aes-gcm = "0.11.0"
argon2 = "0.6.0"
base64 = "0.23"

tempfile = "=3.27.0"
Expand Down
4 changes: 2 additions & 2 deletions crates/application/rook-usecases/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ hex = "0.4"

[dev-dependencies]
cortex-test-support = { path = "../../test-support" }
tokio = { version = "1", features = ["test-util"] }
tokio = { version = "=1.54.1", features = ["test-util"] }
wiremock = "=0.6.5"
serde_json = "1"
serde_json = "=1.0.154"
1 change: 1 addition & 0 deletions crates/huginn/presentation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ uuid.workspace = true
ratatui = { version = "=0.30.2", default-features = false, features = ["crossterm"] }
crossterm = { version = "=0.29.0", features = ["event-stream"] }
unicode-width = "0.2"
rand.workspace = true
zeroize = "1"

[target.'cfg(unix)'.dependencies]
Expand Down
19 changes: 10 additions & 9 deletions crates/huginn/presentation/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
//! Explicit UI event categories. UI-only events are never conversation entries.
use aes_gcm::{
aead::{Aead, OsRng},
Aes256Gcm, KeyInit, Nonce,
};
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
use rand::RngExt;
use std::collections::VecDeque;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
Expand Down Expand Up @@ -108,11 +106,11 @@ impl ErrorDeduplicator {
// Generate a fresh AES-256 key from the OS CSPRNG for this instance.
// Nonces need uniqueness under this key, not randomness or separation
// from the nonces used by other instances with independently generated keys.
let key = Aes256Gcm::generate_key(&mut OsRng);
let key_bytes: [u8; 32] = rand::rng().random();
Self {
clock,
hash_message,
cipher: Aes256Gcm::new(&key),
cipher: Aes256Gcm::new_from_slice(&key_bytes).expect("32-byte key"),
nonce_prefix: [0; 4],
nonce_counter: 0,
entries: VecDeque::new(),
Expand All @@ -136,8 +134,8 @@ impl ErrorDeduplicator {
if entry.key != key {
return None;
}
let nonce = Nonce::from_slice(&entry.fingerprint[..12]);
let plaintext = self.cipher.decrypt(nonce, &entry.fingerprint[12..]).ok()?;
let nonce = Nonce::try_from(&entry.fingerprint[..12]).ok()?;
let plaintext = self.cipher.decrypt(&nonce, &entry.fingerprint[12..]).ok()?;
(plaintext == message.as_bytes()).then_some(index)
}) {
if now.saturating_sub(self.entries[index].last_visible) < ERROR_DEDUP_WINDOW {
Expand Down Expand Up @@ -168,7 +166,10 @@ impl ErrorDeduplicator {
let nonce = [self.nonce_prefix.as_slice(), counter.as_slice()].concat();
let ciphertext = self
.cipher
.encrypt(Nonce::from_slice(&nonce), message.as_bytes())
.encrypt(
&Nonce::try_from(nonce.as_slice()).expect("12-byte nonce"),
message.as_bytes(),
)
.expect("AES-GCM encryption");
let mut fingerprint = nonce;
fingerprint.extend(ciphertext);
Expand Down
8 changes: 4 additions & 4 deletions crates/huginn/presentation/tests/event.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#[cfg(feature = "test-driver")]
use aes_gcm::aead::rand_core::{OsRng, RngCore};
#[cfg(feature = "test-driver")]
use huginn_presentation::event::test_support;
use huginn_presentation::event::*;
#[cfg(feature = "test-driver")]
use rand::RngExt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{sync::Arc, time::Duration};

Expand Down Expand Up @@ -199,9 +199,9 @@ fn dedup_instances_keep_independent_caches() {
fn dedup_decryption_requires_the_matching_key_even_with_the_same_nonce() {
let clock = Arc::new(FakeClock::at(0));
let mut first_key = [0u8; 32];
OsRng.fill_bytes(&mut first_key);
first_key = rand::rng().random();
let mut second_key = [0u8; 32];
OsRng.fill_bytes(&mut second_key);
second_key = rand::rng().random();
if second_key == first_key {
second_key[0] ^= 1;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/huginn/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ process-wrap = { version = "10", features = ["tokio1", "process-group", "kill-on
keyring = { version = "=3.6.3", default-features = false, features = ["apple-native"] }

[target.'cfg(target_os = "linux")'.dependencies]
keyring = { version = "=3.6.3", default-features = false, features = ["sync-secret-service", "crypto-rust"] }
keyring = { version = "=3.6.3", default-features = false, features = ["sync-secret-service", "crypto-rust", "vendored"] }

[[test]]
name = "tls_transport"
Expand Down
2 changes: 1 addition & 1 deletion crates/infrastructure/audit-sqlite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tokio = { version = "1", features = ["rt", "sync", "fs", "time"] }
serde = { version = "1", features = ["derive"] }

# Database
rusqlite = { version = "0.40", features = ["bundled"] }
rusqlite = { version = "0.41", features = ["bundled"] }

# Time
chrono = { version = "0.4", features = ["serde"] }
Expand Down
16 changes: 8 additions & 8 deletions crates/infrastructure/encryption-inmemory/src/key_manager.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng},
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use argon2::{password_hash::PasswordHasher, Argon2};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use rand::RngExt;
use rook_core::ports::{CredentialEncryptionError, KeyManager};

use super::{EncryptionError, Result};
Expand Down Expand Up @@ -32,10 +33,8 @@ impl AesGcmKeyManager {
argon2::Params::new(65_536, 3, 4, Some(32))
.map_err(|_| EncryptionError::KeyDerivation)?,
);
let salt =
SaltString::encode_b64(&salt_bytes).map_err(|_| EncryptionError::KeyDerivation)?;
let hash = argon2
.hash_password(passphrase.as_bytes(), &salt)
.hash_password_with_salt(passphrase.as_bytes(), &salt_bytes)
.map_err(|_| EncryptionError::KeyDerivation)?;
let hash_output = hash.hash.ok_or(EncryptionError::KeyDerivation)?;

Expand All @@ -53,7 +52,8 @@ impl AesGcmKeyManager {

let cipher =
Aes256Gcm::new_from_slice(&self.key).map_err(|_| EncryptionError::CipherError)?;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_bytes: [u8; NONCE_BYTES] = rand::rng().random();
let nonce = Nonce::try_from(nonce_bytes.as_slice()).expect("12-byte nonce");
let ciphertext = cipher
.encrypt(&nonce, plaintext.as_bytes())
.map_err(|_| EncryptionError::CipherError)?;
Expand Down Expand Up @@ -88,9 +88,9 @@ impl AesGcmKeyManager {

let cipher =
Aes256Gcm::new_from_slice(&self.key).map_err(|_| EncryptionError::CipherError)?;
let nonce = Nonce::from_slice(&nonce_bytes);
let nonce = Nonce::try_from(nonce_bytes.as_slice()).expect("12-byte nonce");
let plaintext = cipher
.decrypt(nonce, ciphertext_with_tag.as_ref())
.decrypt(&nonce, ciphertext_with_tag.as_ref())
.map_err(|_| EncryptionError::DecryptKey)?;

String::from_utf8(plaintext).map_err(|_| EncryptionError::DecryptFormat)
Expand Down
14 changes: 8 additions & 6 deletions crates/infrastructure/encryption-inmemory/src/password.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordVerifier},
Argon2, PasswordHasher as _,
password_hash::phc::PasswordHash,
password_hash::{PasswordHasher, PasswordVerifier},
Argon2,
};
use rand::RngExt;
use rook_core::model::PasswordHash as CorePasswordHash;
use rook_core::ports::{PasswordHashError, PasswordHasher};
use rook_core::ports::{PasswordHashError, PasswordHasher as CorePasswordHasher};

/// Argon2id password hasher using the argon2 crate's password_hash API.
///
Expand All @@ -30,16 +32,16 @@ impl Default for Argon2idHasher {
}
}

impl PasswordHasher for Argon2idHasher {
impl CorePasswordHasher for Argon2idHasher {
fn hash_password(&self, password: &str) -> Result<CorePasswordHash, PasswordHashError> {
// Generate a random salt using OS RNG (16 bytes is the default)
let salt = argon2::password_hash::SaltString::generate(&mut OsRng);
let salt_bytes: [u8; 16] = rand::rng().random();

// Hash the password with Argon2id using default (OWASP) params
// Argon2::default() uses: m=65536 (64MB), t=3, p=1
let hash = self
.argon2
.hash_password(password.as_bytes(), &salt)
.hash_password_with_salt(password.as_bytes(), &salt_bytes)
.map_err(|_| PasswordHashError::HashGeneration)?;

Ok(CorePasswordHash::from(hash.to_string()))
Expand Down
Loading