#![cfg(not(target_arch = "wasm32"))]
use gunnar_crypt::keyring::Keyring;
use gunnar_crypt::members::{Level, Members};
use gunnar_crypt::userkey::{self, UserKey, UserKeyError};
use gunnar_crypt::wrap::{self, NoPassphrase, Passphrase, UnwrapError};
use std::process::Command;
fn keygen(dir: &std::path::Path, name: &str, ty: &str, pass: &str) -> (Vec<u8>, String) {
let path = dir.join(name);
let ok = Command::new("ssh-keygen")
.args(["-q", "-t", ty, "-N", pass, "-C", name, "-f"])
.arg(&path)
.status()
.expect("ssh-keygen runs");
assert!(ok.success());
let pem = std::fs::read(&path).unwrap();
let public = std::fs::read_to_string(path.with_extension("pub")).unwrap();
(pem, public.trim().to_string())
}
fn have_ssh_keygen() -> bool {
Command::new("ssh-keygen").arg("-?").output().is_ok()
}
#[test]
fn members_unwrap_and_a_non_member_cannot() {
if !have_ssh_keygen() {
eprintln!("skipped: no ssh-keygen");
return;
}
let dir = tempfile::tempdir().unwrap();
let (alice_pem, alice_pub) = keygen(dir.path(), "alice", "ed25519", "");
let (bob_pem, bob_pub) = keygen(dir.path(), "bob", "rsa", "");
let (carol_pem, _) = keygen(dir.path(), "carol", "ed25519", "");
let (alice, bob) = (
UserKey::generate(Level::Classic).unwrap(),
UserKey::generate(Level::Classic).unwrap(),
);
let alice_wrap = alice.wrap_to_ssh(&alice_pub).unwrap();
let bob_wrap = bob.wrap_to_ssh(&bob_pub).unwrap();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members.add("alice", &alice.public()).unwrap();
members.add("bob", &bob.public()).unwrap();
let ring = Keyring::generate();
let file = wrap::wrap(&ring, &members).unwrap();
assert!(file.starts_with("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(gunnar_crypt::policy::is_age_file(file.as_bytes()));
for (pem, w, public) in [
(&alice_pem, &alice_wrap, alice.public()),
(&bob_pem, &bob_wrap, bob.public()),
] {
let id = wrap::ssh_identity(pem, None, NoPassphrase).unwrap();
let user = userkey::open(w.as_bytes(), id.as_ref(), &public).unwrap();
assert_eq!(
wrap::unwrap_checked(file.as_bytes(), &members.to_text(), &[user.identity()]).unwrap(),
ring
);
}
let carol = wrap::ssh_identity(&carol_pem, None, NoPassphrase).unwrap();
assert_eq!(
userkey::open(alice_wrap.as_bytes(), carol.as_ref(), &alice.public()).unwrap_err(),
UserKeyError::WrongKey
);
let carol_user = UserKey::generate(Level::Classic).unwrap();
assert!(matches!(
wrap::unwrap(file.as_bytes(), &[carol_user.identity()]),
Err(UnwrapError::NotARecipient)
));
}
#[derive(Clone)]
struct Fixed(&'static str);
impl Passphrase for Fixed {
fn ask(&self, _: &str) -> Option<String> {
Some(self.0.to_string())
}
}
#[test]
fn a_passphrase_protected_key_asks_for_its_passphrase() {
if !have_ssh_keygen() {
return;
}
let dir = tempfile::tempdir().unwrap();
let (pem, public) = keygen(dir.path(), "dana", "ed25519", "correct horse");
let user = UserKey::generate(Level::Classic).unwrap();
let file = user.wrap_to_ssh(&public).unwrap();
let without = wrap::ssh_identity(&pem, None, NoPassphrase).unwrap();
assert!(userkey::open(file.as_bytes(), without.as_ref(), &user.public()).is_err());
let with = wrap::ssh_identity(&pem, None, Fixed("correct horse")).unwrap();
assert_eq!(
userkey::open(file.as_bytes(), with.as_ref(), &user.public())
.unwrap()
.public(),
user.public()
);
}
#[test]
fn an_age_file_that_is_not_a_key_ring_is_never_released() {
let user = age::x25519::Identity::generate();
let recipient = user.to_public();
let enc = age::Encryptor::with_recipients(std::iter::once(&recipient as &dyn age::Recipient))
.unwrap();
let mut out = Vec::new();
let mut w = enc.wrap_output(&mut out).unwrap();
std::io::Write::write_all(&mut w, b"DATABASE_PASSWORD=hunter2\n").unwrap();
w.finish().unwrap();
let err = wrap::unwrap(&out, &[&user]).unwrap_err();
assert!(matches!(err, UnwrapError::NotAKeyring(_)), "{err}");
assert!(!err.to_string().contains("hunter2"));
}
#[test]
fn a_recovery_age_key_is_a_recipient_too() {
let recovery = age::x25519::Identity::generate();
let mut members = Members::at(gunnar_crypt::members::Level::Classic);
members
.add("recovery", &recovery.to_public().to_string())
.unwrap();
let ring = Keyring::generate();
let file = wrap::wrap(&ring, &members).unwrap();
assert_eq!(wrap::unwrap(file.as_bytes(), &[&recovery]).unwrap(), ring);
}