use rand_core::{CryptoRng, RngCore};
use zeroize::Zeroizing;
use crate::cipher;
use crate::error::{KeystoreError, Result};
use crate::format::{
decode_file, encode_file, CipherId, KdfParams, KeystoreHeader, FORMAT_VERSION_V1,
};
use crate::kdf;
use crate::password::Password;
pub const MAGIC: [u8; 6] = *b"DIGOP1";
pub const SCHEME_ID: u16 = 0x0004;
pub fn seal(password: &Password, secret: &[u8], kdf_params: KdfParams) -> Result<Vec<u8>> {
seal_with_rng(password, secret, kdf_params, &mut rand_core::OsRng)
}
pub fn seal_with_rng<R: RngCore + CryptoRng>(
password: &Password,
secret: &[u8],
kdf_params: KdfParams,
rng: &mut R,
) -> Result<Vec<u8>> {
let mut salt = [0u8; 16];
let mut nonce = [0u8; 12];
rng.fill_bytes(&mut salt);
rng.fill_bytes(&mut nonce);
let mut header = KeystoreHeader {
magic: MAGIC,
format_version: FORMAT_VERSION_V1,
scheme_id: SCHEME_ID,
kdf: kdf_params,
cipher: CipherId::Aes256Gcm,
salt,
nonce,
payload_len: 0,
};
header.payload_len = (secret.len() + cipher::TAG_SIZE) as u32;
let enc_key = kdf::derive_key(password.as_bytes(), &header.salt, &header.kdf)?;
let header_bytes = header.encode();
let ciphertext_and_tag = cipher::encrypt(&enc_key, &header.nonce, secret, &header_bytes)?;
Ok(encode_file(&header, &ciphertext_and_tag))
}
pub fn open(password: &Password, blob: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
let (header, ciphertext_and_tag, header_bytes) = decode_file(blob)?;
if header.magic != MAGIC || header.scheme_id != SCHEME_ID {
return Err(KeystoreError::SchemeMismatch {
expected: SCHEME_ID,
expected_name: "Opaque",
found: header.scheme_id,
});
}
let key = kdf::derive_key(password.as_bytes(), &header.salt, &header.kdf)?;
cipher::decrypt(&key, &header.nonce, &ciphertext_and_tag, &header_bytes)
}
pub fn verify_password(password: &Password, blob: &[u8]) -> bool {
open(password, blob).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
fn fast_params() -> KdfParams {
KdfParams::FAST_TEST
}
#[test]
fn roundtrip_recovers_exact_secret_at_every_length() {
for len in [0usize, 5, 16, 20, 24, 28, 32, 100] {
let secret = vec![0xABu8; len];
let password = Password::from("correct horse battery staple");
let blob = seal(&password, &secret, fast_params()).unwrap();
let recovered = open(&password, &blob).unwrap();
assert_eq!(&*recovered, &secret[..], "length {len} failed to roundtrip");
}
}
#[test]
fn wrong_password_fails() {
let secret = b"top secret entropy".to_vec();
let blob = seal(&Password::from("right"), &secret, fast_params()).unwrap();
let err = open(&Password::from("wrong"), &blob).unwrap_err();
assert!(matches!(err, KeystoreError::DecryptFailed));
}
#[test]
fn tampered_ciphertext_fails() {
let secret = b"another secret".to_vec();
let password = Password::from("pw");
let mut blob = seal(&password, &secret, fast_params()).unwrap();
let i = blob.len() - 6;
blob[i] ^= 0xFF;
let err = open(&password, &blob).unwrap_err();
assert!(matches!(
err,
KeystoreError::DecryptFailed | KeystoreError::CrcMismatch { .. }
));
}
#[test]
fn verify_password_reports_correctly() {
let secret = b"seed material".to_vec();
let blob = seal(&Password::from("hunter2"), &secret, fast_params()).unwrap();
assert!(verify_password(&Password::from("hunter2"), &blob));
assert!(!verify_password(&Password::from("wrong"), &blob));
}
#[test]
fn typed_keystore_blob_is_rejected_as_opaque() {
use crate::backend::{BackendKey, KeychainBackend, MemoryBackend};
use crate::scheme::BlsSigning;
use std::sync::Arc;
let backend: Arc<dyn KeychainBackend> = Arc::new(MemoryBackend::new());
let password = Password::from("kat-password");
crate::Keystore::<BlsSigning>::create(
backend.clone(),
BackendKey::new("k"),
password.clone(),
None,
fast_params(),
)
.unwrap();
let raw = backend.read(&BackendKey::new("k")).unwrap();
let err = open(&password, &raw).unwrap_err();
assert!(matches!(err, KeystoreError::SchemeMismatch { .. }));
}
#[test]
fn deterministic_kat_matches_pinned_vector() {
let mut rng = ChaCha20Rng::seed_from_u64(KAT_SEED);
let blob = seal_with_rng(
&Password::from(KAT_PASSWORD),
KAT_SECRET,
KdfParams::FAST_TEST,
&mut rng,
)
.unwrap();
let got = hex::encode(&blob);
if KAT_HEX == "_REGENERATE_ME_ON_FIRST_RUN" {
panic!("KAT_HEX = \"{got}\";\n(paste into `src/opaque.rs` KAT_HEX and re-run)");
}
assert_eq!(got, KAT_HEX, "opaque KAT vector drifted");
}
pub(crate) const KAT_SEED: u64 = 0x4B_4159_5354_4F52; pub(crate) const KAT_PASSWORD: &str = "opaque-kat-password";
pub(crate) const KAT_SECRET: &[u8] = b"\x00\x01\x02\x03opaque-kat-secret-bytes\xFF\xFE";
pub(crate) const KAT_HEX: &str = "4449474f5031000100040100002000000000010101d2491ad536cec869b6d6174731645eeacabcde0420b2f2a8151f13900000002df6427db43857eb57c6c5606c5ed12abee298cdcdd56313136add9aada0b8cfb523b6fa6480fa8bcdfbdf78e63bbe815355";
}