use super::entry::EntryId;
use crate::{Error, ExposeSecret, Result, SecretBytes};
use chacha20poly1305::aead::{AeadInOut, KeyInit};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
use hkdf::Hkdf;
use sha2::Sha256;
use subtle::ConstantTimeEq as _;
pub const MASTER_KEY_LEN: usize = 32;
pub struct MasterKey(SecretBytes);
impl MasterKey {
pub fn generate() -> Result<Self> {
let mut bytes = [0_u8; MASTER_KEY_LEN];
crate::getrandom::fill(&mut bytes).map_err(|_| Error::RandomnessUnavailable)?;
Ok(Self(Vec::from(bytes).into()))
}
pub fn from_bytes(bytes: SecretBytes) -> Result<Self> {
if bytes.expose_secret().len() != MASTER_KEY_LEN {
return Err(Error::MasterKeyWrongLength {
expected: MASTER_KEY_LEN,
actual: bytes.expose_secret().len(),
});
}
Ok(Self(bytes))
}
#[must_use]
pub fn expose(&self) -> &[u8] {
self.0.expose_secret()
}
}
impl core::fmt::Debug for MasterKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("MasterKey([REDACTED])")
}
}
#[must_use]
pub fn master_key_to_bytes(key: &MasterKey) -> SecretBytes {
key.0.clone()
}
pub const NONCE_LEN: usize = 24;
pub const TAG_LEN: usize = 16;
pub type Nonce = [u8; NONCE_LEN];
pub(crate) fn random_nonce() -> Result<Nonce> {
let mut nonce = [0_u8; NONCE_LEN];
crate::getrandom::fill(&mut nonce).map_err(|_| Error::RandomnessUnavailable)?;
Ok(nonce)
}
const HKDF_INFO_ENTRY: &[u8] = b"Qualia2FA vault v1 entry encryption";
const HKDF_SALT: [u8; 32] = [0_u8; 32];
pub(crate) fn entry_key(master: &MasterKey) -> Result<[u8; 32]> {
derive_key(master, HKDF_INFO_ENTRY)
}
pub(crate) fn derive_key(master: &MasterKey, info: &[u8]) -> Result<[u8; 32]> {
derive_key_with_salt(master, &HKDF_SALT, info)
}
pub(crate) fn derive_key_with_salt(
master: &MasterKey,
salt: &[u8],
info: &[u8],
) -> Result<[u8; 32]> {
let hkdf = Hkdf::<Sha256>::new(Some(salt), master.expose());
let mut key = [0_u8; 32];
hkdf.expand(info, &mut key)
.map_err(|_| Error::VaultDecryptionFailed)?;
Ok(key)
}
pub(crate) const HKDF_INFO_SYNC_RECORD: &[u8] = HKDF_INFO_ENTRY;
pub(crate) const HKDF_INFO_SYNC_ENVELOPE: &[u8] = b"Qualia2FA sync v1 envelope";
pub(crate) fn seal_bytes(key: &[u8; 32], aad: &[u8], plaintext: &[u8]) -> Result<(Nonce, Vec<u8>)> {
let cipher = XChaCha20Poly1305::new(&Key::from(*key));
let nonce = random_nonce()?;
let mut buffer = plaintext.to_vec();
cipher
.encrypt_in_place(&XNonce::from(nonce), aad, &mut buffer)
.map_err(|_| Error::VaultDecryptionFailed)?;
Ok((nonce, buffer))
}
pub(crate) fn open_bytes(
key: &[u8; 32],
aad: &[u8],
nonce: &Nonce,
ciphertext: &[u8],
) -> Result<Vec<u8>> {
let cipher = XChaCha20Poly1305::new(&Key::from(*key));
let mut buffer = ciphertext.to_vec();
cipher
.decrypt_in_place(&XNonce::from(*nonce), aad, &mut buffer)
.map_err(|_| Error::VaultDecryptionFailed)?;
Ok(buffer)
}
pub const KEY_CHECK_LEN: usize = 32;
const HKDF_INFO_KEY_CHECK: &[u8] = b"Qualia2FA vault v1 master key check";
#[must_use]
pub fn key_check(master: &MasterKey) -> [u8; KEY_CHECK_LEN] {
let hkdf = Hkdf::<Sha256>::new(Some(&HKDF_SALT), master.expose());
let mut check = [0_u8; KEY_CHECK_LEN];
hkdf.expand(HKDF_INFO_KEY_CHECK, &mut check)
.expect("HKDF 输出 32 字节不可能失败");
check
}
pub fn verify_key_check(master: &MasterKey, stored: &[u8]) -> Result<()> {
let stored: &[u8; KEY_CHECK_LEN] = stored.try_into().map_err(|_| Error::VaultBadKeyCheck)?;
if key_check(master).ct_eq(stored).into() {
Ok(())
} else {
Err(Error::VaultDecryptionFailed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RecordKind {
Entry,
Tombstone,
SyncEntry,
SyncTombstone,
}
impl RecordKind {
pub(crate) const fn as_byte(self) -> u8 {
match self {
Self::Entry => 0x01,
Self::Tombstone => 0x02,
Self::SyncEntry => 0x81,
Self::SyncTombstone => 0x82,
}
}
pub(crate) const fn from_byte(byte: u8) -> Option<Self> {
match byte {
0x01 => Some(Self::Entry),
0x02 => Some(Self::Tombstone),
_ => None,
}
}
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "契约定义,供 sync 的编译期断言与测试使用;载荷解析按种类试解密,不走这里"
)
)]
pub(crate) const fn from_sync_byte(byte: u8) -> Option<Self> {
match byte {
0x81 => Some(Self::SyncEntry),
0x82 => Some(Self::SyncTombstone),
_ => None,
}
}
}
const AAD_LEN: usize = 1 + EntryId::LEN + 1;
pub(crate) fn build_aad(kind: RecordKind, entry_id: &EntryId, format_version: u8) -> [u8; AAD_LEN] {
let mut aad = [0_u8; AAD_LEN];
aad[0] = kind.as_byte();
aad[1..1 + EntryId::LEN].copy_from_slice(entry_id.as_bytes());
aad[AAD_LEN - 1] = format_version;
aad
}
pub(crate) fn seal(
master: &MasterKey,
kind: RecordKind,
entry_id: &EntryId,
format_version: u8,
plaintext: &[u8],
) -> Result<(Nonce, Vec<u8>)> {
let key = entry_key(master)?;
seal_with_key(&key, kind, entry_id, format_version, plaintext)
}
pub(crate) fn seal_with_key(
key: &[u8; 32],
kind: RecordKind,
entry_id: &EntryId,
format_version: u8,
plaintext: &[u8],
) -> Result<(Nonce, Vec<u8>)> {
let cipher = XChaCha20Poly1305::new(&Key::from(*key));
let nonce = random_nonce()?;
let aad = build_aad(kind, entry_id, format_version);
let mut buffer = plaintext.to_vec();
cipher
.encrypt_in_place(&XNonce::from(nonce), &aad, &mut buffer)
.map_err(|_| Error::VaultDecryptionFailed)?;
Ok((nonce, buffer))
}
pub(crate) fn open(
master: &MasterKey,
kind: RecordKind,
entry_id: &EntryId,
format_version: u8,
nonce: &Nonce,
ciphertext: &[u8],
) -> Result<Vec<u8>> {
let key = entry_key(master)?;
open_with_key(&key, kind, entry_id, format_version, nonce, ciphertext)
}
pub(crate) fn open_with_key(
key: &[u8; 32],
kind: RecordKind,
entry_id: &EntryId,
format_version: u8,
nonce: &Nonce,
ciphertext: &[u8],
) -> Result<Vec<u8>> {
let cipher = XChaCha20Poly1305::new(&Key::from(*key));
let aad = build_aad(kind, entry_id, format_version);
let mut buffer = ciphertext.to_vec();
cipher
.decrypt_in_place(&XNonce::from(*nonce), &aad, &mut buffer)
.map_err(|_| Error::VaultDecryptionFailed)?;
Ok(buffer)
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> MasterKey {
MasterKey::generate().expect("OS 随机源可用")
}
#[test]
fn master_key_is_32_bytes_and_debug_is_redacted() {
let k = key();
assert_eq!(k.expose().len(), MASTER_KEY_LEN);
let debug = format!("{k:?}");
assert!(debug.contains("REDACTED"), "Debug 未脱敏:{debug}");
for byte in k.expose() {
assert!(
!debug.contains(&format!("{byte}")),
"Debug 泄露了密钥字节:{debug}"
);
}
}
#[test]
fn master_key_rejects_wrong_length() {
for len in [0_usize, 1, 31, 33, 64] {
let bytes: SecretBytes = vec![0_u8; len].into();
match MasterKey::from_bytes(bytes) {
Err(Error::MasterKeyWrongLength { expected, actual }) => {
assert_eq!(expected, MASTER_KEY_LEN);
assert_eq!(actual, len);
}
other => panic!("长度 {len} 应被拒绝,实际 ok={}", other.is_ok()),
}
}
}
#[test]
fn master_key_accepts_exactly_32_bytes() {
let bytes: SecretBytes = vec![7_u8; MASTER_KEY_LEN].into();
let k = MasterKey::from_bytes(bytes).expect("长度正确");
assert_eq!(k.expose(), &[7_u8; MASTER_KEY_LEN]);
}
#[test]
fn derived_key_is_deterministic() {
let bytes: SecretBytes = vec![3_u8; MASTER_KEY_LEN].into();
let a = MasterKey::from_bytes(bytes.clone()).expect("长度正确");
let b = MasterKey::from_bytes(bytes).expect("长度正确");
assert_eq!(
entry_key(&a).expect("可派生"),
entry_key(&b).expect("可派生")
);
}
#[test]
fn derived_key_differs_per_master_key() {
let a = MasterKey::from_bytes(vec![1_u8; MASTER_KEY_LEN].into()).expect("长度正确");
let b = MasterKey::from_bytes(vec![2_u8; MASTER_KEY_LEN].into()).expect("长度正确");
assert_ne!(
entry_key(&a).expect("可派生"),
entry_key(&b).expect("可派生")
);
}
#[test]
fn derived_key_is_not_the_master_key() {
let bytes: SecretBytes = vec![5_u8; MASTER_KEY_LEN].into();
let k = MasterKey::from_bytes(bytes).expect("长度正确");
assert_ne!(
entry_key(&k).expect("可派生"),
*k.expose().first_chunk::<32>().expect("32 字节")
);
}
#[test]
fn seal_then_open_roundtrips() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let plaintext = b"the quick brown fox";
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &id, 1, plaintext).expect("可加密");
assert_eq!(ciphertext.len(), plaintext.len() + TAG_LEN);
assert_ne!(
&ciphertext[..plaintext.len()],
&plaintext[..],
"密文与明文相同"
);
let opened = open(&k, RecordKind::Entry, &id, 1, &nonce, &ciphertext).expect("可解密");
assert_eq!(opened, plaintext);
}
#[test]
fn each_seal_uses_a_fresh_nonce() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let (n1, _) = seal(&k, RecordKind::Entry, &id, 1, b"same").expect("可加密");
let (n2, _) = seal(&k, RecordKind::Entry, &id, 1, b"same").expect("可加密");
assert_ne!(n1, n2, "两次加密复用了 nonce");
}
#[test]
fn ciphertext_is_not_deterministic() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let (_, c1) = seal(&k, RecordKind::Entry, &id, 1, b"same").expect("可加密");
let (_, c2) = seal(&k, RecordKind::Entry, &id, 1, b"same").expect("可加密");
assert_ne!(c1, c2);
}
#[test]
fn wrong_master_key_fails() {
let id = EntryId::generate().expect("OS 随机源可用");
let (nonce, ciphertext) =
seal(&key(), RecordKind::Entry, &id, 1, b"secret").expect("可加密");
let err = open(&key(), RecordKind::Entry, &id, 1, &nonce, &ciphertext);
assert!(matches!(err, Err(Error::VaultDecryptionFailed)));
}
#[test]
fn ciphertext_cannot_move_between_entry_ids() {
let k = key();
let a = EntryId::generate().expect("OS 随机源可用");
let b = EntryId::generate().expect("OS 随机源可用");
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &a, 1, b"payload").expect("可加密");
assert!(open(&k, RecordKind::Entry, &a, 1, &nonce, &ciphertext).is_ok());
let err = open(&k, RecordKind::Entry, &b, 1, &nonce, &ciphertext);
assert!(
matches!(err, Err(Error::VaultDecryptionFailed)),
"密文被搬运到别的条目却仍能解开"
);
}
#[test]
fn ciphertext_cannot_move_between_kinds() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &id, 1, b"payload").expect("可加密");
assert!(open(&k, RecordKind::Tombstone, &id, 1, &nonce, &ciphertext).is_err());
let (nonce2, ciphertext2) =
seal(&k, RecordKind::Tombstone, &id, 1, b"payload").expect("可加密");
assert!(open(&k, RecordKind::Entry, &id, 1, &nonce2, &ciphertext2).is_err());
}
#[test]
fn ciphertext_cannot_move_between_format_versions() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &id, 1, b"payload").expect("可加密");
assert!(open(&k, RecordKind::Entry, &id, 2, &nonce, &ciphertext).is_err());
}
#[test]
fn flipping_any_ciphertext_byte_fails_authentication() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let plaintext = b"0123456789abcdef0123456789abcdef";
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &id, 1, plaintext).expect("可加密");
for index in 0..ciphertext.len() {
let mut tampered = ciphertext.clone();
tampered[index] ^= 0x01;
let err = open(&k, RecordKind::Entry, &id, 1, &nonce, &tampered);
assert!(
matches!(err, Err(Error::VaultDecryptionFailed)),
"密文第 {index} 字节被翻转却仍通过认证"
);
}
for index in 0..NONCE_LEN {
let mut tampered = nonce;
tampered[index] ^= 0x01;
let err = open(&k, RecordKind::Entry, &id, 1, &tampered, &ciphertext);
assert!(
matches!(err, Err(Error::VaultDecryptionFailed)),
"nonce 第 {index} 字节被翻转却仍通过认证"
);
}
}
#[test]
fn aad_layout_is_stable() {
let id = EntryId::from_bytes([0xAB; EntryId::LEN]);
let aad = build_aad(RecordKind::Entry, &id, 1);
assert_eq!(aad.len(), AAD_LEN);
assert_eq!(aad[0], 0x01, "种类字节");
assert_eq!(&aad[1..17], &[0xAB; 16], "entry_id 应紧随其后");
assert_eq!(aad[17], 1, "格式版本应在末字节");
assert_eq!(build_aad(RecordKind::Tombstone, &id, 7)[0], 0x02);
assert_eq!(build_aad(RecordKind::Tombstone, &id, 7)[17], 7);
}
#[test]
fn record_kind_byte_roundtrips_and_rejects_unknown() {
for kind in [RecordKind::Entry, RecordKind::Tombstone] {
assert_eq!(RecordKind::from_byte(kind.as_byte()), Some(kind));
}
for unknown in [0_u8, 3, 0xFF] {
assert_eq!(RecordKind::from_byte(unknown), None);
}
}
#[test]
fn empty_plaintext_roundtrips() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &id, 1, b"").expect("可加密");
assert_eq!(ciphertext.len(), TAG_LEN, "空明文只剩标签");
let opened = open(&k, RecordKind::Entry, &id, 1, &nonce, &ciphertext).expect("可解密");
assert!(opened.is_empty());
}
#[test]
fn truncated_ciphertext_fails() {
let k = key();
let id = EntryId::generate().expect("OS 随机源可用");
let (nonce, ciphertext) = seal(&k, RecordKind::Entry, &id, 1, b"payload").expect("可加密");
for cut in 1..=ciphertext.len() {
let truncated = &ciphertext[..ciphertext.len() - cut];
let err = open(&k, RecordKind::Entry, &id, 1, &nonce, truncated);
assert!(
matches!(err, Err(Error::VaultDecryptionFailed)),
"截断 {cut} 字节后仍通过认证"
);
}
}
#[test]
fn master_key_bytes_roundtrip() {
let original = MasterKey::from_bytes(vec![9_u8; MASTER_KEY_LEN].into()).expect("长度正确");
let bytes = master_key_to_bytes(&original);
let rebuilt = MasterKey::from_bytes(bytes).expect("长度正确");
assert_eq!(original.expose(), rebuilt.expose());
}
}