mod adjudicate;
pub mod compress;
mod merge;
pub mod record;
mod seen;
use crate::vault::crypto;
use crate::vault::{EntryId, MasterKey, NONCE_LEN, TAG_LEN};
use crate::{Error, Result};
pub use adjudicate::{Adjudicated, Decision, adjudicate};
pub use merge::{
Conflict, ConflictKind, HasEntryId, MergeOutcome, MergedRecord, RecordSet, merge_into,
};
use qualia_dik::{DEVICE_ID_LEN, DeviceId};
pub use record::{OpenedRecord, open as open_record, seal_entry, seal_tombstone};
pub use seen::{MAX_SEEN_SYNC_IDS, SeenSyncIds};
pub const MAGIC: &[u8; 8] = b"Q2FASYNC";
pub const FORMAT_VERSION: u8 = 3;
#[cfg(test)]
pub(crate) const LEGACY_FORMAT_VERSION_1: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PayloadKind {
Vault,
Request,
}
impl PayloadKind {
#[must_use]
pub const fn as_byte(self) -> u8 {
match self {
Self::Vault => 0x01,
Self::Request => 0x02,
}
}
#[must_use]
pub const fn from_byte(value: u8) -> Option<Self> {
match value {
0x01 => Some(Self::Vault),
0x02 => Some(Self::Request),
_ => None,
}
}
}
pub const HEADER_LEN: usize = MAGIC.len() + 1 + 1 + SYNC_ID_LEN + DEVICE_ID_LEN;
pub const SYNC_ID_LEN: usize = 32;
pub type SyncId = [u8; SYNC_ID_LEN];
const RECORD_HEADER_LEN: usize = EntryId::LEN + NONCE_LEN + 4;
pub const MAX_RECORDS: usize = 1_000_000;
pub const MAX_PLAINTEXT_LEN: usize = 64 * 1024 * 1024;
pub const HKDF_SALT_SYNC: [u8; 32] = *b"Qualia2FA sync salt v1 (records)";
pub fn derive_record_key(master: &MasterKey) -> Result<[u8; 32]> {
crypto::derive_key_with_salt(master, &HKDF_SALT_SYNC, crypto::HKDF_INFO_SYNC_RECORD)
}
pub fn derive_envelope_key(master: &MasterKey) -> Result<[u8; 32]> {
crypto::derive_key_with_salt(master, &HKDF_SALT_SYNC, crypto::HKDF_INFO_SYNC_ENVELOPE)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub entry_id: EntryId,
pub nonce: [u8; NONCE_LEN],
pub ciphertext: Vec<u8>,
}
impl merge::HasEntryId for Record {
fn entry_id(&self) -> EntryId {
self.entry_id
}
}
impl Record {
#[must_use]
pub fn encoded_len(&self) -> usize {
RECORD_HEADER_LEN + self.ciphertext.len()
}
}
#[derive(Debug)]
pub struct Payload {
pub sync_id: SyncId,
pub sender: DeviceId,
pub records: RecordSet<Record>,
kind: PayloadKind,
}
impl Payload {
pub fn new_sync_id() -> Result<SyncId> {
let mut id = [0_u8; SYNC_ID_LEN];
crate::getrandom::fill(&mut id).map_err(|_| Error::RandomnessUnavailable)?;
Ok(id)
}
#[must_use]
pub const fn kind(&self) -> PayloadKind {
self.kind
}
pub fn request(sender: DeviceId) -> Result<Self> {
Ok(Self {
sync_id: Self::new_sync_id()?,
sender,
records: RecordSet::<Record>::new(),
kind: PayloadKind::Request,
})
}
pub fn from_vault(
master: &MasterKey,
sender: DeviceId,
vault: &crate::vault::Vault,
) -> Result<Self> {
let record_key = derive_record_key(master)?;
let mut records = RecordSet::<Record>::new();
for (id, entry) in vault.iter() {
if vault.get_tombstone(id).is_some() {
continue;
}
records.insert(record::seal_entry(&record_key, id, entry)?);
}
for (_, tombstone) in vault.iter_tombstones() {
records.insert(record::seal_tombstone(&record_key, tombstone)?);
}
for (id, group) in vault.groups() {
records.insert(record::seal_group(&record_key, id, group)?);
}
Ok(Self {
sync_id: Self::new_sync_id()?,
sender,
records,
kind: PayloadKind::Vault,
})
}
pub fn peek(data: &[u8]) -> Result<PayloadHeader> {
let mut reader = Reader::new(data);
let magic = reader.take(MAGIC.len()).ok_or(Error::SyncTruncated)?;
if magic != MAGIC {
return Err(Error::SyncBadMagic);
}
let version = reader.u8_bare().ok_or(Error::SyncTruncated)?;
check_version(version)?;
let kind_byte = reader.u8_bare().ok_or(Error::SyncTruncated)?;
let kind = PayloadKind::from_byte(kind_byte)
.ok_or(Error::SyncUnknownPayloadKind { value: kind_byte })?;
let sync_id: SyncId = reader
.take(SYNC_ID_LEN)
.ok_or(Error::SyncTruncated)?
.try_into()
.map_err(|_| Error::SyncTruncated)?;
let sender_bytes: [u8; DEVICE_ID_LEN] = reader
.take(DEVICE_ID_LEN)
.ok_or(Error::SyncTruncated)?
.try_into()
.map_err(|_| Error::SyncTruncated)?;
Ok(PayloadHeader {
version,
kind,
sync_id,
sender: DeviceId::from_bytes(sender_bytes),
})
}
#[must_use]
pub fn len(&self) -> usize {
self.records.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn seal(&self, master: &MasterKey) -> Result<Vec<u8>> {
if self.records.len() > MAX_RECORDS {
return Err(Error::SyncTooManyRecords {
count: self.records.len(),
limit: MAX_RECORDS,
});
}
let mut plaintext = Vec::new();
#[expect(
clippy::cast_possible_truncation,
reason = "already compared against MAX_RECORDS above, which is far below u32::MAX"
)]
let count = self.records.len() as u32;
plaintext.extend_from_slice(&count.to_be_bytes());
for record in &self.records {
plaintext.extend_from_slice(record.entry_id.as_bytes());
plaintext.extend_from_slice(&record.nonce);
let len = u32::try_from(record.ciphertext.len()).map_err(|_| Error::SyncMalformed {
reason: "a single record ciphertext exceeds 4 GiB",
})?;
plaintext.extend_from_slice(&len.to_be_bytes());
plaintext.extend_from_slice(&record.ciphertext);
}
if plaintext.len() > MAX_PLAINTEXT_LEN {
return Err(Error::SyncMalformed {
reason: "the payload plaintext exceeds the limit",
});
}
let compressed = compress::compress(&plaintext)?;
let key = derive_envelope_key(master)?;
let (nonce, ciphertext) = crypto::seal_bytes(&key, &self.aad(), &compressed)?;
let mut out = Vec::with_capacity(HEADER_LEN + NONCE_LEN + ciphertext.len());
out.extend_from_slice(MAGIC);
out.push(FORMAT_VERSION);
out.push(self.kind.as_byte());
out.extend_from_slice(&self.sync_id);
out.extend_from_slice(self.sender.as_bytes());
out.extend_from_slice(&nonce);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn open(master: &MasterKey, data: &[u8]) -> Result<Self> {
let header = Self::peek(data)?;
let PayloadHeader {
version,
kind,
sync_id,
sender,
} = header;
let mut reader = Reader::new(data);
let _ = reader.take(HEADER_LEN).ok_or(Error::SyncTruncated)?;
let nonce: [u8; NONCE_LEN] = reader
.take(NONCE_LEN)
.ok_or(Error::SyncTruncated)?
.try_into()
.map_err(|_| Error::SyncTruncated)?;
let ciphertext = reader.rest();
if ciphertext.len() < TAG_LEN {
return Err(Error::SyncTruncated);
}
let aad = header_aad(version, kind, &sync_id, &sender);
let key = derive_envelope_key(master)?;
let compressed = crypto::open_bytes(&key, &aad, &nonce, ciphertext)
.map_err(|_| Error::SyncAuthenticationFailed)?;
let plaintext = compress::decompress(&compressed)?;
let mut reader = Reader::new(&plaintext);
let declared = reader.u32_bare().ok_or(Error::SyncTruncated)? as usize;
if declared > MAX_RECORDS {
return Err(Error::SyncTooManyRecords {
count: declared,
limit: MAX_RECORDS,
});
}
let mut records = RecordSet::<Record>::new();
for _ in 0..declared {
let id_bytes: [u8; EntryId::LEN] = reader
.take(EntryId::LEN)
.ok_or(Error::SyncTruncated)?
.try_into()
.map_err(|_| Error::SyncTruncated)?;
let nonce: [u8; NONCE_LEN] = reader
.take(NONCE_LEN)
.ok_or(Error::SyncTruncated)?
.try_into()
.map_err(|_| Error::SyncTruncated)?;
let len = reader.u32_bare().ok_or(Error::SyncTruncated)? as usize;
if len < TAG_LEN {
return Err(Error::SyncMalformed {
reason: "the record ciphertext is shorter than the authentication tag",
});
}
let ciphertext = reader.take(len).ok_or(Error::SyncTruncated)?.to_vec();
if !records.insert(Record {
entry_id: EntryId::from_bytes(id_bytes),
nonce,
ciphertext,
}) {
return Err(Error::SyncMalformed {
reason: "the payload holds two records for the same entry",
});
}
}
if !reader.is_empty() {
return Err(Error::SyncMalformed {
reason: "the payload plaintext holds content beyond the declared record count",
});
}
Ok(Self {
sync_id,
sender,
records,
kind,
})
}
pub fn open_records(&self, master: &MasterKey) -> Result<Vec<OpenedRecord>> {
let key = derive_record_key(master)?;
let mut opened = Vec::with_capacity(self.records.len());
for record in &self.records {
opened.push(record::open(&key, record)?);
}
Ok(opened)
}
fn aad(&self) -> [u8; AAD_LEN] {
header_aad(FORMAT_VERSION, self.kind, &self.sync_id, &self.sender)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PayloadHeader {
pub version: u8,
pub kind: PayloadKind,
pub sync_id: SyncId,
pub sender: DeviceId,
}
fn check_version(version: u8) -> Result<()> {
if version != FORMAT_VERSION {
return Err(Error::SyncUnsupportedVersion { version });
}
Ok(())
}
struct Reader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
const fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn take(&mut self, len: usize) -> Option<&'a [u8]> {
let end = self.pos.checked_add(len)?;
let slice = self.data.get(self.pos..end)?;
self.pos = end;
Some(slice)
}
fn u8_bare(&mut self) -> Option<u8> {
self.take(1).map(|s| s[0])
}
fn u32_bare(&mut self) -> Option<u32> {
self.take(4)
.and_then(|s| <[u8; 4]>::try_from(s).ok())
.map(u32::from_be_bytes)
}
fn rest(&mut self) -> &'a [u8] {
let rest = self.data.get(self.pos..).unwrap_or(&[]);
self.pos = self.data.len();
rest
}
fn is_empty(&self) -> bool {
self.pos >= self.data.len()
}
}
const AAD_LEN: usize = HEADER_LEN;
fn header_aad(
version: u8,
kind: PayloadKind,
sync_id: &SyncId,
sender: &DeviceId,
) -> [u8; AAD_LEN] {
debug_assert_eq!(version, FORMAT_VERSION);
let mut aad = [0_u8; AAD_LEN];
let mut offset = 0;
aad[..MAGIC.len()].copy_from_slice(MAGIC);
offset += MAGIC.len();
aad[offset] = version;
offset += 1;
aad[offset] = kind.as_byte();
offset += 1;
aad[offset..offset + SYNC_ID_LEN].copy_from_slice(sync_id);
offset += SYNC_ID_LEN;
aad[offset..].copy_from_slice(sender.as_bytes());
aad
}
pub(crate) use crate::vault::RecordKind as InnerRecordKind;
const _: () = {
assert!(InnerRecordKind::SyncEntry.as_byte() != InnerRecordKind::Entry.as_byte());
assert!(InnerRecordKind::SyncTombstone.as_byte() != InnerRecordKind::Tombstone.as_byte());
assert!(InnerRecordKind::SyncGroup.as_byte() != InnerRecordKind::Group.as_byte());
};
#[cfg(test)]
mod tests {
use super::*;
use crate::otp::{Algorithm, Digits, OtpSecret};
use crate::vault::{
Entry as VaultEntry, EntryKind, RecordKind, Timestamp, Vault, master_key_to_bytes,
};
const _: () = assert!(LEGACY_FORMAT_VERSION_1 != FORMAT_VERSION);
fn sample_entry(seed: u8) -> VaultEntry {
VaultEntry::new(
format!("entry {seed}"),
Some("组织".into()),
format!("user{seed}@example.com"),
OtpSecret::from_base32("JBSWY3DPEHPK3PXP").expect("valid Base32"),
Algorithm::Sha1,
Digits::SIX,
EntryKind::Totp { period: 30 },
String::new(),
Timestamp::from(1_700_000_000_u64),
Timestamp::from(1_700_000_000_u64),
)
.expect("valid fields")
}
fn device(seed: u8) -> DeviceId {
DeviceId::from_bytes([seed; DEVICE_ID_LEN])
}
fn a_master_key() -> MasterKey {
MasterKey::generate().expect("OS randomness available")
}
fn opened_kinds(payload: &Payload, master: &MasterKey) -> Vec<(EntryId, bool)> {
payload
.open_records(master)
.expect("opening record by record")
.into_iter()
.filter_map(|record| match record {
OpenedRecord::Entry { id, .. } => Some((id, true)),
OpenedRecord::Tombstone(tombstone) => Some((tombstone.entry_id, false)),
OpenedRecord::Group { .. } => None,
})
.collect()
}
fn plaintext_of(sealed: &[u8], master: &MasterKey) -> Vec<u8> {
let mut reader = Reader::new(sealed);
assert_eq!(reader.take(MAGIC.len()), Some(&MAGIC[..]), "magic");
let version = reader.u8_bare().expect("version");
assert_eq!(version, FORMAT_VERSION, "version");
let kind = PayloadKind::from_byte(reader.u8_bare().expect("kind")).expect("valid kind");
let sync_id = reader.take(SYNC_ID_LEN).expect("sync_id");
let sender = reader.take(DEVICE_ID_LEN).expect("sender");
let nonce = reader.take(NONCE_LEN).expect("nonce");
let ciphertext = reader.rest();
assert!(reader.is_empty(), "extra bytes follow the ciphertext");
let sync_id = <[u8; SYNC_ID_LEN]>::try_from(sync_id).expect("correct length");
let sender = DeviceId::from_bytes(<[u8; DEVICE_ID_LEN]>::try_from(sender).expect("length"));
let nonce = <[u8; NONCE_LEN]>::try_from(nonce).expect("correct length");
let aad = header_aad(version, kind, &sync_id, &sender);
let envelope_key = derive_envelope_key(master).expect("derivation succeeds");
let compressed = crate::vault::crypto::open_bytes(&envelope_key, &aad, &nonce, ciphertext)
.expect("the outer layer must open");
compress::decompress(&compressed).expect("must decompress")
}
fn parse_record_frames(plaintext: &[u8]) -> Vec<([u8; EntryId::LEN], [u8; NONCE_LEN], usize)> {
let mut reader = Reader::new(plaintext);
let count = reader.u32_bare().expect("record count") as usize;
let mut frames = Vec::with_capacity(count);
for _ in 0..count {
let id = reader.take(EntryId::LEN).expect("identifier");
let nonce = reader.take(NONCE_LEN).expect("nonce");
let len = reader.u32_bare().expect("length") as usize;
reader.take(len).expect("ciphertext");
frames.push((
<[u8; EntryId::LEN]>::try_from(id).expect("length"),
<[u8; NONCE_LEN]>::try_from(nonce).expect("length"),
len,
));
}
assert!(reader.is_empty(), "extra bytes follow the record section");
frames
}
#[test]
fn the_plaintext_record_framing_matches_encoded_len() {
let master = a_master_key();
let sender = device(7);
let mut vault = Vault::new();
for seed in 0..3_u8 {
vault
.insert(sample_entry(seed))
.expect("randomness available");
}
let doomed = vault.insert(sample_entry(9)).expect("randomness available");
vault.remove(&doomed, Timestamp::from(1_700_000_100_u64));
let payload = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
let sealed = payload.seal(&master).expect("sealing succeeds");
let plaintext = plaintext_of(&sealed, &master);
let frames = parse_record_frames(&plaintext);
assert_eq!(frames.len(), payload.len(), "record count mismatch");
assert_eq!(
frames.len(),
vault.len() + vault.tombstone_count() + vault.group_count(),
"the record count in the payload must equal entries plus tombstones plus groups"
);
for (id_bytes, nonce, len) in &frames {
let id = EntryId::from_bytes(*id_bytes);
let record = payload
.records
.get(&id)
.expect("the payload must hold this record");
assert_eq!(record.nonce, *nonce, "nonce mismatch");
assert_eq!(record.ciphertext.len(), *len, "ciphertext length mismatch");
assert_eq!(
record.encoded_len(),
EntryId::LEN + NONCE_LEN + 4 + len,
"encoded_len does not match the actual byte count of the wire format"
);
}
let expected: usize = 4
+ frames.len() * (EntryId::LEN + NONCE_LEN + 4)
+ frames.iter().map(|f| f.2).sum::<usize>();
assert_eq!(
plaintext.len(),
expected,
"the plaintext total length does not match the wire format"
);
}
#[test]
fn the_record_kind_is_not_in_the_plaintext() {
let master = a_master_key();
let sender = device(7);
let mut vault = Vault::new();
let id = vault.insert(sample_entry(1)).expect("randomness available");
vault.remove(&id, Timestamp::from(1_700_000_100_u64));
let payload = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
let sealed = payload.seal(&master).expect("sealing succeeds");
let plaintext = plaintext_of(&sealed, &master);
let frames = parse_record_frames(&plaintext);
let expected = 4 + frames
.iter()
.map(|(_, _, len)| EntryId::LEN + NONCE_LEN + 4 + len)
.sum::<usize>();
assert_eq!(
plaintext.len(),
expected,
"the plaintext must be the header plus the frames, with no kind byte anywhere"
);
assert_eq!(
&plaintext[4..4 + EntryId::LEN],
frames[0].0.as_slice(),
"the first identifier must follow the record count immediately, with no kind byte"
);
assert_ne!(
RecordKind::SyncTombstone.as_byte(),
plaintext[3],
"byte 4 is part of the identifier"
);
}
#[test]
fn the_domain_separation_constants_are_stable() {
assert_eq!(&HKDF_SALT_SYNC, b"Qualia2FA sync salt v1 (records)");
assert_eq!(HKDF_SALT_SYNC.len(), 32);
assert_eq!(MAGIC, b"Q2FASYNC");
assert_eq!(FORMAT_VERSION, 3);
assert!(
crate::vault::READABLE_VERSIONS.contains(&crate::vault::FORMAT_VERSION),
"the version a file format declares must be in its own readable version list"
);
}
#[test]
fn a_version_1_payload_is_rejected() {
let master = a_master_key();
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("randomness available");
let payload = Payload::from_vault(&master, device(1), &vault).expect("packing succeeds");
let sealed = payload.seal(&master).expect("sealing succeeds");
let mut legacy = sealed.clone();
legacy[MAGIC.len()] = LEGACY_FORMAT_VERSION_1;
for result in [
Payload::peek(&legacy).map(|_| ()),
Payload::open(&master, &legacy).map(|_| ()),
] {
match result {
Err(Error::SyncUnsupportedVersion { version }) => {
assert_eq!(version, LEGACY_FORMAT_VERSION_1);
}
other => panic!("version 1 was not rejected as a version, actual {other:?}"),
}
}
}
#[test]
fn a_request_payload_carries_no_records_and_keeps_its_kind() {
let master = a_master_key();
let sender = device(3);
let payload = Payload::request(sender).expect("building a request");
assert_eq!(payload.kind(), PayloadKind::Request);
assert!(payload.is_empty(), "a request must not carry records");
assert_eq!(payload.sender, sender);
let sealed = payload.seal(&master).expect("sealing succeeds");
let header = Payload::peek(&sealed).expect("peek");
assert_eq!(header.kind, PayloadKind::Request);
assert_eq!(header.sender, sender);
assert_eq!(header.sync_id, payload.sync_id);
assert_eq!(header.version, FORMAT_VERSION);
let opened = Payload::open(&master, &sealed).expect("opening the payload");
assert_eq!(opened.kind(), PayloadKind::Request);
assert!(opened.is_empty());
}
#[test]
fn flipping_the_payload_kind_breaks_authentication() {
let master = a_master_key();
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("randomness available");
let payload = Payload::from_vault(&master, device(1), &vault).expect("packing succeeds");
let sealed = payload.seal(&master).expect("sealing succeeds");
let kind_offset = MAGIC.len() + 1;
assert_eq!(sealed[kind_offset], PayloadKind::Vault.as_byte());
let mut flipped = sealed.clone();
flipped[kind_offset] = PayloadKind::Request.as_byte();
let peeked = Payload::peek(&flipped).expect("peek only reads plaintext and must succeed");
assert_eq!(peeked.kind, PayloadKind::Request);
let result = Payload::open(&master, &flipped);
assert!(
matches!(result, Err(Error::SyncAuthenticationFailed)),
"the payload still opened after the kind was tampered with, actual {result:?}"
);
assert!(Payload::open(&master, &sealed).is_ok());
}
#[test]
fn an_unknown_payload_kind_is_rejected() {
let master = a_master_key();
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("randomness available");
let payload = Payload::from_vault(&master, device(1), &vault).expect("packing succeeds");
let sealed = payload.seal(&master).expect("sealing succeeds");
for bogus in [0x00_u8, 0x03, 0x80, 0xFF] {
let mut bad = sealed.clone();
bad[MAGIC.len() + 1] = bogus;
let result = Payload::peek(&bad);
match result {
Err(Error::SyncUnknownPayloadKind { value }) => assert_eq!(value, bogus),
other => panic!("unknown kind {bogus:#04x} was not rejected, actual {other:?}"),
}
}
}
#[test]
fn peek_never_panics_on_arbitrary_bytes() {
let cases: [&[u8]; 6] = [
b"",
b"Q",
b"Q2FASYNC",
b"Q2FASYNC\x02",
b"Q2FASYNC\x02\x01",
b"Q2FASYNC\x09\xff",
];
for case in cases {
let _ = Payload::peek(case);
}
let master = a_master_key();
let payload = Payload::request(device(1)).expect("request");
let sealed = payload.seal(&master).expect("sealing succeeds");
assert!(Payload::peek(&sealed).is_ok());
}
#[test]
fn the_payload_header_is_laid_out_byte_for_byte() {
let master = a_master_key();
let sender = device(0xAB);
let payload = Payload::request(sender).expect("request");
let sealed = payload.seal(&master).expect("sealing succeeds");
assert_eq!(&sealed[..8], b"Q2FASYNC", "magic");
assert_eq!(sealed[8], FORMAT_VERSION, "version");
assert_eq!(sealed[9], PayloadKind::Request.as_byte(), "kind");
assert_eq!(&sealed[10..42], &payload.sync_id, "sync_id");
assert_eq!(&sealed[42..74], sender.as_bytes(), "sender");
let plaintext = plaintext_of(&sealed, &master);
assert_eq!(
plaintext,
0_u32.to_be_bytes(),
"the plaintext of an empty request must be just a zero record count"
);
assert!(
sealed.len() >= HEADER_LEN + NONCE_LEN + TAG_LEN,
"not even long enough for header + nonce + authentication tag: {} < {}",
sealed.len(),
HEADER_LEN + NONCE_LEN + TAG_LEN
);
assert_eq!(
HEADER_LEN, 74,
"header byte count: 8 magic + 1 version + 1 kind + 32 sync_id + 32 sender"
);
}
#[test]
fn the_sync_and_vault_keys_are_different() {
let master = a_master_key();
let record = derive_record_key(&master).expect("derivation succeeds");
let envelope = derive_envelope_key(&master).expect("derivation succeeds");
let vault_key = crate::vault::crypto::entry_key(&master).expect("derivation succeeds");
assert_ne!(
record, envelope,
"the record key collided with the envelope key"
);
assert_ne!(
record, vault_key,
"the sync record key collided with the vault entry key"
);
assert_ne!(
envelope, vault_key,
"the envelope key collided with the vault entry key"
);
}
#[test]
fn an_empty_vault_roundtrips() {
let master = a_master_key();
let sender = device(1);
let payload =
Payload::from_vault(&master, sender, &Vault::new()).expect("packing succeeds");
assert!(payload.is_empty(), "an empty vault must produce no records");
assert_eq!(payload.len(), 0);
let sealed = payload.seal(&master).expect("sealing succeeds");
let opened = Payload::open(&master, &sealed).expect("opening the payload");
assert!(opened.is_empty());
assert_eq!(opened.sender, sender);
assert_eq!(opened.sync_id, payload.sync_id);
assert!(
opened
.open_records(&master)
.expect("opening record by record")
.is_empty(),
"an empty payload must not open any record"
);
}
#[test]
fn every_payload_gets_a_fresh_sync_id() {
let master = a_master_key();
let sender = device(1);
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("randomness available");
let first = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
let second = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
assert_ne!(
first.sync_id, second.sync_id,
"two packings produced the same payload identifier"
);
assert_eq!(
first.len(),
second.len(),
"two packings must yield the same record count"
);
}
#[test]
fn sealing_the_same_vault_twice_produces_different_bytes() {
let master = a_master_key();
let sender = device(1);
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("randomness available");
let first = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
let second = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
let a = first.seal(&master).expect("sealing succeeds");
let b = second.seal(&master).expect("sealing succeeds");
assert_ne!(
a, b,
"two sealings produced identical bytes, the nonce was most likely reused"
);
let ra = plaintext_of(&a, &master);
let rb = plaintext_of(&b, &master);
let fa = parse_record_frames(&ra);
let fb = parse_record_frames(&rb);
assert_eq!(fa.len(), fb.len());
for (x, y) in fa.iter().zip(fb.iter()) {
assert_eq!(x.0, y.0, "same entry");
assert_ne!(x.1, y.1, "the same record reused its nonce");
}
}
#[test]
fn an_entry_with_a_tombstone_is_not_sent() {
let master = a_master_key();
let sender = device(1);
let mut vault = Vault::new();
let kept = vault.insert(sample_entry(1)).expect("randomness available");
let disputed = vault.insert(sample_entry(2)).expect("randomness available");
vault.record_tombstone(crate::vault::Tombstone {
entry_id: disputed,
deleted_at: Timestamp::from(1_700_000_100_u64),
deleted_by: None,
});
assert_eq!(
vault.len(),
2,
"precondition failed: both live entries must still be there"
);
assert_eq!(
vault.tombstone_count(),
1,
"precondition failed: a tombstone must exist"
);
assert!(
vault.get(&disputed).is_some(),
"precondition failed: `record_tombstone` must not delete a live entry"
);
let payload = Payload::from_vault(&master, sender, &vault).expect("packing succeeds");
assert_eq!(
payload.len(),
3,
"there must be exactly three records: the kept entry, the tombstone, and the top group"
);
assert!(
payload.records.get(&kept).is_some(),
"the kept entry must be sent"
);
let records = opened_kinds(&payload, &master);
let mut entries = Vec::new();
let mut tombstones = Vec::new();
for (id, is_entry) in records {
if is_entry {
entries.push(id);
} else {
tombstones.push(id);
}
}
assert_eq!(
entries,
vec![kept],
"exactly one live entry must be sent, and it must be the kept one"
);
assert_eq!(
tombstones,
vec![disputed],
"exactly one tombstone must be sent"
);
assert!(
!entries.contains(&disputed),
"a live entry with a tombstone was sent as an entry"
);
}
#[test]
fn an_absurd_record_count_is_rejected_before_allocating() {
let master = a_master_key();
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("randomness available");
let payload = Payload::from_vault(&master, device(1), &vault).expect("packing succeeds");
let mut plaintext = u32::MAX.to_be_bytes().to_vec();
plaintext.extend_from_slice(&[0_u8; 64]);
let compressed = compress::compress(&plaintext).expect("compression succeeds");
let envelope_key = derive_envelope_key(&master).expect("derivation succeeds");
let aad = header_aad(
FORMAT_VERSION,
PayloadKind::Vault,
&payload.sync_id,
&payload.sender,
);
let (nonce, ciphertext) =
crate::vault::crypto::seal_bytes(&envelope_key, &aad, &compressed)
.expect("sealing succeeds");
let mut sealed = Vec::new();
sealed.extend_from_slice(MAGIC);
sealed.push(FORMAT_VERSION);
sealed.push(PayloadKind::Vault.as_byte());
sealed.extend_from_slice(&payload.sync_id);
sealed.extend_from_slice(payload.sender.as_bytes());
sealed.extend_from_slice(&nonce);
sealed.extend_from_slice(&ciphertext);
let result = Payload::open(&master, &sealed);
assert!(
matches!(result, Err(Error::SyncTooManyRecords { .. })),
"an over-limit record count was not rejected, actual {result:?}"
);
}
#[test]
fn the_master_key_roundtrips_through_bytes() {
let master = a_master_key();
let copy = MasterKey::from_bytes(master_key_to_bytes(&master)).expect("correct length");
assert_eq!(
derive_record_key(&master).expect("derivation succeeds"),
derive_record_key(©).expect("derivation succeeds"),
"a copy of the same master key must derive the same record key"
);
}
}