#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use gunnar_crypt::keyring::Keyring;
use gunnar_crypt::members::{Level, Members, MembersError};
use gunnar_crypt::userkey::{self, recovery_code, security_key, UserKey, UserKeyError};
use gunnar_crypt::wrap::{self, UnwrapError};
#[cfg(not(target_arch = "wasm32"))]
const TEST_KEY: &str = include_str!("vectors/test_ed25519");
const TEST_KEY_PUB: &str = include_str!("vectors/test_ed25519.pub");
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn prf(fill: u8) -> [u8; 32] {
[fill; 32]
}
#[test]
fn a_security_key_gives_the_vector_recipient_on_this_target() {
let p: [u8; 32] = std::array::from_fn(|i| i as u8);
let cred: Vec<u8> = (1..=16).collect();
assert_eq!(
security_key::recipient(&p, &cred),
"age163l93dqcd6xl9yc689yhcmmhdjva74a5haahfj30khnlc7j9qy9str6pg0"
);
let cred: Vec<u8> = (0..64).collect();
assert_eq!(
security_key::recipient(&prf(0xA5), &cred),
"age1zc0xvt6zgy283sz0dpjfculu3stwkc4u4hvmd8gde0h7pmh4g5wqkweyay"
);
assert_eq!(
security_key::recipient(&prf(0xA5), &cred),
security_key::recipient(&prf(0xA5), &cred),
"fixed per credential"
);
assert_ne!(
security_key::recipient(&prf(0xA5), &cred),
security_key::recipient(&prf(0xA5), &cred[..63]),
"the credential id is context"
);
assert_eq!(
security_key::hmac_secret_salt().to_vec(),
unhex("fdac5f81e153833ee47bdb6ddbd5e912c26d2e3ad614d73295f23efc611d5e3b"),
"the CTAP2 salt is WebAuthn's PRF salt, so the CLI and the browser get one secret"
);
}
#[test]
fn a_user_key_opens_with_its_security_key_and_not_with_another() {
let user = UserKey::generate(Level::Classic).unwrap();
let cred = b"credential-1";
let wrap = user
.wrap_to_security_key(&security_key::recipient(&prf(7), cred))
.unwrap();
let opened = userkey::open(
wrap.as_bytes(),
&security_key::identity(&prf(7), cred),
&user.public(),
)
.unwrap();
assert_eq!(opened.public(), user.public());
let other = userkey::open(
wrap.as_bytes(),
&security_key::identity(&prf(8), cred),
&user.public(),
);
assert_eq!(other.unwrap_err(), UserKeyError::WrongKey);
let other_cred = userkey::open(
wrap.as_bytes(),
&security_key::identity(&prf(7), b"credential-2"),
&user.public(),
);
assert_eq!(other_cred.unwrap_err(), UserKeyError::WrongKey);
assert!(UserKeyError::WrongKey
.to_string()
.contains("not one of your unlock methods"));
}
#[test]
fn a_substituted_wrap_is_refused() {
let user = UserKey::generate(Level::Classic).unwrap();
let planted = UserKey::generate(Level::Classic).unwrap();
let wrap = planted
.wrap_to_security_key(&security_key::recipient(&prf(1), b"c"))
.unwrap();
let r = userkey::open(
wrap.as_bytes(),
&security_key::identity(&prf(1), b"c"),
&user.public(),
);
assert_eq!(r.unwrap_err(), UserKeyError::NotThisAccountsKey);
}
#[test]
fn any_method_opens_every_ring_and_adding_one_touches_no_repository() {
let user = UserKey::generate(Level::Classic).unwrap();
let ring = Keyring::generate();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members.add("alice", &user.public()).unwrap();
let keys = wrap::wrap(&ring, &members).unwrap();
let keys_before = keys.clone();
let by_key_one = user
.wrap_to_security_key(&security_key::recipient(&prf(1), b"one"))
.unwrap();
let open_one = userkey::open(
by_key_one.as_bytes(),
&security_key::identity(&prf(1), b"one"),
&user.public(),
)
.unwrap();
let by_key_two = open_one
.wrap_to_security_key(&security_key::recipient(&prf(2), b"two"))
.unwrap();
for (w, id) in [
(&by_key_one, security_key::identity(&prf(1), b"one")),
(&by_key_two, security_key::identity(&prf(2), b"two")),
] {
let k = userkey::open(w.as_bytes(), &id, &user.public()).unwrap();
let got =
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[k.identity()]).unwrap();
assert_eq!(got, ring);
}
assert_eq!(keys, keys_before, "no repository was touched");
}
#[test]
fn a_member_is_added_without_being_present() {
let alice = UserKey::generate(Level::Classic).unwrap();
let bob = UserKey::generate(Level::Classic).unwrap();
let bob_public = bob.public();
let ring = Keyring::generate();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members.add("alice", &alice.public()).unwrap();
members.add("bob", &bob_public).unwrap();
let keys = wrap::wrap(&ring, &members).unwrap();
let bob_wrap = bob
.wrap_to_recovery_code(&recovery_code::from_bytes(&[3; 20]))
.unwrap();
drop(bob);
let bob = userkey::open_with_recovery_code(
bob_wrap.as_bytes(),
&recovery_code::from_bytes(&[3; 20]),
&bob_public,
)
.unwrap();
assert_eq!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[bob.identity()]).unwrap(),
ring
);
let stranger = UserKey::generate(Level::Classic).unwrap();
assert!(matches!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[stranger.identity()]),
Err(UnwrapError::NotARecipient)
));
}
#[test]
fn an_ssh_key_is_refused_as_a_member_by_name() {
let mut m = Members::at(gunnar_crypt::members::Level::Classic);
let e = m.add("alice", TEST_KEY_PUB.trim()).unwrap_err();
assert_eq!(
e,
MembersError::SshKeyIsAMethod {
name: "alice".into()
}
);
assert!(e.to_string().contains("encryption key"), "{e}");
let e = m
.add(
"alice",
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t",
)
.unwrap_err();
assert_eq!(
e,
MembersError::SshKeyIsAMethod {
name: "alice".into()
}
);
}
#[test]
fn a_recovery_code_opens_and_a_wrong_one_is_named() {
let code = recovery_code::from_bytes(&[0xFF; 20]);
assert_eq!(code.len(), 32 + 7);
assert_eq!(
&*recovery_code::normalize(&code.to_lowercase().replace('-', " ")).unwrap(),
&code.replace('-', "")
);
let user = UserKey::generate(Level::Classic).unwrap();
let wrap = user.wrap_to_recovery_code(&code).unwrap();
assert!(userkey::open_with_recovery_code(wrap.as_bytes(), &code, &user.public()).is_ok());
let wrong = recovery_code::from_bytes(&[0xFE; 20]);
assert_eq!(
userkey::open_with_recovery_code(wrap.as_bytes(), &wrong, &user.public()).unwrap_err(),
UserKeyError::WrongKey
);
assert_eq!(
userkey::open_with_recovery_code(wrap.as_bytes(), "1234", &user.public()).unwrap_err(),
UserKeyError::BadRecoveryCode
);
}
#[test]
fn a_wrap_that_holds_no_user_key_is_refused() {
let id = security_key::identity(&prf(9), b"c");
let mut out = Vec::new();
{
use std::io::Write as _;
let r = id.to_public();
let enc = gunnar_crypt::wrap::age::Encryptor::with_recipients(std::iter::once(
&r as &dyn gunnar_crypt::wrap::age::Recipient,
))
.unwrap();
let mut w = enc.wrap_output(&mut out).unwrap();
w.write_all(b"gunnar-crypt keyring v1\n").unwrap();
w.finish().unwrap();
}
assert_eq!(
userkey::open(&out, &id, "age1x").unwrap_err(),
UserKeyError::NotAUserKey
);
assert!(matches!(
userkey::open(b"not age", &id, "age1x"),
Err(UserKeyError::Malformed(_))
));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn the_ssh_key_file_opens_the_user_key_and_every_ring() {
let user = UserKey::generate(Level::Classic).unwrap();
let w = user.wrap_to_ssh(TEST_KEY_PUB).unwrap();
assert!(gunnar_crypt::policy::age_stanzas(w.as_bytes())
.unwrap()
.iter()
.any(|s| s.tag == "ssh-ed25519"));
let ssh = wrap::ssh_identity(TEST_KEY.as_bytes(), None, wrap::NoPassphrase).unwrap();
let k = userkey::open(w.as_bytes(), ssh.as_ref(), &user.public()).unwrap();
let ring = Keyring::generate();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members.add("alice", &user.public()).unwrap();
let keys = wrap::wrap(&ring, &members).unwrap();
assert_eq!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[k.identity()]).unwrap(),
ring
);
let stanzas = gunnar_crypt::policy::age_stanzas(keys.as_bytes()).unwrap();
assert!(
stanzas
.iter()
.all(|s| s.tag == "X25519" || s.tag.ends_with("-grease")),
"the ring names no SSH key: {stanzas:?}"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn a_quantum_safe_user_key_opens_a_quantum_safe_ring() {
let user = UserKey::generate(Level::QuantumSafe).unwrap();
assert!(user.public().starts_with("age1pq1"));
let w = user.wrap_to_ssh(TEST_KEY_PUB).unwrap();
let ssh = wrap::ssh_identity(TEST_KEY.as_bytes(), None, wrap::NoPassphrase).unwrap();
let k = userkey::open(w.as_bytes(), ssh.as_ref(), &user.public()).unwrap();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members.set_level(Level::QuantumSafe).unwrap();
members.add("alice", &k.public()).unwrap();
let ring = Keyring::generate();
let keys = wrap::wrap(&ring, &members).unwrap();
assert_eq!(
wrap::unwrap_checked(keys.as_bytes(), &members.to_text(), &[k.identity()]).unwrap(),
ring
);
}