use std::fmt;
use std::num::NonZeroU32;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use fs4::FileExt as Fs4FileExt;
use ring::{aead, pbkdf2};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, Zeroizing};
use super::hex_lower;
const KEY_LEN: usize = 32;
const MAC_LEN: usize = 32;
const KEY_ID_LEN: usize = 16;
pub(crate) const KEY_FILE_NAME: &str = "store_auth_root.json";
const KEY_FILE_TMP_NAME: &str = "store_auth_root.json.tmp";
const KEY_LOCK_FILE_NAME: &str = "store_auth_root.lock";
const KEY_FILE_SCHEMA: &str = "ee.store_auth.keyfile.v1";
const MAX_RETIRED_KEYS: usize = 4;
const MAX_KEY_FILE_BYTES: u64 = 64 * 1024;
const RECOVERY_SCHEMA: &str = "ee.store_auth.recovery.v1";
const RECOVERY_KDF: &str = "pbkdf2-hmac-sha256";
const RECOVERY_ITERATIONS: u32 = 600_000;
pub(crate) const MAX_RECOVERY_BYTES: usize = 512 * 1024;
pub(crate) const MAX_RECOVERY_PASSPHRASE_BYTES: usize = 4096;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct StoreAuthRecoveryEnvelope {
schema: String,
kdf: String,
iterations: u32,
salt: [u8; 32],
nonce: [u8; aead::NONCE_LEN],
ciphertext: Vec<u8>,
}
pub(crate) struct RecoveredStoreAuth {
pub(crate) key_file: Zeroizing<Vec<u8>>,
pub(crate) key_ids: Vec<String>,
}
impl fmt::Debug for RecoveredStoreAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RecoveredStoreAuth")
.field("key_ids", &self.key_ids)
.field("key_file", &"<redacted>")
.finish()
}
}
fn recovery_error(message: &str) -> StoreAuthError {
StoreAuthError::Malformed {
message: message.to_owned(),
}
}
pub(crate) fn validate_recovery_passphrase(passphrase: &str) -> Result<(), StoreAuthError> {
if passphrase.len() > MAX_RECOVERY_PASSPHRASE_BYTES
|| !(12..=1024).contains(&passphrase.chars().count())
|| passphrase.contains(['\r', '\n', '\0'])
{
return Err(recovery_error(
"recovery passphrase must contain 12 to 1024 characters on a single line",
));
}
Ok(())
}
fn recovery_cipher(passphrase: &str, salt: &[u8]) -> Result<aead::LessSafeKey, StoreAuthError> {
let iterations = NonZeroU32::new(RECOVERY_ITERATIONS)
.ok_or_else(|| recovery_error("invalid recovery KDF work factor"))?;
let mut key = Zeroizing::new([0_u8; KEY_LEN]);
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
iterations,
salt,
passphrase.as_bytes(),
key.as_mut(),
);
aead::UnboundKey::new(&aead::CHACHA20_POLY1305, key.as_ref())
.map(aead::LessSafeKey::new)
.map_err(|_| recovery_error("could not initialize recovery encryption"))
}
pub const MESH_STORE_AUTHENTICATION_UNAVAILABLE_CODE: &str =
"mesh_store_authentication_unavailable";
#[must_use]
pub fn workspace_keys_dir(workspace_path: &Path) -> PathBuf {
workspace_path
.join(crate::config::WORKSPACE_MARKER)
.join("keys")
}
const SELF_CHECK_CONTEXT: &str = "ee.store_auth.self_check.v1";
const SELF_CHECK_MESSAGE: &[u8] = b"ee.store_auth.self_check.message.v1";
const KAT_MESSAGE: &[u8] = b"ee-store-auth-kat-message";
const KAT_SUBKEY_HEX: &str = "cb573690cdf5ecbcfbc91c2dc82459a8d8161e673e52abd8e2be14dba253037f";
const KAT_MAC_HEX: &str = "d95066c3c600bbb4fb8f307bcfb553a56862e442155d2f5e3d80497ead8bd0c0";
const KAT_SELF_CHECK_HEX: &str = "2dc8db78eb25d723bae6ec5280656f8fe9070a417e476c859d194ef6f22d6f3e";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StoreAuthError {
Randomness { message: String },
Io { path: String, message: String },
InsecurePermissions { path: String, detail: String },
SymlinkComponent { path: String },
Malformed { message: String },
SchemaMismatch { found: String, expected: String },
SelfCheckFailed,
PrimitiveKnownAnswerFailed { detail: String },
AlreadyInitialized { path: String },
NotInitialized { path: String },
}
impl StoreAuthError {
#[must_use]
pub fn degraded_code(&self) -> &'static str {
MESH_STORE_AUTHENTICATION_UNAVAILABLE_CODE
}
#[must_use]
pub fn message(&self) -> String {
match self {
Self::Randomness { message } => {
format!("Store-authentication randomness failed: {message}")
}
Self::Io { path, message } => {
format!("Store-authentication key store I/O failed at {path}: {message}")
}
Self::InsecurePermissions { path, detail } => {
format!("Store-authentication key store at {path} is not owner-only: {detail}")
}
Self::SymlinkComponent { path } => {
format!("Store-authentication key path {path} traverses a symbolic link")
}
Self::Malformed { message } => {
format!("Store-authentication key store is malformed: {message}")
}
Self::SchemaMismatch { found, expected } => {
format!(
"Store-authentication key store schema {found} is not the supported {expected}"
)
}
Self::SelfCheckFailed => {
"Store-authentication key store failed its integrity self-check".to_owned()
}
Self::PrimitiveKnownAnswerFailed { detail } => {
format!("Store-authentication primitive self-test failed: {detail}")
}
Self::AlreadyInitialized { path } => {
format!("Store-authentication key store already exists at {path}")
}
Self::NotInitialized { path } => {
format!("Store-authentication key store is not initialized at {path}")
}
}
}
#[must_use]
pub fn repair(&self) -> String {
match self {
Self::InsecurePermissions { .. } => {
"Restrict the key directory to 0700 and the key file to 0600 (owner-only), \
then re-run."
.to_owned()
}
Self::SymlinkComponent { .. } => {
"Replace the symlinked key path with a real owner-only directory and re-run."
.to_owned()
}
Self::SelfCheckFailed | Self::Malformed { .. } | Self::SchemaMismatch { .. } => {
"The key store is unusable. Restore the protected key directory from a secure \
backup, or re-initialize the store (imported native-trust rows must be \
re-attested)."
.to_owned()
}
Self::NotInitialized { .. } => {
"Initialize the store-authentication root before importing at native trust."
.to_owned()
}
_ => "Resolve the underlying key-store fault and re-run; nothing was admitted."
.to_owned(),
}
}
}
impl fmt::Display for StoreAuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message())
}
}
impl std::error::Error for StoreAuthError {}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct KeyId([u8; KEY_ID_LEN]);
impl KeyId {
#[must_use]
pub fn to_hex(&self) -> String {
hex_lower(&self.0)
}
pub fn from_hex(value: &str) -> Result<Self, StoreAuthError> {
Ok(Self(decode_hex_fixed::<KEY_ID_LEN>(value, "key id")?))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; KEY_ID_LEN] {
&self.0
}
}
impl fmt::Debug for KeyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "KeyId({})", self.to_hex())
}
}
impl fmt::Display for KeyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
#[derive(Clone, Copy)]
pub struct Mac([u8; MAC_LEN]);
impl Mac {
#[must_use]
pub const fn from_bytes(bytes: [u8; MAC_LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub fn to_hex(&self) -> String {
hex_lower(&self.0)
}
pub fn from_hex(value: &str) -> Result<Self, StoreAuthError> {
Ok(Self(decode_hex_fixed::<MAC_LEN>(value, "mac")?))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; MAC_LEN] {
&self.0
}
}
impl PartialEq for Mac {
fn eq(&self, other: &Self) -> bool {
constant_time_eq(&self.0, &other.0)
}
}
impl Eq for Mac {}
impl fmt::Debug for Mac {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Mac({})", self.to_hex())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum MacDomain {
NativeImportRecordsRoot,
PlaybookImportRecordsRoot,
LaneApprovalSnapshotTag,
LaneApprovalEnvelopeMac,
LaneApprovalAuditId,
BodyApprovalSnapshotTag,
BodyApprovalEnvelopeMac,
BodyApprovalAuditId,
}
impl MacDomain {
#[must_use]
pub const fn context(self) -> &'static str {
match self {
Self::NativeImportRecordsRoot => "ee.store_auth.native_import.records_root.v1",
Self::PlaybookImportRecordsRoot => "ee.store_auth.playbook_import.records_root.v1",
Self::LaneApprovalSnapshotTag => "ee.store_auth.lane_approval.snapshot_tag.v1",
Self::LaneApprovalEnvelopeMac => "ee.store_auth.lane_approval.envelope_mac.v1",
Self::LaneApprovalAuditId => "ee.store_auth.lane_approval.audit_id.v1",
Self::BodyApprovalSnapshotTag => "ee.store_auth.body_approval.snapshot_tag.v1",
Self::BodyApprovalEnvelopeMac => "ee.store_auth.body_approval.envelope_mac.v1",
Self::BodyApprovalAuditId => "ee.store_auth.body_approval.audit_id.v1",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KeyClass {
Current,
Retired,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KeyVerification {
Match { key_class: KeyClass },
Mismatch,
KeyOutsideWindow,
}
struct Secret([u8; KEY_LEN]);
impl Secret {
fn as_bytes(&self) -> &[u8; KEY_LEN] {
&self.0
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret(<redacted>)")
}
}
impl Drop for Secret {
fn drop(&mut self) {
self.0.zeroize();
}
}
#[derive(Debug)]
struct KeyEntry {
key_id: KeyId,
root: Secret,
}
impl KeyEntry {
fn derive(&self, context: &str) -> Secret {
Secret(blake3::derive_key(context, self.root.as_bytes()))
}
fn mac(&self, domain: MacDomain, message: &[u8]) -> Mac {
let subkey = self.derive(domain.context());
let tag = blake3::keyed_hash(subkey.as_bytes(), message);
Mac(*tag.as_bytes())
}
fn self_check(&self) -> Mac {
let subkey = self.derive(SELF_CHECK_CONTEXT);
let tag = blake3::keyed_hash(subkey.as_bytes(), SELF_CHECK_MESSAGE);
Mac(*tag.as_bytes())
}
fn to_file_entry(&self) -> KeyFileEntry {
KeyFileEntry {
key_id: self.key_id.to_hex(),
root: hex_lower(self.root.as_bytes()),
}
}
}
pub struct StoreAuthRoot {
keys_dir: PathBuf,
current: KeyEntry,
retired: Vec<KeyEntry>,
}
pub struct StoreAuthReadGuard {
root: StoreAuthRoot,
lock_file: std::fs::File,
}
impl Deref for StoreAuthReadGuard {
type Target = StoreAuthRoot;
fn deref(&self) -> &Self::Target {
&self.root
}
}
impl fmt::Debug for StoreAuthReadGuard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("StoreAuthReadGuard")
.field(&self.root)
.finish()
}
}
impl Drop for StoreAuthReadGuard {
fn drop(&mut self) {
let _ = Fs4FileExt::unlock(&self.lock_file);
}
}
struct StoreAuthWriteGuard {
lock_file: std::fs::File,
}
impl Drop for StoreAuthWriteGuard {
fn drop(&mut self) {
let _ = Fs4FileExt::unlock(&self.lock_file);
}
}
impl fmt::Debug for StoreAuthRoot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoreAuthRoot")
.field("keys_dir", &self.keys_dir)
.field("current_key_id", &self.current.key_id)
.field("retired_keys", &self.retired.len())
.finish()
}
}
impl StoreAuthRoot {
pub fn open_or_create(keys_dir: impl AsRef<Path>) -> Result<Self, StoreAuthError> {
let keys_dir = keys_dir.as_ref();
let path = keys_dir.join(KEY_FILE_NAME);
match path.try_exists() {
Ok(true) => Self::open(keys_dir),
Ok(false) => match Self::create(keys_dir) {
Err(StoreAuthError::AlreadyInitialized { .. }) => Self::open(keys_dir),
other => other,
},
Err(error) => Err(StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
}),
}
}
pub fn create(keys_dir: impl AsRef<Path>) -> Result<Self, StoreAuthError> {
primitive_known_answer_check()?;
let keys_dir = keys_dir.as_ref();
let path = keys_dir.join(KEY_FILE_NAME);
reject_symlink_components(keys_dir, &path)?;
ensure_hardened_dir(keys_dir)?;
let current = KeyEntry {
key_id: KeyId(random_bytes::<KEY_ID_LEN>()?),
root: Secret(random_bytes::<KEY_LEN>()?),
};
let root = Self {
keys_dir: keys_dir.to_path_buf(),
current,
retired: Vec::new(),
};
let serialized = root.serialize()?;
write_exclusive(&path, &serialized)?;
Ok(root)
}
pub fn open(keys_dir: impl AsRef<Path>) -> Result<Self, StoreAuthError> {
primitive_known_answer_check()?;
let keys_dir = keys_dir.as_ref();
let path = keys_dir.join(KEY_FILE_NAME);
reject_symlink_components(keys_dir, &path)?;
match path.try_exists() {
Ok(true) => {}
Ok(false) => {
return Err(StoreAuthError::NotInitialized {
path: path.display().to_string(),
});
}
Err(error) => {
return Err(StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
});
}
}
enforce_owner_only_dir(keys_dir)?;
enforce_owner_only_file(&path)?;
let bytes = Zeroizing::new(read_key_file(&path)?);
Self::from_serialized(keys_dir, &bytes)
}
pub(crate) fn from_serialized(keys_dir: &Path, bytes: &[u8]) -> Result<Self, StoreAuthError> {
primitive_known_answer_check()?;
if bytes.len() as u64 > MAX_KEY_FILE_BYTES {
return Err(recovery_error("key file exceeds the size limit"));
}
let doc: KeyFileDoc =
serde_json::from_slice(bytes).map_err(|error| StoreAuthError::Malformed {
message: format!("key file JSON: {error}"),
})?;
if doc.schema != KEY_FILE_SCHEMA {
return Err(StoreAuthError::SchemaMismatch {
found: doc.schema,
expected: KEY_FILE_SCHEMA.to_owned(),
});
}
if doc.retired.len() > MAX_RETIRED_KEYS {
return Err(StoreAuthError::Malformed {
message: format!(
"retired window has {} keys, exceeds max {MAX_RETIRED_KEYS}",
doc.retired.len()
),
});
}
let current = doc.current.into_entry()?;
let retired = doc
.retired
.into_iter()
.map(KeyFileEntry::into_entry)
.collect::<Result<Vec<_>, _>>()?;
let root = Self {
keys_dir: keys_dir.to_path_buf(),
current,
retired,
};
let expected = Mac::from_hex(&doc.self_check)?;
if root.current.self_check() != expected {
return Err(StoreAuthError::SelfCheckFailed);
}
let ids = root.window_key_ids();
if ids
.iter()
.enumerate()
.any(|(index, id)| ids[..index].contains(id))
{
return Err(recovery_error(
"key file contains duplicate key identifiers",
));
}
Ok(root)
}
pub(crate) fn encrypted_recovery(&self, passphrase: &str) -> Result<Vec<u8>, StoreAuthError> {
validate_recovery_passphrase(passphrase)?;
let salt = random_bytes::<32>()?;
let nonce = random_bytes::<{ aead::NONCE_LEN }>()?;
let mut ciphertext = Zeroizing::new(self.serialize()?);
recovery_cipher(passphrase, &salt)?
.seal_in_place_append_tag(
aead::Nonce::assume_unique_for_key(nonce),
aead::Aad::from(RECOVERY_SCHEMA.as_bytes()),
&mut *ciphertext,
)
.map_err(|_| recovery_error("could not encrypt recovery keys"))?;
let envelope = StoreAuthRecoveryEnvelope {
schema: RECOVERY_SCHEMA.to_owned(),
kdf: RECOVERY_KDF.to_owned(),
iterations: RECOVERY_ITERATIONS,
salt,
nonce,
ciphertext: ciphertext.to_vec(),
};
serde_json::to_vec(&envelope)
.map_err(|_| recovery_error("could not serialize encrypted recovery keys"))
}
pub(crate) fn decrypt_recovery(
bytes: &[u8],
passphrase: &str,
) -> Result<RecoveredStoreAuth, StoreAuthError> {
validate_recovery_passphrase(passphrase)?;
if bytes.len() > MAX_RECOVERY_BYTES {
return Err(recovery_error(
"encrypted recovery envelope exceeds the size limit",
));
}
let envelope: StoreAuthRecoveryEnvelope = serde_json::from_slice(bytes)
.map_err(|_| recovery_error("invalid encrypted recovery envelope"))?;
if envelope.schema != RECOVERY_SCHEMA
|| envelope.kdf != RECOVERY_KDF
|| envelope.iterations != RECOVERY_ITERATIONS
|| envelope.ciphertext.len() < aead::CHACHA20_POLY1305.tag_len()
|| envelope.ciphertext.len() as u64
> MAX_KEY_FILE_BYTES + aead::CHACHA20_POLY1305.tag_len() as u64
{
return Err(recovery_error(
"unsupported or malformed encrypted recovery envelope",
));
}
let cipher = recovery_cipher(passphrase, &envelope.salt)?;
let mut plaintext = Zeroizing::new(envelope.ciphertext);
let opened = cipher
.open_in_place(
aead::Nonce::assume_unique_for_key(envelope.nonce),
aead::Aad::from(RECOVERY_SCHEMA.as_bytes()),
plaintext.as_mut(),
)
.map_err(|_| {
recovery_error("recovery decryption failed: wrong passphrase or modified envelope")
})?;
let root = Self::from_serialized(Path::new("recovered-store-auth"), opened)
.map_err(|_| recovery_error("decrypted recovery key file is invalid"))?;
Ok(RecoveredStoreAuth {
key_file: Zeroizing::new(root.serialize()?),
key_ids: root.window_key_ids().iter().map(KeyId::to_hex).collect(),
})
}
pub fn open_read_locked(
keys_dir: impl AsRef<Path>,
) -> Result<StoreAuthReadGuard, StoreAuthError> {
let keys_dir = keys_dir.as_ref();
let lock_file = open_key_lock_file(keys_dir)?;
Fs4FileExt::lock_shared(&lock_file).map_err(|error| StoreAuthError::Io {
path: keys_dir.join(KEY_LOCK_FILE_NAME).display().to_string(),
message: format!("acquire shared key-store lock: {error}"),
})?;
match Self::open(keys_dir) {
Ok(root) => Ok(StoreAuthReadGuard { root, lock_file }),
Err(error) => {
let _ = Fs4FileExt::unlock(&lock_file);
Err(error)
}
}
}
pub fn rotate(&mut self) -> Result<KeyId, StoreAuthError> {
let lock_file = open_key_lock_file(&self.keys_dir)?;
Fs4FileExt::lock(&lock_file).map_err(|error| StoreAuthError::Io {
path: self.keys_dir.join(KEY_LOCK_FILE_NAME).display().to_string(),
message: format!("acquire exclusive key-store lock: {error}"),
})?;
let _write_guard = StoreAuthWriteGuard { lock_file };
let disk = Self::open(&self.keys_dir)?;
self.current = disk.current;
self.retired = disk.retired;
let new_entry = KeyEntry {
key_id: KeyId(random_bytes::<KEY_ID_LEN>()?),
root: Secret(random_bytes::<KEY_LEN>()?),
};
let previous = std::mem::replace(&mut self.current, new_entry);
self.retired.insert(0, previous);
self.retired.truncate(MAX_RETIRED_KEYS);
let path = self.keys_dir.join(KEY_FILE_NAME);
let tmp = self.keys_dir.join(KEY_FILE_TMP_NAME);
let serialized = self.serialize()?;
write_replace(&tmp, &path, &serialized)?;
Ok(self.current.key_id)
}
#[must_use]
pub fn current_key_id(&self) -> KeyId {
self.current.key_id
}
#[must_use]
pub fn window_key_ids(&self) -> Vec<KeyId> {
let mut ids = Vec::with_capacity(1 + self.retired.len());
ids.push(self.current.key_id);
ids.extend(self.retired.iter().map(|entry| entry.key_id));
ids
}
pub fn mac(&self, domain: MacDomain, message: &[u8]) -> Result<Mac, StoreAuthError> {
Ok(self.current.mac(domain, message))
}
pub fn verify(
&self,
domain: MacDomain,
message: &[u8],
candidate: &Mac,
) -> Result<bool, StoreAuthError> {
Ok(self.current.mac(domain, message) == *candidate)
}
pub fn verify_with_key(
&self,
key_id: KeyId,
domain: MacDomain,
message: &[u8],
candidate: &Mac,
) -> Result<KeyVerification, StoreAuthError> {
let (entry, key_class) = if self.current.key_id == key_id {
(&self.current, KeyClass::Current)
} else if let Some(entry) = self.retired.iter().find(|entry| entry.key_id == key_id) {
(entry, KeyClass::Retired)
} else {
return Ok(KeyVerification::KeyOutsideWindow);
};
if entry.mac(domain, message) == *candidate {
Ok(KeyVerification::Match { key_class })
} else {
Ok(KeyVerification::Mismatch)
}
}
fn serialize(&self) -> Result<Vec<u8>, StoreAuthError> {
let doc = KeyFileDoc {
schema: KEY_FILE_SCHEMA.to_owned(),
current: self.current.to_file_entry(),
retired: self.retired.iter().map(KeyEntry::to_file_entry).collect(),
self_check: self.current.self_check().to_hex(),
};
serde_json::to_vec_pretty(&doc).map_err(|error| StoreAuthError::Io {
path: self.keys_dir.join(KEY_FILE_NAME).display().to_string(),
message: format!("serialize key file: {error}"),
})
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct KeyFileDoc {
schema: String,
current: KeyFileEntry,
#[serde(default)]
retired: Vec<KeyFileEntry>,
self_check: String,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct KeyFileEntry {
key_id: String,
root: String,
}
impl fmt::Debug for KeyFileEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyFileEntry")
.field("key_id", &self.key_id)
.field("root", &"<redacted>")
.finish()
}
}
impl Drop for KeyFileEntry {
fn drop(&mut self) {
self.root.zeroize();
}
}
impl KeyFileEntry {
fn into_entry(self) -> Result<KeyEntry, StoreAuthError> {
Ok(KeyEntry {
key_id: KeyId::from_hex(&self.key_id)?,
root: Secret(decode_hex_fixed::<KEY_LEN>(&self.root, "root")?),
})
}
}
fn primitive_known_answer_check() -> Result<(), StoreAuthError> {
let root: [u8; KEY_LEN] = std::array::from_fn(|index| index as u8);
let subkey = blake3::derive_key(MacDomain::NativeImportRecordsRoot.context(), &root);
if hex_lower(&subkey) != KAT_SUBKEY_HEX {
return Err(StoreAuthError::PrimitiveKnownAnswerFailed {
detail: "derive_key subkey mismatch".to_owned(),
});
}
let mac = blake3::keyed_hash(&subkey, KAT_MESSAGE);
if hex_lower(mac.as_bytes()) != KAT_MAC_HEX {
return Err(StoreAuthError::PrimitiveKnownAnswerFailed {
detail: "keyed_hash MAC mismatch".to_owned(),
});
}
let self_check_subkey = blake3::derive_key(SELF_CHECK_CONTEXT, &root);
let self_check = blake3::keyed_hash(&self_check_subkey, SELF_CHECK_MESSAGE);
if hex_lower(self_check.as_bytes()) != KAT_SELF_CHECK_HEX {
return Err(StoreAuthError::PrimitiveKnownAnswerFailed {
detail: "self-check construction mismatch".to_owned(),
});
}
Ok(())
}
fn random_bytes<const N: usize>() -> Result<[u8; N], StoreAuthError> {
let mut buffer = [0_u8; N];
getrandom::fill(&mut buffer).map_err(|error| StoreAuthError::Randomness {
message: error.to_string(),
})?;
Ok(buffer)
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut diff = 0_u8;
for (a, b) in left.iter().zip(right.iter()) {
diff |= a ^ b;
}
diff == 0
}
fn decode_hex_fixed<const N: usize>(value: &str, label: &str) -> Result<[u8; N], StoreAuthError> {
let trimmed = value.trim();
if trimmed.len() != N * 2 {
return Err(StoreAuthError::Malformed {
message: format!(
"{label} must be {} hex chars, found {}",
N * 2,
trimmed.len()
),
});
}
let bytes = trimmed.as_bytes();
let mut out = [0_u8; N];
let mut index = 0;
while index < N {
let high = hex_nibble(bytes[index * 2], label)?;
let low = hex_nibble(bytes[index * 2 + 1], label)?;
out[index] = (high << 4) | low;
index += 1;
}
Ok(out)
}
fn hex_nibble(byte: u8, label: &str) -> Result<u8, StoreAuthError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(StoreAuthError::Malformed {
message: format!("{label} contains a non-hex character"),
}),
}
}
fn read_key_file(path: &Path) -> Result<Vec<u8>, StoreAuthError> {
let metadata = std::fs::symlink_metadata(path).map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
})?;
if metadata.len() > MAX_KEY_FILE_BYTES {
return Err(StoreAuthError::Malformed {
message: format!("key file is {} bytes, exceeds cap", metadata.len()),
});
}
std::fs::read(path).map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
})
}
fn reject_symlink_components(keys_dir: &Path, path: &Path) -> Result<(), StoreAuthError> {
for candidate in [keys_dir, path] {
if let Ok(metadata) = std::fs::symlink_metadata(candidate)
&& metadata.file_type().is_symlink()
{
return Err(StoreAuthError::SymlinkComponent {
path: candidate.display().to_string(),
});
}
}
Ok(())
}
fn ensure_hardened_dir(keys_dir: &Path) -> Result<(), StoreAuthError> {
std::fs::create_dir_all(keys_dir).map_err(|error| StoreAuthError::Io {
path: keys_dir.display().to_string(),
message: error.to_string(),
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(keys_dir, std::fs::Permissions::from_mode(0o700)).map_err(
|error| StoreAuthError::Io {
path: keys_dir.display().to_string(),
message: format!("harden directory permissions: {error}"),
},
)?;
}
Ok(())
}
#[cfg(unix)]
fn enforce_owner_only_dir(keys_dir: &Path) -> Result<(), StoreAuthError> {
enforce_owner_only_mode(keys_dir, "key directory")
}
#[cfg(unix)]
fn enforce_owner_only_file(path: &Path) -> Result<(), StoreAuthError> {
enforce_owner_only_mode(path, "key file")
}
#[cfg(unix)]
fn enforce_owner_only_mode(path: &Path, label: &str) -> Result<(), StoreAuthError> {
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::symlink_metadata(path).map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
})?;
let mode = metadata.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
return Err(StoreAuthError::InsecurePermissions {
path: path.display().to_string(),
detail: format!("{label} mode {mode:04o} grants group/other access"),
});
}
Ok(())
}
#[cfg(not(unix))]
fn enforce_owner_only_dir(_keys_dir: &Path) -> Result<(), StoreAuthError> {
Ok(())
}
#[cfg(not(unix))]
fn enforce_owner_only_file(_path: &Path) -> Result<(), StoreAuthError> {
Ok(())
}
fn open_key_lock_file(keys_dir: &Path) -> Result<std::fs::File, StoreAuthError> {
ensure_hardened_dir(keys_dir)?;
let path = keys_dir.join(KEY_LOCK_FILE_NAME);
reject_symlink_components(keys_dir, &path)?;
let mut options = std::fs::OpenOptions::new();
options.read(true).write(true).create(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let file = options.open(&path).map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: format!("open key-store lock: {error}"),
})?;
enforce_owner_only_file(&path)?;
Ok(file)
}
fn write_exclusive(path: &Path, bytes: &[u8]) -> Result<(), StoreAuthError> {
use std::io::Write as _;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::AlreadyExists {
StoreAuthError::AlreadyInitialized {
path: path.display().to_string(),
}
} else {
StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
}
}
})?;
file.write_all(bytes).map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
})?;
file.sync_all().map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: error.to_string(),
})?;
Ok(())
}
fn write_replace(tmp: &Path, path: &Path, bytes: &[u8]) -> Result<(), StoreAuthError> {
use std::io::Write as _;
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(tmp).map_err(|error| StoreAuthError::Io {
path: tmp.display().to_string(),
message: error.to_string(),
})?;
file.write_all(bytes).map_err(|error| StoreAuthError::Io {
path: tmp.display().to_string(),
message: error.to_string(),
})?;
file.sync_all().map_err(|error| StoreAuthError::Io {
path: tmp.display().to_string(),
message: error.to_string(),
})?;
std::fs::rename(tmp, path).map_err(|error| StoreAuthError::Io {
path: path.display().to_string(),
message: format!("atomic replace: {error}"),
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn keys_dir() -> tempfile::TempDir {
tempfile::TempDir::new().expect("tempdir")
}
#[test]
fn encrypted_recovery_preserves_the_complete_rotation_window() {
let dir = keys_dir();
let mut root = StoreAuthRoot::create(dir.path()).expect("create");
let message = b"portable backup evidence";
let mut signatures = Vec::new();
for index in 0..=MAX_RETIRED_KEYS {
signatures.push((
root.current_key_id(),
root.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac"),
));
if index < MAX_RETIRED_KEYS {
root.rotate().expect("rotate");
}
}
let original = std::fs::read(dir.path().join(KEY_FILE_NAME)).expect("read source");
let passphrase = "synthetic recovery passphrase 123";
let encrypted = root.encrypted_recovery(passphrase).expect("encrypt");
let second = root.encrypted_recovery(passphrase).expect("encrypt again");
assert_ne!(encrypted, second, "independent salt and nonce");
let recovered = StoreAuthRoot::decrypt_recovery(&encrypted, passphrase).expect("decrypt");
let reopened =
StoreAuthRoot::from_serialized(dir.path(), &recovered.key_file).expect("parse");
assert_eq!(root.window_key_ids(), reopened.window_key_ids());
for (id, mac) in signatures {
assert!(matches!(
reopened
.verify_with_key(id, MacDomain::NativeImportRecordsRoot, message, &mac)
.expect("verify"),
KeyVerification::Match { .. }
));
}
assert_eq!(
original,
std::fs::read(dir.path().join(KEY_FILE_NAME)).expect("read unchanged")
);
let doc: KeyFileDoc = serde_json::from_slice(&original).expect("key doc");
let public_output = format!(
"{} {recovered:?} {doc:?}",
String::from_utf8_lossy(&encrypted)
);
assert!(!public_output.contains(passphrase));
for entry in std::iter::once(&doc.current).chain(&doc.retired) {
assert!(!public_output.contains(&entry.root), "root secret leaked");
}
}
#[test]
fn encrypted_recovery_rejects_wrong_password_tampering_and_unbounded_work() {
let dir = keys_dir();
let root = StoreAuthRoot::create(dir.path()).expect("create");
let passphrase = "synthetic recovery passphrase 123";
let bytes = root.encrypted_recovery(passphrase).expect("encrypt");
assert!(StoreAuthRoot::decrypt_recovery(&bytes, "different synthetic passphrase").is_err());
let envelope: StoreAuthRecoveryEnvelope = serde_json::from_slice(&bytes).expect("envelope");
for field in [
"ciphertext",
"nonce",
"salt",
"schema",
"kdf",
"iterations",
"empty",
] {
let mut changed = envelope.clone();
match field {
"ciphertext" => changed.ciphertext[0] ^= 1,
"nonce" => changed.nonce[0] ^= 1,
"salt" => changed.salt[0] ^= 1,
"schema" => changed.schema.push('x'),
"kdf" => changed.kdf.push('x'),
"iterations" => changed.iterations = u32::MAX,
_ => changed.ciphertext.clear(),
}
let changed = serde_json::to_vec(&changed).expect("changed envelope");
assert!(
StoreAuthRoot::decrypt_recovery(&changed, passphrase).is_err(),
"accepted changed {field}"
);
}
assert!(StoreAuthRoot::decrypt_recovery(&bytes[..bytes.len() / 2], passphrase).is_err());
assert!(
StoreAuthRoot::decrypt_recovery(&vec![b' '; MAX_RECOVERY_BYTES + 1], passphrase)
.is_err()
);
}
#[test]
fn encrypted_recovery_rejects_invalid_plaintext_and_passphrases() {
for passphrase in [
"",
"short",
"long enough but\nmultiline",
"long enough but\0nul",
] {
assert!(validate_recovery_passphrase(passphrase).is_err());
}
assert!(validate_recovery_passphrase(&"x".repeat(1025)).is_err());
assert!(validate_recovery_passphrase(&"🦀".repeat(1024)).is_ok());
let dir = keys_dir();
let root = StoreAuthRoot::create(dir.path()).expect("create");
let mut doc: serde_json::Value =
serde_json::from_slice(&root.serialize().expect("serialize")).expect("json");
doc["retired"] = serde_json::json!([doc["current"].clone()]);
let passphrase = "synthetic recovery passphrase 123";
let mut envelope: StoreAuthRecoveryEnvelope =
serde_json::from_slice(&root.encrypted_recovery(passphrase).expect("encrypt"))
.expect("envelope");
envelope.nonce = random_bytes().expect("fresh nonce");
envelope.ciphertext = serde_json::to_vec(&doc).expect("duplicate key payload");
recovery_cipher(passphrase, &envelope.salt)
.expect("cipher")
.seal_in_place_append_tag(
aead::Nonce::assume_unique_for_key(envelope.nonce),
aead::Aad::from(RECOVERY_SCHEMA.as_bytes()),
&mut envelope.ciphertext,
)
.expect("seal");
let invalid = serde_json::to_vec(&envelope).expect("invalid envelope");
let error = StoreAuthRoot::decrypt_recovery(&invalid, passphrase)
.expect_err("duplicate identifiers rejected");
assert!(
matches!(error, StoreAuthError::Malformed { message } if message == "decrypted recovery key file is invalid")
);
}
#[test]
fn primitive_known_answer_check_passes_against_reference_vectors() {
primitive_known_answer_check().expect("BLAKE3 wiring must match pinned b3sum vectors");
}
#[test]
fn create_then_open_round_trips_and_macs_are_stable() {
let dir = keys_dir();
let created = StoreAuthRoot::create(dir.path()).expect("create");
let message = b"records-root-digest";
let mac_before = created
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
drop(created);
let opened = StoreAuthRoot::open(dir.path()).expect("open");
let mac_after = opened
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
assert_eq!(mac_before, mac_after, "MAC must survive a reopen");
assert!(
opened
.verify(MacDomain::NativeImportRecordsRoot, message, &mac_before)
.expect("verify")
);
}
#[test]
fn open_or_create_is_idempotent() {
let dir = keys_dir();
let first = StoreAuthRoot::open_or_create(dir.path()).expect("first");
let id = first.current_key_id();
drop(first);
let second = StoreAuthRoot::open_or_create(dir.path()).expect("second");
assert_eq!(id, second.current_key_id(), "must adopt the existing root");
}
#[test]
fn read_guard_blocks_rotation_lock_until_the_transaction_finishes() {
let dir = keys_dir();
StoreAuthRoot::create(dir.path()).expect("create");
let guard = StoreAuthRoot::open_read_locked(dir.path()).expect("shared read lock");
let contender = open_key_lock_file(dir.path()).expect("rotation contender");
assert!(
matches!(
Fs4FileExt::try_lock(&contender),
Err(fs4::TryLockError::WouldBlock)
),
"rotation must not enter while an approval transaction holds the read guard"
);
drop(guard);
Fs4FileExt::try_lock(&contender)
.expect("rotation may enter after the approval transaction releases its guard");
Fs4FileExt::unlock(&contender).expect("unlock contender");
}
#[test]
fn create_twice_is_already_initialized() {
let dir = keys_dir();
StoreAuthRoot::create(dir.path()).expect("create");
let error = StoreAuthRoot::create(dir.path()).expect_err("second create must fail");
assert!(matches!(error, StoreAuthError::AlreadyInitialized { .. }));
}
#[test]
fn open_uninitialized_is_not_initialized() {
let dir = keys_dir();
let error = StoreAuthRoot::open(dir.path()).expect_err("open must fail");
assert!(matches!(error, StoreAuthError::NotInitialized { .. }));
}
#[test]
fn inaccessible_key_path_is_io_not_not_initialized() {
let dir = keys_dir();
let regular_file = dir.path().join("not-a-directory");
std::fs::write(®ular_file, b"occupied").expect("write regular-file path component");
let impossible_keys_dir = regular_file.join("keys");
for error in [
StoreAuthRoot::open(&impossible_keys_dir).expect_err("open must reject invalid path"),
StoreAuthRoot::open_or_create(&impossible_keys_dir)
.expect_err("open-or-create must reject invalid path"),
] {
assert!(
matches!(error, StoreAuthError::Io { .. }),
"an inaccessible key path is an I/O fault, not an absent store: {error:?}"
);
}
}
#[test]
fn distinct_domains_yield_distinct_macs() {
let dir = keys_dir();
let root = StoreAuthRoot::create(dir.path()).expect("create");
let message = b"same-bytes";
let native = root
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
let playbook = root
.mac(MacDomain::PlaybookImportRecordsRoot, message)
.expect("mac");
assert_ne!(
native, playbook,
"cross-domain MACs over identical bytes must differ"
);
}
#[test]
fn lane_mac_cannot_replay_in_the_reserved_body_approval_domain() {
let dir = keys_dir();
let root = StoreAuthRoot::create(dir.path()).expect("create");
let message = b"T1.4 body metadata lane snapshot";
let lane = root
.mac(MacDomain::LaneApprovalSnapshotTag, message)
.expect("lane MAC");
assert!(
root.verify(MacDomain::LaneApprovalSnapshotTag, message, &lane)
.expect("verify in lane domain"),
"the generic T1.4 lane domain must accept its own MAC"
);
assert!(
!root
.verify(MacDomain::BodyApprovalSnapshotTag, message, &lane)
.expect("verify in reserved body domain"),
"a T1.4 metadata-body lane MAC must not replay in T5.9's reserved domain"
);
}
#[test]
fn tampered_message_fails_verification() {
let dir = keys_dir();
let root = StoreAuthRoot::create(dir.path()).expect("create");
let mac = root
.mac(MacDomain::NativeImportRecordsRoot, b"authentic")
.expect("mac");
assert!(
!root
.verify(MacDomain::NativeImportRecordsRoot, b"tampered", &mac)
.expect("verify")
);
}
#[test]
fn two_stores_have_independent_roots() {
let dir_a = keys_dir();
let dir_b = keys_dir();
let root_a = StoreAuthRoot::create(dir_a.path()).expect("a");
let root_b = StoreAuthRoot::create(dir_b.path()).expect("b");
assert_ne!(root_a.current_key_id(), root_b.current_key_id());
let message = b"cross-store";
let mac_a = root_a
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
assert!(
!root_b
.verify(MacDomain::NativeImportRecordsRoot, message, &mac_a)
.expect("verify"),
"a foreign store's MAC must not verify"
);
}
#[test]
fn rotation_moves_prior_key_into_the_window() {
let dir = keys_dir();
let mut root = StoreAuthRoot::create(dir.path()).expect("create");
let old_id = root.current_key_id();
let message = b"pre-rotation";
let old_mac = root
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
let new_id = root.rotate().expect("rotate");
assert_ne!(old_id, new_id, "rotation must mint a new key id");
assert_eq!(new_id, root.current_key_id());
let verdict = root
.verify_with_key(
old_id,
MacDomain::NativeImportRecordsRoot,
message,
&old_mac,
)
.expect("verify");
assert_eq!(
verdict,
KeyVerification::Match {
key_class: KeyClass::Retired
}
);
let current_verdict = root
.verify_with_key(
new_id,
MacDomain::NativeImportRecordsRoot,
message,
&old_mac,
)
.expect("verify");
assert_eq!(current_verdict, KeyVerification::Mismatch);
}
#[test]
fn rotation_window_evicts_the_oldest_key() {
let dir = keys_dir();
let mut root = StoreAuthRoot::create(dir.path()).expect("create");
let oldest = root.current_key_id();
let message = b"windowed";
let oldest_mac = root
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
for _ in 0..(MAX_RETIRED_KEYS + 1) {
root.rotate().expect("rotate");
}
assert_eq!(root.window_key_ids().len(), MAX_RETIRED_KEYS + 1);
let verdict = root
.verify_with_key(
oldest,
MacDomain::NativeImportRecordsRoot,
message,
&oldest_mac,
)
.expect("verify");
assert_eq!(verdict, KeyVerification::KeyOutsideWindow);
}
#[test]
fn rotation_persists_and_reopens() {
let dir = keys_dir();
let mut root = StoreAuthRoot::create(dir.path()).expect("create");
let old_id = root.current_key_id();
let message = b"persisted-rotation";
let old_mac = root
.mac(MacDomain::NativeImportRecordsRoot, message)
.expect("mac");
let new_id = root.rotate().expect("rotate");
drop(root);
let reopened = StoreAuthRoot::open(dir.path()).expect("reopen");
assert_eq!(reopened.current_key_id(), new_id);
let verdict = reopened
.verify_with_key(
old_id,
MacDomain::NativeImportRecordsRoot,
message,
&old_mac,
)
.expect("verify");
assert_eq!(
verdict,
KeyVerification::Match {
key_class: KeyClass::Retired
}
);
}
#[test]
fn corrupted_root_fails_the_self_check() {
let dir = keys_dir();
StoreAuthRoot::create(dir.path()).expect("create");
let path = dir.path().join(KEY_FILE_NAME);
let raw = std::fs::read_to_string(&path).expect("read");
let mut doc: serde_json::Value = serde_json::from_str(&raw).expect("json");
doc["current"]["root"] = serde_json::Value::String("00".repeat(KEY_LEN));
std::fs::write(&path, doc.to_string()).expect("write");
let error = StoreAuthRoot::open(dir.path()).expect_err("self-check must fail");
assert_eq!(error, StoreAuthError::SelfCheckFailed);
}
#[test]
fn schema_mismatch_is_rejected() {
let dir = keys_dir();
StoreAuthRoot::create(dir.path()).expect("create");
let path = dir.path().join(KEY_FILE_NAME);
let raw = std::fs::read_to_string(&path).expect("read");
let mut doc: serde_json::Value = serde_json::from_str(&raw).expect("json");
doc["schema"] = serde_json::Value::String("ee.store_auth.keyfile.v0".to_owned());
std::fs::write(&path, doc.to_string()).expect("write");
let error = StoreAuthRoot::open(dir.path()).expect_err("schema must fail");
assert!(matches!(error, StoreAuthError::SchemaMismatch { .. }));
}
#[cfg(unix)]
#[test]
fn group_readable_key_file_is_insecure() {
use std::os::unix::fs::PermissionsExt;
let dir = keys_dir();
StoreAuthRoot::create(dir.path()).expect("create");
let path = dir.path().join(KEY_FILE_NAME);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).expect("chmod");
let error = StoreAuthRoot::open(dir.path()).expect_err("insecure perms must fail");
assert!(matches!(error, StoreAuthError::InsecurePermissions { .. }));
}
#[cfg(unix)]
#[test]
fn created_key_file_is_owner_only() {
use std::os::unix::fs::PermissionsExt;
let dir = keys_dir();
StoreAuthRoot::create(dir.path()).expect("create");
let path = dir.path().join(KEY_FILE_NAME);
let mode = std::fs::symlink_metadata(&path)
.expect("metadata")
.permissions()
.mode()
& 0o777;
assert_eq!(mode & 0o077, 0, "created key file must be owner-only");
}
#[test]
fn secret_debug_is_redacted_and_mac_debug_shows_hex() {
let secret = Secret([7_u8; KEY_LEN]);
assert_eq!(format!("{secret:?}"), "Secret(<redacted>)");
let mac = Mac([0xab_u8; MAC_LEN]);
assert!(format!("{mac:?}").contains(&"ab".repeat(MAC_LEN)));
assert_eq!(Mac::from_bytes(*mac.as_bytes()), mac);
}
#[test]
fn key_id_hex_round_trips() {
let id = KeyId([0x3c_u8; KEY_ID_LEN]);
let parsed = KeyId::from_hex(&id.to_hex()).expect("round trip");
assert_eq!(id, parsed);
assert!(KeyId::from_hex("zz").is_err());
}
#[test]
fn error_degraded_code_is_the_store_auth_code() {
let error = StoreAuthError::SelfCheckFailed;
assert_eq!(
error.degraded_code(),
MESH_STORE_AUTHENTICATION_UNAVAILABLE_CODE
);
assert!(!error.message().is_empty());
assert!(!error.repair().is_empty());
}
#[test]
fn constant_time_eq_matches_semantic_equality() {
assert!(constant_time_eq(b"abcd", b"abcd"));
assert!(!constant_time_eq(b"abcd", b"abce"));
assert!(!constant_time_eq(b"abc", b"abcd"));
}
}