refactor(server): separate crypto by purpose and moved outside of actor into separate module
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
pub mod v1;
|
|
||||||
@@ -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<u8> {
|
|
||||||
self.0.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
|
||||||
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<Key>);
|
|
||||||
impl From<SafeCell<Key>> for KeyCell {
|
|
||||||
fn from(value: SafeCell<Key>) -> Self {
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(mut value: SafeCell<Vec<u8>>) -> Result<Self, Self::Error> {
|
|
||||||
let value = value.read();
|
|
||||||
if value.len() != size_of::<Key>() {
|
|
||||||
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<Vec<u8>>,
|
|
||||||
) -> 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<Vec<u8>>,
|
|
||||||
) -> 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<u8> = 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<Vec<u8>, 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<Vec<u8>>, 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<sha2::Sha256>;
|
|
||||||
|
|
||||||
let mut derived = SafeCell::new(Key::default());
|
|
||||||
seal_key.0.read_inline(|seal_key_bytes| {
|
|
||||||
let mut mac = <HmacSha256 as hmac::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<Item = &'a [u8]>,
|
|
||||||
{
|
|
||||||
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
|
||||||
|
|
||||||
let mut output_tag = [0u8; 32];
|
|
||||||
integrity_key.0.read_inline(|integrity_key_bytes| {
|
|
||||||
let mut mac = <HmacSha256 as hmac::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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ use kameo::{Actor, Reply, messages};
|
|||||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||||
use tracing::{error, info};
|
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::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
@@ -17,9 +17,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
use encryption::v1::{self, KeyCell, Nonce};
|
|
||||||
|
|
||||||
pub mod encryption;
|
|
||||||
|
|
||||||
#[derive(Default, EnumDiscriminants)]
|
#[derive(Default, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||||
@@ -32,7 +30,6 @@ enum State {
|
|||||||
Unsealed {
|
Unsealed {
|
||||||
root_key_history_id: i32,
|
root_key_history_id: i32,
|
||||||
root_key: KeyCell,
|
root_key: KeyCell,
|
||||||
integrity_key: KeyCell,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +113,7 @@ impl KeyHolder {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut nonce =
|
let mut nonce =
|
||||||
v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
||||||
error!(
|
error!(
|
||||||
"Broken database: invalid nonce for root key history id={}",
|
"Broken database: invalid nonce for root key history id={}",
|
||||||
root_key_id
|
root_key_id
|
||||||
@@ -145,14 +142,12 @@ impl KeyHolder {
|
|||||||
return Err(Error::AlreadyBootstrapped);
|
return Err(Error::AlreadyBootstrapped);
|
||||||
}
|
}
|
||||||
let salt = v1::generate_salt();
|
let salt = v1::generate_salt();
|
||||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||||
let integrity_key =
|
|
||||||
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
|
|
||||||
let mut root_key = KeyCell::new_secure_random();
|
let mut root_key = KeyCell::new_secure_random();
|
||||||
|
|
||||||
// Zero nonces are fine because they are one-time
|
// Zero nonces are fine because they are one-time
|
||||||
let root_key_nonce = v1::Nonce::default();
|
let root_key_nonce = Nonce::default();
|
||||||
let data_encryption_nonce = v1::Nonce::default();
|
let data_encryption_nonce = Nonce::default();
|
||||||
|
|
||||||
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
||||||
let root_key_reader = reader.as_slice();
|
let root_key_reader = reader.as_slice();
|
||||||
@@ -196,7 +191,6 @@ impl KeyHolder {
|
|||||||
self.state = State::Unsealed {
|
self.state = State::Unsealed {
|
||||||
root_key,
|
root_key,
|
||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
integrity_key,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Keyholder bootstrapped successfully");
|
info!("Keyholder bootstrapped successfully");
|
||||||
@@ -229,9 +223,7 @@ impl KeyHolder {
|
|||||||
error!("Broken database: invalid salt for root key");
|
error!("Broken database: invalid salt for root key");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?;
|
})?;
|
||||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||||
let integrity_key =
|
|
||||||
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
|
|
||||||
|
|
||||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||||
|
|
||||||
@@ -251,11 +243,10 @@ impl KeyHolder {
|
|||||||
|
|
||||||
self.state = State::Unsealed {
|
self.state = State::Unsealed {
|
||||||
root_key_history_id: current_key.id,
|
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!(?err, "Broken database: invalid encryption key size");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?,
|
})?,
|
||||||
integrity_key,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Keyholder unsealed successfully");
|
info!("Keyholder unsealed successfully");
|
||||||
@@ -270,12 +261,12 @@ impl KeyHolder {
|
|||||||
purpose_tag: Vec<u8>,
|
purpose_tag: Vec<u8>,
|
||||||
data_parts: Vec<Vec<u8>>,
|
data_parts: Vec<Vec<u8>>,
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
let State::Unsealed { integrity_key, .. } = &mut self.state else {
|
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||||
return Err(Error::NotBootstrapped);
|
return Err(Error::NotBootstrapped);
|
||||||
};
|
};
|
||||||
|
|
||||||
let tag = v1::compute_integrity_tag(
|
let tag = compute_integrity_tag(
|
||||||
integrity_key,
|
root_key,
|
||||||
&purpose_tag,
|
&purpose_tag,
|
||||||
data_parts.iter().map(Vec::as_slice),
|
data_parts.iter().map(Vec::as_slice),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ use crate::{
|
|||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
keyholder::{self, SignIntegrityTag},
|
keyholder::{self, SignIntegrityTag},
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||||
},
|
}, crypto::integrity::v1::USERAGENT_INTEGRITY_TAG, db::schema
|
||||||
db::schema,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -269,7 +268,7 @@ where
|
|||||||
.actors
|
.actors
|
||||||
.key_holder
|
.key_holder
|
||||||
.ask(SignIntegrityTag {
|
.ask(SignIntegrityTag {
|
||||||
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
|
purpose_tag: USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||||
data_parts: vec![
|
data_parts: vec![
|
||||||
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
|
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
|
||||||
pubkey.to_stored_bytes(),
|
pubkey.to_stored_bytes(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
|
|||||||
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
||||||
use crate::actors::keyholder::KeyHolderState;
|
use crate::actors::keyholder::KeyHolderState;
|
||||||
use crate::actors::user_agent::session::Error;
|
use crate::actors::user_agent::session::Error;
|
||||||
|
use crate::crypto::integrity::v1::USERAGENT_INTEGRITY_TAG;
|
||||||
use crate::db::models::{
|
use crate::db::models::{
|
||||||
EvmWalletAccess, KeyType, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
EvmWalletAccess, KeyType, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
||||||
};
|
};
|
||||||
@@ -112,7 +113,7 @@ impl UserAgentSession {
|
|||||||
.actors
|
.actors
|
||||||
.key_holder
|
.key_holder
|
||||||
.ask(SignIntegrityTag {
|
.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],
|
data_parts: vec![(key_type as i32).to_be_bytes().to_vec(), public_key],
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod v1;
|
||||||
111
server/crates/arbiter-server/src/crypto/encryption/v1.rs
Normal file
111
server/crates/arbiter-server/src/crypto/encryption/v1.rs
Normal file
@@ -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<u8> {
|
||||||
|
self.0.to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
||||||
|
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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
server/crates/arbiter-server/src/crypto/integrity/mod.rs
Normal file
1
server/crates/arbiter-server/src/crypto/integrity/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod v1;
|
||||||
77
server/crates/arbiter-server/src/crypto/integrity/v1.rs
Normal file
77
server/crates/arbiter-server/src/crypto/integrity/v1.rs
Normal file
@@ -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<Item = &'a [u8]>,
|
||||||
|
{
|
||||||
|
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
||||||
|
|
||||||
|
let mut output_tag = [0u8; 32];
|
||||||
|
integrity_key.0.read_inline(|integrity_key_bytes: &Key| {
|
||||||
|
let mut mac = <HmacSha256 as hmac::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
153
server/crates/arbiter-server/src/crypto/mod.rs
Normal file
153
server/crates/arbiter-server/src/crypto/mod.rs
Normal file
@@ -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<Key>);
|
||||||
|
impl From<SafeCell<Key>> for KeyCell {
|
||||||
|
fn from(value: SafeCell<Key>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(mut value: SafeCell<Vec<u8>>) -> Result<Self, Self::Error> {
|
||||||
|
let value = value.read();
|
||||||
|
if value.len() != size_of::<Key>() {
|
||||||
|
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<Vec<u8>>,
|
||||||
|
) -> 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<Vec<u8>>,
|
||||||
|
) -> 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<u8> = 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<Vec<u8>, 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<Vec<u8>>, 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
use crate::context::ServerContext;
|
use crate::context::ServerContext;
|
||||||
|
|
||||||
|
pub mod crypto;
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, KeyHolder},
|
actors::keyholder::{Error, KeyHolder}, crypto::encryption::v1::{Nonce, ROOT_KEY_TAG}, db::{self, models, schema}, safe_cell::{SafeCell, SafeCellHandle as _}
|
||||||
db::{self, models, schema},
|
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
};
|
||||||
use diesel::{QueryDsl, SelectableHelper};
|
use diesel::{QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -27,13 +25,13 @@ async fn test_bootstrap() {
|
|||||||
assert_eq!(row.schema_version, 1);
|
assert_eq!(row.schema_version, 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
row.tag,
|
row.tag,
|
||||||
arbiter_server::actors::keyholder::encryption::v1::ROOT_KEY_TAG
|
ROOT_KEY_TAG
|
||||||
);
|
);
|
||||||
assert!(!row.ciphertext.is_empty());
|
assert!(!row.ciphertext.is_empty());
|
||||||
assert!(!row.salt.is_empty());
|
assert!(!row.salt.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
row.data_encryption_nonce,
|
row.data_encryption_nonce,
|
||||||
arbiter_server::actors::keyholder::encryption::v1::Nonce::default().to_vec()
|
Nonce::default().to_vec()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, encryption::v1},
|
actors::keyholder::Error, crypto::encryption::v1::Nonce, db::{self, models, schema}, safe_cell::{SafeCell, SafeCellHandle as _}
|
||||||
db::{self, models, schema},
|
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
|
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
|
||||||
use diesel_async::RunQueryDsl;
|
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");
|
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
||||||
|
|
||||||
for (i, row) in rows.iter().enumerate() {
|
for (i, row) in rows.iter().enumerate() {
|
||||||
let mut expected = v1::Nonce::default();
|
let mut expected = Nonce::default();
|
||||||
for _ in 0..=i {
|
for _ in 0..=i {
|
||||||
expected.increment();
|
expected.increment();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,8 +279,12 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_err = task.await.unwrap();
|
||||||
|
|
||||||
|
println!("Received expected error: {expected_err:#?}");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
task.await.unwrap(),
|
expected_err,
|
||||||
Err(auth::Error::InvalidChallengeSolution)
|
Err(auth::Error::InvalidChallengeSolution)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user