use secrecy::{ExposeSecret as _, SecretString};
use crate::error::{Error, Result};
use crate::pairing::{self, Adoption, JoinerSession, NonceLog, PendingSession, QrPayload};
use crate::recovery::{self, KdfParams};
use crate::secret::SecretBytes;
use crate::vault::MasterKey;
use crate::vault::{Timestamp, Vault, VaultFile};
use qualia_dik::{DeviceId, DevicePublicKey, DeviceSignature};
pub trait DeviceKeys {
fn get_or_create(&mut self, label: &str) -> Result<DevicePublicKey>;
fn sign(&mut self, label: &str, message: &[u8]) -> Result<DeviceSignature>;
}
pub trait SecureStore {
fn load_master_key(&self) -> Result<Option<MasterKey>>;
fn store_master_key(&mut self, key: &MasterKey) -> Result<()>;
fn delete_master_key(&mut self) -> Result<()>;
fn is_hardware_backed(&self) -> bool;
}
#[derive(Debug)]
pub struct Registered {
device: DevicePublicKey,
vault: VaultFile,
hardware_backed: bool,
}
impl Registered {
#[must_use]
pub const fn device(&self) -> &DevicePublicKey {
&self.device
}
#[must_use]
pub fn device_id(&self) -> DeviceId {
self.device.device_id()
}
#[must_use]
pub const fn vault(&self) -> &VaultFile {
&self.vault
}
pub const fn vault_mut(&mut self) -> &mut VaultFile {
&mut self.vault
}
#[must_use]
pub const fn is_hardware_backed(&self) -> bool {
self.hardware_backed
}
#[must_use]
pub fn into_vault(self) -> VaultFile {
self.vault
}
#[must_use]
pub fn into_parts(self) -> (MasterKey, Vault) {
self.vault.into_parts()
}
}
pub fn registration(
keys: &mut dyn DeviceKeys,
store: &mut dyn SecureStore,
label: &str,
) -> Result<Registered> {
let master = MasterKey::generate()?;
let device = keys.get_or_create(label)?;
let hardware_backed = store.is_hardware_backed();
store.store_master_key(&master)?;
Ok(Registered {
device,
vault: VaultFile::with_master_key(master),
hardware_backed,
})
}
pub struct Login {
payload: QrPayload,
pending: Option<PendingSession>,
adoption: Option<Adoption>,
used: NonceLog,
}
impl core::fmt::Debug for Login {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Login")
.field("capabilities", &self.payload.capabilities())
.field("outstanding", &self.pending.is_some())
.field("authorized", &self.adoption.is_some())
.field("used_sessions", &self.used.len())
.finish_non_exhaustive()
}
}
impl Login {
pub fn new() -> Result<Self> {
let (payload, pending) = pairing::begin()?;
Ok(Self {
payload,
pending: Some(pending),
adoption: None,
used: NonceLog::new(),
})
}
#[must_use]
pub const fn payload(&self) -> &QrPayload {
&self.payload
}
pub fn authorize(&mut self, msg1: &[u8], master: &MasterKey) -> Result<Vec<u8>> {
let pending = self
.pending
.take()
.ok_or(Error::PairingAuthenticationFailed)?;
let adoption = pending.accept(msg1, master)?;
if !self.used.record(*adoption.session_id()) {
return Err(Error::PairingAuthenticationFailed);
}
let reply = adoption.reply().to_vec();
self.adoption = Some(adoption);
Ok(reply)
}
#[must_use]
pub fn session_id(&self) -> Option<&[u8; pairing::SESSION_ID_LEN]> {
self.adoption.as_ref().map(Adoption::session_id)
}
#[must_use]
pub const fn used_sessions(&self) -> &NonceLog {
&self.used
}
#[must_use]
pub fn with_used_sessions(mut self, used: NonceLog) -> Self {
self.used = used;
self
}
#[must_use]
pub fn joiner_device(&self) -> Option<DeviceId> {
self.adoption
.as_ref()
.and_then(|a| DevicePublicKey::from_sec1_bytes(a.joiner_identity()).ok())
.map(|key| key.device_id())
}
}
pub struct Joiner {
session: std::sync::Mutex<Option<JoinerSession>>,
label: String,
session_id: Option<[u8; pairing::SESSION_ID_LEN]>,
request: Vec<u8>,
}
impl core::fmt::Debug for Joiner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Joiner")
.field("label", &self.label)
.field("requested", &self.session_id.is_some())
.finish_non_exhaustive()
}
}
impl Joiner {
pub fn new(payload: &QrPayload, label: &str) -> Result<Self> {
Ok(Self {
session: std::sync::Mutex::new(Some(pairing::join(payload)?)),
label: label.to_owned(),
session_id: None,
request: Vec::new(),
})
}
pub fn from_base32(text: &str, label: &str) -> Result<Self> {
Self::new(&QrPayload::from_base32(text)?, label)
}
pub fn request(&mut self, keys: &mut dyn DeviceKeys) -> Result<Vec<u8>> {
if self.request.is_empty() {
let identity = keys.get_or_create(&self.label)?;
let (message, session_id) = {
let session = self.session_guard();
let session = session.as_ref().ok_or(Error::PairingAuthenticationFailed)?;
session.request(identity.as_sec1_bytes())?
};
self.request = message;
self.session_id = Some(session_id);
}
Ok(self.request.clone())
}
#[must_use]
pub const fn session_id(&self) -> Option<&[u8; pairing::SESSION_ID_LEN]> {
self.session_id.as_ref()
}
pub fn complete(
&self,
msg2: &[u8],
keys: &mut dyn DeviceKeys,
store: &mut dyn SecureStore,
) -> Result<MasterKey> {
let identity = keys.get_or_create(&self.label)?;
let session = self
.session_guard()
.take()
.ok_or(Error::PairingAuthenticationFailed)?;
let master = session.open(msg2, identity.as_sec1_bytes())?;
store.store_master_key(&master)?;
Ok(master)
}
#[must_use]
pub fn identity(&self) -> Option<[u8; pairing::PUBLIC_KEY_LEN]> {
self.session_guard()
.as_ref()
.map(|session| *session.public_key())
}
fn session_guard(&self) -> std::sync::MutexGuard<'_, Option<JoinerSession>> {
self.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
pub fn export_recovery_key(
master: &MasterKey,
passphrase: &SecretString,
params: Option<KdfParams>,
) -> Result<Vec<u8>> {
recovery::export(
master,
passphrase,
¶ms.unwrap_or_else(KdfParams::recommended),
)
}
pub fn import_recovery_key(
file: &[u8],
passphrase: &SecretString,
store: &mut dyn SecureStore,
) -> Result<MasterKey> {
let master = recovery::import(file, passphrase)?;
store.store_master_key(&master)?;
Ok(master)
}
pub fn open_vault(store: &dyn SecureStore, data: &[u8]) -> Result<VaultFile> {
let master = store.load_master_key()?.ok_or(Error::DeviceNotRegistered)?;
VaultFile::parse(master, data)
}
pub fn master_key_from_bytes(bytes: SecretBytes) -> Result<MasterKey> {
MasterKey::from_bytes(bytes)
}
#[must_use]
pub fn master_key_to_bytes(key: &MasterKey) -> SecretBytes {
crate::vault::master_key_to_bytes(key)
}
#[must_use]
pub fn empty_vault(master: MasterKey) -> VaultFile {
VaultFile::with_master_key(master)
}
pub type Now = Timestamp;
pub const fn check_now(now: Now) -> Result<Now> {
if now == 0 {
return Err(Error::TimestampZero);
}
Ok(now)
}
#[must_use]
pub fn passphrase_len(passphrase: &SecretString) -> usize {
passphrase.expose_secret().chars().count()
}
impl<T: DeviceKeys + ?Sized> DeviceKeys for &mut T {
fn get_or_create(&mut self, label: &str) -> Result<DevicePublicKey> {
(**self).get_or_create(label)
}
fn sign(&mut self, label: &str, message: &[u8]) -> Result<DeviceSignature> {
(**self).sign(label, message)
}
}
impl<T: SecureStore + ?Sized> SecureStore for &mut T {
fn load_master_key(&self) -> Result<Option<MasterKey>> {
(**self).load_master_key()
}
fn store_master_key(&mut self, key: &MasterKey) -> Result<()> {
(**self).store_master_key(key)
}
fn delete_master_key(&mut self) -> Result<()> {
(**self).delete_master_key()
}
fn is_hardware_backed(&self) -> bool {
(**self).is_hardware_backed()
}
}
pub fn expect_device(
actual: &DevicePublicKey,
claimed: &[u8; pairing::PUBLIC_KEY_LEN],
) -> Result<()> {
if actual.as_sec1_bytes() != claimed {
return Err(Error::DeviceIdentityMismatch);
}
Ok(())
}
pub fn verify_device(
public_key: &DevicePublicKey,
message: &[u8],
signature: &DeviceSignature,
) -> Result<()> {
Ok(qualia_dik::device::verify(public_key, message, signature)?)
}
#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::{SigningKey, signature::Signer as _};
use qualia_dik::device::SIGNATURE_LEN;
use std::collections::HashMap;
struct FakeKeys {
keys: HashMap<String, SigningKey>,
creates: usize,
}
impl FakeKeys {
fn new() -> Self {
Self {
keys: HashMap::new(),
creates: 0,
}
}
}
impl DeviceKeys for FakeKeys {
fn get_or_create(&mut self, label: &str) -> Result<DevicePublicKey> {
if !self.keys.contains_key(label) {
let scalar = label.bytes().fold(1_u8, u8::wrapping_add).max(1);
let mut seed = [0_u8; 32];
seed[31] = scalar;
self.keys.insert(
label.to_owned(),
SigningKey::from_slice(&seed).expect("the test scalar must be valid"),
);
self.creates += 1;
}
let key = &self.keys[label];
let point = key.verifying_key().to_sec1_point(false);
Ok(DevicePublicKey::from_sec1_bytes(point.as_bytes())?)
}
fn sign(&mut self, label: &str, message: &[u8]) -> Result<DeviceSignature> {
let key = self.keys.get(label).ok_or(Error::DeviceKeyMissing)?;
let signature: p256::ecdsa::Signature = key.sign(message);
Ok(DeviceSignature::from_bytes(&signature.to_bytes())?)
}
}
#[derive(Default)]
struct FakeStore {
master: Option<MasterKey>,
hardware: bool,
fail_store: bool,
deletes: usize,
}
impl SecureStore for FakeStore {
fn load_master_key(&self) -> Result<Option<MasterKey>> {
Ok(self.master.as_ref().map(|k| {
MasterKey::from_bytes(master_key_to_bytes(k))
.expect("the length is guaranteed by construction")
}))
}
fn store_master_key(&mut self, key: &MasterKey) -> Result<()> {
if self.fail_store {
return Err(Error::SecureStoreWriteFailed {
reason: "failure injected by the test",
});
}
self.master = Some(
MasterKey::from_bytes(master_key_to_bytes(key))
.expect("the length is guaranteed by construction"),
);
Ok(())
}
fn delete_master_key(&mut self) -> Result<()> {
self.deletes += 1;
self.master = None;
Ok(())
}
fn is_hardware_backed(&self) -> bool {
self.hardware
}
}
const LABEL: &str = "qualia2fa-device";
fn registered() -> (FakeKeys, FakeStore, Registered) {
let mut keys = FakeKeys::new();
let mut store = FakeStore::default();
let result =
registration(&mut keys, &mut store, LABEL).expect("registration should succeed");
(keys, store, result)
}
#[test]
fn registration_produces_a_usable_device() {
let (keys, store, result) = registered();
assert!(
store.master.is_some(),
"the master key must be in the platform store"
);
assert!(
keys.keys.contains_key(LABEL),
"the device identity must have been created and be retrievable by the same label"
);
assert!(!result.is_hardware_backed());
let data = result
.vault
.serialize()
.expect("serialization should succeed");
let opened =
open_vault(&store, &data).expect("the stored master key should open the vault");
assert!(opened.vault().is_empty());
assert_eq!(
opened.master_key().expose(),
result.vault.master_key().expose()
);
}
#[test]
fn each_registration_generates_a_fresh_master_key() {
let (_k1, _s1, first) = registered();
let (_k2, _s2, second) = registered();
assert_ne!(
first.vault.master_key().expose(),
second.vault.master_key().expose()
);
}
#[test]
fn device_identity_is_stable_across_calls() {
let mut keys = FakeKeys::new();
let first = keys
.get_or_create(LABEL)
.expect("the first creation should succeed");
let second = keys
.get_or_create(LABEL)
.expect("the second retrieval should succeed");
assert_eq!(first.as_sec1_bytes(), second.as_sec1_bytes());
assert_eq!(keys.creates, 1, "must not create a second identity");
}
#[test]
fn registration_does_not_create_a_vault_when_storage_fails() {
let mut keys = FakeKeys::new();
let mut store = FakeStore {
fail_store: true,
..FakeStore::default()
};
let error = registration(&mut keys, &mut store, LABEL).expect_err("should fail");
assert!(
matches!(error, Error::SecureStoreWriteFailed { .. }),
"the error should be a write failure, got {error:?}"
);
assert!(store.master.is_none());
}
#[test]
fn opening_without_a_stored_key_reports_not_registered() {
let store = FakeStore::default();
let (_k, _s, result) = registered();
let data = result
.vault
.serialize()
.expect("serialization should succeed");
let error = open_vault(&store, &data).expect_err("must fail without a stored master key");
assert!(matches!(error, Error::DeviceNotRegistered), "got {error:?}");
}
#[test]
fn an_empty_vault_rejects_a_wrong_key() {
let (_k, _s, result) = registered();
assert!(
result.vault.vault().is_empty(),
"precondition: this vault really is empty"
);
let data = result
.vault
.serialize()
.expect("serialization should succeed");
let (_k2, store, device2) = registered();
assert_ne!(
device2.vault().master_key().expose(),
result.vault.master_key().expose(),
"the two devices must have different master keys, or this test proves nothing"
);
let error =
open_vault(&store, &data).expect_err("an empty vault must reject the wrong key");
assert!(
matches!(error, Error::VaultDecryptionFailed),
"got {error:?}"
);
}
#[test]
fn a_vault_with_entries_rejects_the_wrong_key() {
use crate::uri;
use crate::vault::{Timestamp, entry_from_parsed_uri};
let (_k, _s, mut result) = registered();
let parsed = uri::parse("otpauth://totp/ACME:alice?secret=JBSWY3DPEHPK3PXP&issuer=ACME")
.expect("the URI should be valid");
const NOW: Timestamp = 1_700_000_000;
let entry = entry_from_parsed_uri(parsed, NOW).expect("the entry should be built");
result
.vault_mut()
.vault_mut()
.insert(entry)
.expect("the insert should succeed");
let data = result
.vault
.serialize()
.expect("serialization should succeed");
let (_k2, store, _device2) = registered();
let error =
open_vault(&store, &data).expect_err("a vault with entries must reject the wrong key");
assert!(
matches!(error, Error::VaultDecryptionFailed),
"got {error:?}"
);
}
#[test]
fn a_login_can_only_be_completed_once() {
let (_old_keys, _old_store, account) = registered();
let master = account.vault.master_key();
let mut login = Login::new().expect("the login should start");
let payload = login.payload().clone();
let mut keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let request = joiner
.request(&mut keys)
.expect("the request should be produced");
let reply = login
.authorize(&request, master)
.expect("authorization should succeed");
let mut store = FakeStore::default();
joiner
.complete(&reply, &mut keys, &mut store)
.expect("the first call should succeed");
let error = joiner
.complete(&reply, &mut keys, &mut store)
.expect_err("the second call must fail");
assert!(
matches!(error, Error::PairingAuthenticationFailed),
"must report the same error as a forged reply, got {error:?}"
);
assert!(joiner.identity().is_none());
}
#[test]
fn login_transfers_the_exact_master_key() {
let (_old_keys, old_store, account) = registered();
let master = account.vault.master_key();
let mut login = Login::new().expect("the login should start");
let payload = login.payload().clone();
let mut joiner_keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let request = joiner
.request(&mut joiner_keys)
.expect("the request should be produced");
let reply = login
.authorize(&request, master)
.expect("authorization should succeed")
.to_vec();
let mut joiner_store = FakeStore::default();
let received = joiner
.complete(&reply, &mut joiner_keys, &mut joiner_store)
.expect("the login should complete");
assert_eq!(
received.expose(),
master.expose(),
"the new device's master key must equal the old device's"
);
assert!(
joiner_store.master.is_some(),
"the master key must be in the new device's platform store"
);
let data = account
.vault
.serialize()
.expect("serialization should succeed");
let opened = open_vault(&joiner_store, &data)
.expect("the new device should open the old device's vault");
assert_eq!(
opened.master_key().expose(),
old_store.master.as_ref().unwrap().expose()
);
}
#[test]
fn login_reports_the_session_for_persistence() {
let (_k, _s, account) = registered();
let mut login = Login::new().expect("the login should start");
assert!(
login.session_id().is_none(),
"no session identifier before authorization"
);
assert!(login.joiner_device().is_none());
let payload = login.payload().clone();
let mut keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let request = joiner
.request(&mut keys)
.expect("the request should be produced");
let _reply = login
.authorize(&request, account.vault.master_key())
.expect("authorization should succeed");
let session_id = login
.session_id()
.expect("a session identifier should exist after authorization");
assert_eq!(
session_id,
joiner
.session_id()
.expect("both sides must derive the same identifier")
);
assert!(
login.joiner_device().is_some(),
"the peer identity should be available"
);
assert_eq!(login.used_sessions().len(), 1);
}
#[test]
fn the_same_request_cannot_be_authorized_twice() {
let (_k, _s, account) = registered();
let mut login = Login::new().expect("the login should start");
let payload = login.payload().clone();
let mut keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let request = joiner
.request(&mut keys)
.expect("the request should be produced");
let master = account.vault.master_key();
login
.authorize(&request, master)
.expect("the first call should succeed");
let error = login
.authorize(&request, master)
.expect_err("the second call must fail");
assert!(
matches!(
error,
Error::PairingAuthenticationFailed | Error::AuthMessageMalformed { .. }
),
"got {error:?}"
);
}
#[test]
fn a_request_from_another_login_is_rejected() {
let (_k, _s, account) = registered();
let first = Login::new().expect("the login should start");
let payload_from_first = first.payload().clone();
let mut second = Login::new().expect("the login should start");
let mut keys = FakeKeys::new();
let mut joiner = Joiner::new(&payload_from_first, "new-device")
.expect("the joiner session should start");
let request = joiner
.request(&mut keys)
.expect("the request should be produced");
let error = second
.authorize(&request, account.vault.master_key())
.expect_err("a request from another device's payload must not be accepted");
assert!(
matches!(error, Error::PairingAuthenticationFailed),
"got {error:?}"
);
assert!(
second.session_id().is_none(),
"a failure must leave no session identifier"
);
}
#[test]
fn a_reply_encrypted_for_another_session_cannot_be_opened() {
let (_k, _s, account) = registered();
let mut keys = FakeKeys::new();
let mut login_a = Login::new().expect("the login should start");
let payload_a = login_a.payload().clone();
let mut joiner_a =
Joiner::new(&payload_a, "new-device").expect("the joiner session should start");
let request_a = joiner_a
.request(&mut keys)
.expect("the request should be produced");
login_a
.authorize(&request_a, account.vault.master_key())
.expect("session A should authorize");
let mut login_b = Login::new().expect("the login should start");
let payload_b = login_b.payload().clone();
let mut joiner_b =
Joiner::new(&payload_b, "new-device").expect("the joiner session should start");
let request_b = joiner_b
.request(&mut keys)
.expect("the request should be produced");
let reply_b = login_b
.authorize(&request_b, account.vault.master_key())
.expect("authorization should succeed");
let mut store = FakeStore::default();
let error = joiner_a
.complete(&reply_b, &mut keys, &mut store)
.expect_err("a reply from another session must not be accepted");
assert!(
matches!(error, Error::PairingAuthenticationFailed),
"got {error:?}"
);
assert!(store.master.is_none(), "a failure must leave no master key");
}
#[test]
fn tampering_with_the_reply_is_detected() {
let (_k, _s, account) = registered();
let mut login = Login::new().expect("the login should start");
let payload = login.payload().clone();
let mut keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let request = joiner
.request(&mut keys)
.expect("the request should be produced");
let reply = login
.authorize(&request, account.vault.master_key())
.expect("authorization should succeed")
.to_vec();
for index in 0..reply.len() {
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let _ = joiner
.request(&mut keys)
.expect("the request should be produced");
let mut tampered = reply.clone();
tampered[index] ^= 0x01;
let mut store = FakeStore::default();
assert!(
joiner.complete(&tampered, &mut keys, &mut store).is_err(),
"byte {index} was flipped, yet still accepted"
);
assert!(store.master.is_none(), "a failure must leave no master key");
}
}
#[test]
fn requesting_twice_returns_the_same_message() {
let login = Login::new().expect("the login should start");
let payload = login.payload().clone();
let mut keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let first = joiner
.request(&mut keys)
.expect("the request should be produced");
let second = joiner
.request(&mut keys)
.expect("the request should be produced");
assert_eq!(first, second);
assert_eq!(
keys.creates, 1,
"the second request must not create a new identity"
);
}
#[test]
fn recovery_roundtrip_restores_the_master_key() {
let (_k, mut store, account) = registered();
let master = account.vault.master_key();
let params = KdfParams::new(8, 1, 1).expect("the minimum parameters should be valid");
let passphrase = SecretString::from("correct horse battery staple");
let file =
export_recovery_key(master, &passphrase, Some(params)).expect("export should succeed");
let restored =
import_recovery_key(&file, &passphrase, &mut store).expect("import should succeed");
assert_eq!(restored.expose(), master.expose());
let data = account
.vault
.serialize()
.expect("serialization should succeed");
open_vault(&store, &data).expect("the vault should open after recovery");
}
#[test]
fn recovery_with_a_wrong_passphrase_leaves_storage_untouched() {
let mut store = FakeStore::default();
let master = MasterKey::generate().expect("randomness should be available");
let params = KdfParams::new(8, 1, 1).expect("the minimum parameters should be valid");
let file = export_recovery_key(&master, &SecretString::from("right"), Some(params))
.expect("export should succeed");
let error = import_recovery_key(&file, &SecretString::from("wrong"), &mut store)
.expect_err("must fail with the wrong passphrase");
assert!(
matches!(error, Error::RecoveryAuthenticationFailed),
"got {error:?}"
);
assert!(
store.master.is_none(),
"a failure must not write a master key"
);
}
#[test]
fn an_empty_passphrase_is_rejected() {
let master = MasterKey::generate().expect("randomness should be available");
let error = export_recovery_key(&master, &SecretString::from(""), None)
.expect_err("an empty passphrase must be rejected");
assert!(
matches!(error, Error::RecoveryPassphraseEmpty),
"got {error:?}"
);
}
#[test]
fn hardware_backing_is_reported_truthfully() {
let mut keys = FakeKeys::new();
let mut soft = FakeStore::default();
let result = registration(&mut keys, &mut soft, LABEL).expect("should succeed");
assert!(!result.is_hardware_backed());
let mut keys = FakeKeys::new();
let mut hard = FakeStore {
hardware: true,
..FakeStore::default()
};
let result = registration(&mut keys, &mut hard, LABEL).expect("should succeed");
assert!(result.is_hardware_backed());
}
#[test]
fn device_identity_comparison_works_both_ways() {
let mut keys = FakeKeys::new();
let actual = keys.get_or_create(LABEL).expect("should succeed");
expect_device(&actual, actual.as_sec1_bytes()).expect("the same identity should pass");
let other = keys.get_or_create("other").expect("should succeed");
let error = expect_device(&other, actual.as_sec1_bytes())
.expect_err("a different identity must fail");
assert!(
matches!(error, Error::DeviceIdentityMismatch),
"got {error:?}"
);
}
#[test]
fn signatures_verify_and_have_a_stable_length() {
let mut keys = FakeKeys::new();
let public = keys.get_or_create(LABEL).expect("should succeed");
let message = b"hello";
let signature = keys.sign(LABEL, message).expect("signing should succeed");
assert_eq!(signature.as_bytes().len(), SIGNATURE_LEN);
verify_device(&public, message, &signature).expect("verification should pass");
assert!(verify_device(&public, b"other", &signature).is_err());
}
#[test]
fn signing_without_an_identity_fails_cleanly() {
let mut keys = FakeKeys::new();
let error = keys.sign("missing", b"x").expect_err("should fail");
assert!(matches!(error, Error::DeviceKeyMissing), "got {error:?}");
}
#[test]
fn a_zero_timestamp_is_rejected() {
assert!(check_now(0).is_err());
assert_eq!(check_now(1).expect("a non-zero timestamp should pass"), 1);
}
#[test]
fn passphrase_length_is_available_without_exposing_the_passphrase() {
let passphrase = SecretString::from("hunter2");
assert_eq!(passphrase_len(&passphrase), 7);
assert_eq!(passphrase_len(&SecretString::from("")), 0);
assert_eq!(passphrase_len(&SecretString::from("口令")), 2);
}
#[test]
fn debug_output_is_redacted() {
let (_k, _s, account) = registered();
let login = Login::new().expect("the login should start");
let payload = login.payload().clone();
let mut keys = FakeKeys::new();
let mut joiner =
Joiner::new(&payload, "new-device").expect("the joiner session should start");
let _ = joiner
.request(&mut keys)
.expect("the request should be produced");
let texts: [(&str, String); 3] = [
("Login", format!("{login:?}")),
("Joiner", format!("{joiner:?}")),
("MasterKey", format!("{:?}", account.vault.master_key())),
];
let secrets: [(&str, String); 2] = [
(
"master key hex",
account
.vault
.master_key()
.expose()
.iter()
.fold(String::new(), |mut acc, b| {
use core::fmt::Write as _;
let _ = write!(acc, "{b:02x}");
acc
}),
),
(
"master key decimal",
account
.vault
.master_key()
.expose()
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(","),
),
];
for (name, text) in texts {
for (secret_name, secret) in &secrets {
assert!(
!text.contains(secret.as_str()),
"{name} Debug output contains the {secret_name}: {text}"
);
}
}
}
#[test]
fn an_empty_vault_is_openable() {
let master = MasterKey::generate().expect("randomness should be available");
let vault = empty_vault(
MasterKey::from_bytes(master_key_to_bytes(&master)).expect("the length is correct"),
);
let data = vault.serialize().expect("serialization should succeed");
let mut store = FakeStore::default();
store
.store_master_key(&master)
.expect("the write should succeed");
let opened = open_vault(&store, &data).expect("the vault should open");
assert!(opened.vault().is_empty());
}
#[test]
fn mutable_references_forward_correctly() {
let mut keys = FakeKeys::new();
let via_ref = {
let borrowed: &mut FakeKeys = &mut keys;
let dynamic: &mut dyn DeviceKeys = borrowed;
dynamic.get_or_create(LABEL)
}
.expect("should succeed");
let direct = keys.get_or_create(LABEL).expect("should succeed");
assert_eq!(via_ref.as_sec1_bytes(), direct.as_sec1_bytes());
assert_eq!(keys.creates, 1);
}
}