Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
use lockstore_rs::{LockstoreError, LockstoreKeystore, KEK_REF_LOCAL, KEK_REF_PRP};
use std::thread::sleep;
use std::time::Duration;
use tempfile::tempdir;
const PW: &[u8] = b"correct horse battery staple";
const PW_WRONG: &[u8] = b"Tr0ub4dor&3";
const PW_NEW: &[u8] = b"gs5^&mR2!fb@1";
fn on_disk_keystore() -> (LockstoreKeystore, tempfile::TempDir) {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("lockstore.keys.sqlite");
let ks = LockstoreKeystore::new(path).expect("new");
(ks, dir)
}
#[test]
fn has_and_init_prp() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
assert!(!ks.has_prp());
assert!(!ks.is_prp_unlocked());
ks.set_prp_test_only(None, PW).expect("set");
assert!(ks.has_prp());
assert!(!ks.is_prp_unlocked());
}
#[test]
fn set_without_old_when_already_initialized_fails() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
let err = ks.set_prp_test_only(None, PW_NEW).unwrap_err();
assert!(matches!(err, LockstoreError::InvalidConfiguration(_)));
}
#[test]
fn unlock_then_get_dek_succeeds() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60)).expect("unlock");
assert!(ks.is_prp_unlocked());
ks.create_dek("col", KEK_REF_PRP, true).expect("create_dek");
let (dek, _cs) = ks.get_dek("col", KEK_REF_PRP).expect("get_dek");
assert_eq!(dek.len(), 32);
}
#[test]
fn get_dek_when_locked_fails() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60)).expect("unlock");
ks.create_dek("col", KEK_REF_PRP, true).expect("create_dek");
ks.lock_prp();
assert!(!ks.is_prp_unlocked());
let err = ks.get_dek("col", KEK_REF_PRP).unwrap_err();
assert!(matches!(err, LockstoreError::Locked), "got: {:?}", err);
}
#[test]
fn unlock_expires_after_timeout() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.create_dek("col", KEK_REF_LOCAL, true)
.expect("create_dek");
ks.unlock_prp(PW, Duration::from_millis(100))
.expect("unlock");
ks.add_kek("col", KEK_REF_LOCAL, KEK_REF_PRP)
.expect("add PrP level");
sleep(Duration::from_millis(200));
assert!(!ks.is_prp_unlocked());
let err = ks.get_dek("col", KEK_REF_PRP).unwrap_err();
assert!(matches!(err, LockstoreError::Locked), "got: {:?}", err);
// LocalKey access still works after PrP expires.
ks.get_dek("col", KEK_REF_LOCAL).expect("local still ok");
}
#[test]
fn wrong_password_returns_wrong_password_and_does_not_cache() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
let err = ks
.unlock_prp(PW_WRONG, Duration::from_secs(60))
.unwrap_err();
assert!(matches!(err, LockstoreError::WrongPassword));
assert!(!ks.is_prp_unlocked());
}
#[test]
fn unlock_before_init_returns_not_initialized() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
let err = ks.unlock_prp(PW, Duration::from_secs(60)).unwrap_err();
assert!(matches!(err, LockstoreError::NotInitialized));
}
#[test]
fn change_prp_rewraps_deks() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60))
.expect("unlock old");
ks.create_dek("c1", KEK_REF_PRP, true).expect("create c1");
let (dek_before, _) = ks.get_dek("c1", KEK_REF_PRP).expect("get c1");
ks.set_prp_test_only(Some(PW), PW_NEW).expect("change");
assert!(!ks.is_prp_unlocked());
// Old password no longer works.
assert!(matches!(
ks.unlock_prp(PW, Duration::from_secs(60)),
Err(LockstoreError::WrongPassword)
));
// New password works and the DEK is unchanged (rewrap preserves DEK bytes).
ks.unlock_prp(PW_NEW, Duration::from_secs(60))
.expect("unlock new");
let (dek_after, _) = ks.get_dek("c1", KEK_REF_PRP).expect("get c1 again");
assert_eq!(dek_before, dek_after);
}
#[test]
fn change_with_wrong_old_password_rejected() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
let err = ks.set_prp_test_only(Some(PW_WRONG), PW_NEW).unwrap_err();
assert!(matches!(err, LockstoreError::WrongPassword));
ks.unlock_prp(PW, Duration::from_secs(60))
.expect("unlock with old still works");
}
#[test]
fn add_then_remove_local_level_leaves_prp_only() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60)).expect("unlock");
ks.create_dek("c", KEK_REF_LOCAL, true).expect("create");
ks.add_kek("c", KEK_REF_LOCAL, KEK_REF_PRP).expect("add");
ks.remove_kek("c", KEK_REF_LOCAL).expect("remove");
let err = ks.get_dek("c", KEK_REF_LOCAL).unwrap_err();
assert!(matches!(err, LockstoreError::NotFound(_)));
ks.get_dek("c", KEK_REF_PRP).expect("prp ok");
ks.lock_prp();
let err = ks.get_dek("c", KEK_REF_PRP).unwrap_err();
assert!(matches!(err, LockstoreError::Locked));
}
#[test]
fn reopen_on_disk_keystore_requires_unlock() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("lockstore.keys.sqlite");
{
let ks = LockstoreKeystore::new(path.clone()).expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60)).expect("unlock");
ks.create_dek("persisted", KEK_REF_PRP, true)
.expect("create");
ks.close();
}
let ks2 = LockstoreKeystore::new(path).expect("reopen");
assert!(ks2.has_prp());
assert!(!ks2.is_prp_unlocked());
let err = ks2.get_dek("persisted", KEK_REF_PRP).unwrap_err();
assert!(matches!(err, LockstoreError::Locked));
ks2.unlock_prp(PW, Duration::from_secs(60))
.expect("unlock reopened");
ks2.get_dek("persisted", KEK_REF_PRP)
.expect("get after reopen+unlock");
}
#[test]
fn close_locks_prp() {
let (ks, _dir) = on_disk_keystore();
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(3600))
.expect("unlock");
assert!(ks.is_prp_unlocked());
ks.close();
// A fresh keystore at the same path must not inherit the unlocked state
// (covered by `reopen_on_disk_keystore_requires_unlock`).
}
#[test]
fn encrypt_decrypt_roundtrip_local() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.create_dek("c", KEK_REF_LOCAL, false).expect("create");
let plaintext = b"hello, lockstore";
let blob = ks.encrypt("c", KEK_REF_LOCAL, plaintext).expect("encrypt");
assert_ne!(blob, plaintext);
let round = ks.decrypt("c", KEK_REF_LOCAL, &blob).expect("decrypt");
assert_eq!(round, plaintext);
}
#[test]
fn encrypt_decrypt_roundtrip_prp() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60)).expect("unlock");
ks.create_dek("c", KEK_REF_PRP, false).expect("create");
let plaintext = b"secret";
let blob = ks.encrypt("c", KEK_REF_PRP, plaintext).expect("encrypt");
let round = ks.decrypt("c", KEK_REF_PRP, &blob).expect("decrypt");
assert_eq!(round, plaintext);
ks.lock_prp();
let err = ks.encrypt("c", KEK_REF_PRP, plaintext).unwrap_err();
assert!(matches!(err, LockstoreError::Locked));
let err = ks.decrypt("c", KEK_REF_PRP, &blob).unwrap_err();
assert!(matches!(err, LockstoreError::Locked));
}
#[test]
fn encrypt_non_extractable_dek_still_works() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.create_dek("c", KEK_REF_LOCAL, false).expect("create");
// get_dek rejects because not extractable...
let err = ks.get_dek("c", KEK_REF_LOCAL).unwrap_err();
assert!(matches!(err, LockstoreError::NotExtractable(_)));
// ...but encrypt/decrypt bypass the extractability gate.
let blob = ks.encrypt("c", KEK_REF_LOCAL, b"abc").expect("encrypt");
let round = ks.decrypt("c", KEK_REF_LOCAL, &blob).expect("decrypt");
assert_eq!(round, b"abc");
}
#[test]
fn prp_dek_supports_non_extractable() {
let ks = LockstoreKeystore::new_in_memory().expect("new");
ks.set_prp_test_only(None, PW).expect("set");
ks.unlock_prp(PW, Duration::from_secs(60)).expect("unlock");
ks.create_dek("nonex", KEK_REF_PRP, false)
.expect("create_dek non-extractable");
let err = ks.get_dek("nonex", KEK_REF_PRP).unwrap_err();
assert!(
matches!(err, LockstoreError::NotExtractable(_)),
"expected NotExtractable, got {:?}",
err
);
let pt = b"PrP-bound payload";
let ct = ks
.encrypt("nonex", KEK_REF_PRP, pt)
.expect("encrypt under non-extractable PrP DEK");
let pt2 = ks
.decrypt("nonex", KEK_REF_PRP, &ct)
.expect("decrypt under non-extractable PrP DEK");
assert_eq!(pt2, pt);
}