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 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 = 2;
#[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)?);
}
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 = "已在上面与 MAX_RECORDS 比较过,它远小于 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: "单条记录的密文超过 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: "载荷明文超过上限",
});
}
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: "记录密文短于认证标签",
});
}
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: "载荷里同一个条目出现了两条记录",
});
}
}
if !reader.is_empty() {
return Err(Error::SyncMalformed {
reason: "载荷明文含超出声明记录数的内容",
});
}
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());
};
#[cfg(test)]
mod tests {
use super::*;
use crate::otp::{Algorithm, Digits, OtpSecret};
use crate::vault::{Entry as VaultEntry, RecordKind, Timestamp, Vault, master_key_to_bytes};
const _: () = assert!(LEGACY_FORMAT_VERSION_1 != FORMAT_VERSION);
fn sample_entry(seed: u8) -> VaultEntry {
VaultEntry::new(
format!("服务 {seed}"),
Some("组织".into()),
format!("user{seed}@example.com"),
OtpSecret::from_base32("JBSWY3DPEHPK3PXP").expect("合法 Base32"),
Algorithm::Sha1,
Digits::Six,
30,
None,
String::new(),
Timestamp::from(1_700_000_000_u64),
Timestamp::from(1_700_000_000_u64),
)
.expect("字段合法")
}
fn device(seed: u8) -> DeviceId {
DeviceId::from_bytes([seed; DEVICE_ID_LEN])
}
fn a_master_key() -> MasterKey {
MasterKey::generate().expect("OS 随机源可用")
}
fn opened_kinds(payload: &Payload, master: &MasterKey) -> Vec<(EntryId, bool)> {
payload
.open_records(master)
.expect("逐条解密")
.into_iter()
.map(|record| match record {
OpenedRecord::Entry { id, .. } => (id, true),
OpenedRecord::Tombstone(tombstone) => (tombstone.entry_id, false),
})
.collect()
}
fn plaintext_of(sealed: &[u8], master: &MasterKey) -> Vec<u8> {
let mut reader = Reader::new(sealed);
assert_eq!(reader.take(MAGIC.len()), Some(&MAGIC[..]), "魔数");
let version = reader.u8_bare().expect("版本");
assert_eq!(version, FORMAT_VERSION, "版本");
let kind = PayloadKind::from_byte(reader.u8_bare().expect("种类")).expect("种类合法");
let sync_id = reader.take(SYNC_ID_LEN).expect("sync_id");
let sender = reader.take(DEVICE_ID_LEN).expect("发送方");
let nonce = reader.take(NONCE_LEN).expect("nonce");
let ciphertext = reader.rest();
assert!(reader.is_empty(), "密文之后还有多余字节");
let sync_id = <[u8; SYNC_ID_LEN]>::try_from(sync_id).expect("长度正确");
let sender = DeviceId::from_bytes(<[u8; DEVICE_ID_LEN]>::try_from(sender).expect("长度"));
let nonce = <[u8; NONCE_LEN]>::try_from(nonce).expect("长度正确");
let aad = header_aad(version, kind, &sync_id, &sender);
let envelope_key = derive_envelope_key(master).expect("可派生");
let compressed = crate::vault::crypto::open_bytes(&envelope_key, &aad, &nonce, ciphertext)
.expect("外层应当解得开");
compress::decompress(&compressed).expect("应当解得开")
}
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("记录数") as usize;
let mut frames = Vec::with_capacity(count);
for _ in 0..count {
let id = reader.take(EntryId::LEN).expect("标识");
let nonce = reader.take(NONCE_LEN).expect("nonce");
let len = reader.u32_bare().expect("长度") as usize;
reader.take(len).expect("密文");
frames.push((
<[u8; EntryId::LEN]>::try_from(id).expect("长度"),
<[u8; NONCE_LEN]>::try_from(nonce).expect("长度"),
len,
));
}
assert!(reader.is_empty(), "记录段之后还有多余字节");
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("随机源可用");
}
let doomed = vault.insert(sample_entry(9)).expect("随机源可用");
vault.remove(&doomed, Timestamp::from(1_700_000_100_u64));
let payload = Payload::from_vault(&master, sender, &vault).expect("打包");
let sealed = payload.seal(&master).expect("封装");
let plaintext = plaintext_of(&sealed, &master);
let frames = parse_record_frames(&plaintext);
assert_eq!(frames.len(), payload.len(), "记录数不一致");
assert_eq!(
frames.len(),
vault.len() + vault.tombstone_count(),
"载荷里的记录数应当等于条目数加墓碑数"
);
for (id_bytes, nonce, len) in &frames {
let id = EntryId::from_bytes(*id_bytes);
let record = payload.records.get(&id).expect("载荷里应当有这条记录");
assert_eq!(record.nonce, *nonce, "nonce 对不上");
assert_eq!(record.ciphertext.len(), *len, "密文长度对不上");
assert_eq!(
record.encoded_len(),
EntryId::LEN + NONCE_LEN + 4 + len,
"encoded_len 与线格式的实际字节数不一致"
);
}
let expected: usize = 4
+ frames.len() * (EntryId::LEN + NONCE_LEN + 4)
+ frames.iter().map(|f| f.2).sum::<usize>();
assert_eq!(plaintext.len(), expected, "明文总长度与线格式不一致");
}
#[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("随机源可用");
vault.remove(&id, Timestamp::from(1_700_000_100_u64));
let payload = Payload::from_vault(&master, sender, &vault).expect("打包");
let sealed = payload.seal(&master).expect("封装");
let plaintext = plaintext_of(&sealed, &master);
assert_eq!(
plaintext.len(),
4 + EntryId::LEN + NONCE_LEN + 4 + {
let frames = parse_record_frames(&plaintext);
frames[0].2
}
);
assert_eq!(
&plaintext[4..4 + EntryId::LEN],
id.as_bytes(),
"记录数之后紧接着的应当是标识,没有种类字节"
);
assert_ne!(
RecordKind::SyncTombstone.as_byte(),
plaintext[3],
"第 4 字节是标识的一部分"
);
}
#[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, 2);
assert!(
crate::vault::READABLE_VERSIONS.contains(&crate::vault::FORMAT_VERSION),
"文件格式自己声明的版本必须在自己可读的版本列表里"
);
}
#[test]
fn a_version_1_payload_is_rejected() {
let master = a_master_key();
let mut vault = Vault::new();
vault.insert(sample_entry(1)).expect("随机源可用");
let payload = Payload::from_vault(&master, device(1), &vault).expect("打包");
let sealed = payload.seal(&master).expect("封装");
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!("版本 1 未被按版本拒绝,实际 {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("造请求");
assert_eq!(payload.kind(), PayloadKind::Request);
assert!(payload.is_empty(), "请求不该带记录");
assert_eq!(payload.sender, sender);
let sealed = payload.seal(&master).expect("封装");
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("拆包");
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("随机源可用");
let payload = Payload::from_vault(&master, device(1), &vault).expect("打包");
let sealed = payload.seal(&master).expect("封装");
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 只看明文,应当成功");
assert_eq!(peeked.kind, PayloadKind::Request);
let result = Payload::open(&master, &flipped);
assert!(
matches!(result, Err(Error::SyncAuthenticationFailed)),
"篡改种类之后仍能打开,实际 {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("随机源可用");
let payload = Payload::from_vault(&master, device(1), &vault).expect("打包");
let sealed = payload.seal(&master).expect("封装");
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!("未知种类 {bogus:#04x} 未被拒绝,实际 {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("请求");
let sealed = payload.seal(&master).expect("封装");
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("请求");
let sealed = payload.seal(&master).expect("封装");
assert_eq!(&sealed[..8], b"Q2FASYNC", "魔数");
assert_eq!(sealed[8], FORMAT_VERSION, "版本");
assert_eq!(sealed[9], PayloadKind::Request.as_byte(), "种类");
assert_eq!(&sealed[10..42], &payload.sync_id, "sync_id");
assert_eq!(&sealed[42..74], sender.as_bytes(), "发送方");
let plaintext = plaintext_of(&sealed, &master);
assert_eq!(
plaintext,
0_u32.to_be_bytes(),
"空请求的明文应当就是一个「零条记录」的计数"
);
assert!(
sealed.len() >= HEADER_LEN + NONCE_LEN + TAG_LEN,
"连「头 + nonce + 认证标签」都不够长:{} < {}",
sealed.len(),
HEADER_LEN + NONCE_LEN + TAG_LEN
);
assert_eq!(
HEADER_LEN, 74,
"头的字节数:8 魔数 + 1 版本 + 1 种类 + 32 sync_id + 32 发送方"
);
}
#[test]
fn the_sync_and_vault_keys_are_different() {
let master = a_master_key();
let record = derive_record_key(&master).expect("可派生");
let envelope = derive_envelope_key(&master).expect("可派生");
let vault_key = crate::vault::crypto::entry_key(&master).expect("可派生");
assert_ne!(record, envelope, "记录密钥与信封密钥撞上了");
assert_ne!(record, vault_key, "同步记录密钥与保险库条目密钥撞上了");
assert_ne!(envelope, vault_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("打包");
assert!(payload.is_empty(), "空保险库不该产生记录");
assert_eq!(payload.len(), 0);
let sealed = payload.seal(&master).expect("封装");
let opened = Payload::open(&master, &sealed).expect("拆包");
assert!(opened.is_empty());
assert_eq!(opened.sender, sender);
assert_eq!(opened.sync_id, payload.sync_id);
assert!(
opened.open_records(&master).expect("逐条解密").is_empty(),
"空载荷不该解开出任何记录"
);
}
#[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("随机源可用");
let first = Payload::from_vault(&master, sender, &vault).expect("打包");
let second = Payload::from_vault(&master, sender, &vault).expect("打包");
assert_ne!(
first.sync_id, second.sync_id,
"两次打包拿到了同一个载荷标识"
);
assert_eq!(first.len(), second.len(), "两次打包的记录数应当相同");
}
#[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("随机源可用");
let first = Payload::from_vault(&master, sender, &vault).expect("打包");
let second = Payload::from_vault(&master, sender, &vault).expect("打包");
let a = first.seal(&master).expect("封装");
let b = second.seal(&master).expect("封装");
assert_ne!(a, b, "两次封装的字节完全相同,nonce 很可能被复用了");
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, "同一条目");
assert_ne!(x.1, y.1, "同一条记录的 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("随机源可用");
let disputed = vault.insert(sample_entry(2)).expect("随机源可用");
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, "前提不成立:两条活条目都应当还在");
assert_eq!(vault.tombstone_count(), 1, "前提不成立:应当有墓碑");
assert!(
vault.get(&disputed).is_some(),
"前提不成立:`record_tombstone` 不该删掉活条目"
);
let payload = Payload::from_vault(&master, sender, &vault).expect("打包");
assert_eq!(payload.len(), 2, "应当只有「保留的条目 + 墓碑」两条记录");
assert!(payload.records.get(&kept).is_some(), "保留的条目应当被发出");
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],
"应当只发出一条活条目,而且是保留的那条"
);
assert_eq!(tombstones, vec![disputed], "应当只发出一条墓碑");
assert!(
!entries.contains(&disputed),
"有墓碑的活条目被当成条目发出了"
);
}
#[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("随机源可用");
let payload = Payload::from_vault(&master, device(1), &vault).expect("打包");
let mut plaintext = u32::MAX.to_be_bytes().to_vec();
plaintext.extend_from_slice(&[0_u8; 64]);
let compressed = compress::compress(&plaintext).expect("可压缩");
let envelope_key = derive_envelope_key(&master).expect("可派生");
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("可加密");
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 { .. })),
"超限的记录数未被拒绝,实际 {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("长度正确");
assert_eq!(
derive_record_key(&master).expect("可派生"),
derive_record_key(©).expect("可派生"),
"同一把主密钥的副本应当派生出同一把记录密钥"
);
}
}