#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use gunnar_crypt::keyring::Keyring;
use gunnar_crypt::members::Members;
use gunnar_crypt::{envelope, userkey, wrap};
const KEYRING: &str = include_str!("vectors/keyring.txt");
const CASES: &str = include_str!("vectors/cases.txt");
const TEST_KEY: &str = include_str!("vectors/test_ed25519");
const KEYS_FILE: &str = include_str!("vectors/keys.age");
const TEST_USER_KEY: &str = include_str!("vectors/test_user_key");
const USER_KEY_SSH: &str = include_str!("vectors/user-key-ssh.age");
const USER_KEY_SK: &str = include_str!("vectors/user-key-security-key.age");
const TEST_PRF: [u8; 32] = [0x5A; 32];
const TEST_CREDENTIAL: &[u8] = b"gunnar-crypt test credential";
const DIFF_OLD: &str = include_str!("vectors/diff_old.txt");
const DIFF_NEW: &str = include_str!("vectors/diff_new.txt");
const DIFF_UNIFIED: &str = include_str!("vectors/diff_unified.txt");
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
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 cases() -> Vec<(String, Vec<u8>, Vec<u8>)> {
CASES
.lines()
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
.map(|l| {
let mut p = l.split(' ');
let name = p.next().unwrap().to_string();
let plain = B64.decode(p.next().unwrap()).unwrap();
let env = unhex(p.next().unwrap());
(name, plain, env)
})
.collect()
}
fn ring() -> Keyring {
Keyring::parse(KEYRING).unwrap()
}
#[test]
fn the_vector_envelopes_open_on_this_target() {
let ring = ring();
for (name, plain, env) in cases() {
assert_eq!(envelope::open(&ring, &env).unwrap(), plain, "case {name}");
}
}
#[test]
fn sealing_v1_on_this_target_gives_the_v1_vector_bytes() {
let ring = ring();
for (name, plain, env) in cases() {
let epoch = envelope::epoch_of(&env).unwrap();
let sealed = envelope::seal_v1(ring.get(epoch).unwrap(), &plain);
assert_eq!(hex(&sealed), hex(&env), "case {name}");
}
}
fn user_public() -> String {
test_user_key().public()
}
fn test_user_key() -> userkey::UserKey {
userkey::UserKey::X25519(TEST_USER_KEY.trim().parse().unwrap())
}
fn members() -> Members {
let mut m = Members::at(gunnar_crypt::members::Level::Classic);
m.add("vector", &user_public()).unwrap();
m
}
#[test]
fn the_vector_key_file_unwraps_on_this_target() {
let got = wrap::unwrap_checked(
KEYS_FILE.as_bytes(),
&members().to_text(),
&[test_user_key().identity()],
)
.unwrap();
assert_eq!(got, ring());
}
#[test]
fn the_vector_user_key_opens_with_the_security_key_and_then_the_ring() {
let id = userkey::security_key::identity(&TEST_PRF, TEST_CREDENTIAL);
let user = userkey::open(USER_KEY_SK.as_bytes(), &id, &user_public()).unwrap();
let got = wrap::unwrap_checked(
KEYS_FILE.as_bytes(),
&members().to_text(),
&[user.identity()],
)
.unwrap();
assert_eq!(got, ring());
}
#[test]
fn the_vector_user_key_opens_with_the_ssh_key_file() {
let id = wrap::ssh_identity(TEST_KEY.as_bytes(), None, wrap::NoPassphrase).unwrap();
let user = userkey::open(USER_KEY_SSH.as_bytes(), id.as_ref(), &user_public()).unwrap();
assert_eq!(user.public(), user_public());
}
#[test]
fn a_key_file_wrapped_on_this_target_unwraps_and_is_carried_across() {
let file = wrap::wrap(&ring(), &members()).unwrap();
assert_eq!(
wrap::unwrap(file.as_bytes(), &[test_user_key().identity()]).unwrap(),
ring()
);
let sk = test_user_key()
.wrap_to_security_key(&userkey::security_key::recipient(
&TEST_PRF,
TEST_CREDENTIAL,
))
.unwrap();
let line = format!("GUNNAR-CRYPT-CROSS {}", B64.encode(file.as_bytes()));
let sk_line = format!("GUNNAR-CRYPT-CROSS-USER-KEY {}", B64.encode(sk.as_bytes()));
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_test::console_log!("{line}");
wasm_bindgen_test::console_log!("{sk_line}");
}
#[cfg(not(target_arch = "wasm32"))]
let _ = (line, sk_line);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn a_key_file_wrapped_by_wasm_unwraps_natively() {
let Ok(path) = std::env::var("GUNNAR_CRYPT_WASM_WRAPPED") else {
eprintln!("not under tests/wasm-cross.sh: nothing carried over from wasm");
return;
};
let carried = std::fs::read_to_string(path).unwrap();
let mut lines = carried.lines();
let file = B64.decode(lines.next().unwrap().trim()).unwrap();
assert_eq!(
wrap::unwrap(&file, &[test_user_key().identity()]).unwrap(),
ring()
);
let sk = B64
.decode(lines.next().expect("the wasm user-key wrap").trim())
.unwrap();
let id = userkey::security_key::identity(&TEST_PRF, TEST_CREDENTIAL);
assert_eq!(
userkey::open(&sk, &id, &user_public()).unwrap().public(),
user_public()
);
eprintln!("CROSS-OK: a key file and a user-key wrap made by the wasm32 build opened natively");
}
#[test]
fn the_diff_of_two_sealed_versions_is_the_vector_diff() {
let ring = ring();
let key = ring.newest();
let old = envelope::seal(key, DIFF_OLD.as_bytes());
let new = envelope::seal(key, DIFF_NEW.as_bytes());
let d = gunnar_crypt::diff_blobs(&ring, Some(&old), Some(&new)).unwrap();
assert_eq!(gunnar_crypt::diff::unified(&d), DIFF_UNIFIED);
}
#[test]
fn an_unknown_epoch_is_named_on_this_target() {
let ring = ring();
let removed = ring.truncated(1);
let (_, _, env) = cases()
.into_iter()
.find(|(_, _, e)| envelope::epoch_of(e) == Some(2))
.expect("a case sealed under epoch 2");
assert_eq!(
envelope::open(&removed, &env),
Err(envelope::OpenError::UnknownEpoch(2))
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
#[ignore]
fn write_vectors() {
use gunnar_crypt::keyring::{DataKey, IdKey};
if std::env::var("GUNNAR_CRYPT_WRITE_VECTORS").is_err() {
return;
}
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors");
let id: [u8; 32] = std::array::from_fn(|i| i as u8);
let k1: [u8; 64] = std::array::from_fn(|i| (i * 3 + 1) as u8);
let k2: [u8; 64] = std::array::from_fn(|i| (0x80 + i) as u8);
let mut ring = Keyring::new(IdKey::from_bytes(id), DataKey::from_bytes(1, k1));
let text = format!("{}epoch 2 {}\n", ring.to_text().as_str(), B64.encode(k2));
ring = Keyring::parse(&text).unwrap();
std::fs::write(dir.join("keyring.txt"), ring.to_text().as_bytes()).unwrap();
let plains: [(&str, u32, &[u8]); 5] = [
("empty", 1, b""),
("hello", 1, b"hello, world\n"),
("binary", 1, &[0u8, 255, 1, 254, 0, 0, 7]),
("utf8", 2, "räksmörgås ✓\n".as_bytes()),
("long", 2, &[b'x'; 4099]),
];
let mut cases = String::from("# name plaintext(base64) envelope(hex) — TEST KEYS ONLY\n");
for (name, epoch, plain) in plains {
let env = envelope::seal_v1(ring.get(epoch).unwrap(), plain);
cases.push_str(&format!("{name} {} {}\n", B64.encode(plain), hex(&env)));
}
std::fs::write(dir.join("cases.txt"), cases).unwrap();
let user = userkey::UserKey::X25519(userkey::security_key::identity(
&[0x11; 32],
b"gunnar-crypt test user key",
));
std::fs::write(
dir.join("test_user_key"),
format!("{}\n", age_secret(&user)),
)
.unwrap();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members.add("vector", &user.public()).unwrap();
std::fs::write(dir.join("keys.age"), wrap::wrap(&ring, &members).unwrap()).unwrap();
let ssh_pub = std::fs::read_to_string(dir.join("test_ed25519.pub")).unwrap();
std::fs::write(
dir.join("user-key-ssh.age"),
user.wrap_to_ssh(ssh_pub.trim()).unwrap(),
)
.unwrap();
std::fs::write(
dir.join("user-key-security-key.age"),
user.wrap_to_security_key(&userkey::security_key::recipient(
&TEST_PRF,
TEST_CREDENTIAL,
))
.unwrap(),
)
.unwrap();
let d = gunnar_crypt::diff::hunks(
&std::fs::read_to_string(dir.join("diff_old.txt")).unwrap(),
&std::fs::read_to_string(dir.join("diff_new.txt")).unwrap(),
);
std::fs::write(
dir.join("diff_unified.txt"),
gunnar_crypt::diff::unified(&d),
)
.unwrap();
}
#[cfg(not(target_arch = "wasm32"))]
fn age_secret(user: &userkey::UserKey) -> String {
use gunnar_crypt::wrap::age::secrecy::ExposeSecret as _;
match user {
userkey::UserKey::X25519(id) => id.to_string().expose_secret().to_string(),
_ => unreachable!("the TEST user key is X25519"),
}
}