onionwire/tests/backup.rs

136 lines
4.1 KiB
Rust
Raw Normal View History

//! Encrypted identity backup: owbak1 || salt[16] || nonce[12] || ciphertext.
use onionwire::Store;
use onionwire::backup::{self, BackupKeys};
use onionwire::tui::{
BackupDecision, BackupPrompt, SlashCmd, backup_screen_text, parse_cmd, restore_screen_text,
};
fn keys(tag: u8) -> BackupKeys {
BackupKeys {
identity_sk: [tag; 32],
identity_pk: [tag.wrapping_add(1); 32],
prekey_sk: [tag.wrapping_add(2); 32],
prekey_pk: [tag.wrapping_add(3); 32],
}
}
#[test]
fn roundtrip_keys() {
let k = keys(7);
let blob = backup::seal("correct horse", &k).expect("seal");
assert!(blob.starts_with(b"owbak1"), "magic");
assert_eq!(&blob[0..6], b"owbak1");
let opened = backup::open("correct horse", &blob).expect("open");
assert_eq!(opened.identity_sk, k.identity_sk);
assert_eq!(opened.identity_pk, k.identity_pk);
assert_eq!(opened.prekey_sk, k.prekey_sk);
assert_eq!(opened.prekey_pk, k.prekey_pk);
}
#[test]
fn onion_is_not_in_backup() {
let k = keys(3);
let blob = backup::seal("pw", &k).unwrap();
let onion = b"abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopq.onion";
assert!(
!blob.windows(onion.len()).any(|w| w == onion),
"locator must not be in the backup file"
);
}
#[test]
fn wrong_passphrase_fails() {
let blob = backup::seal("right", &keys(1)).unwrap();
assert!(backup::open("wrong", &blob).is_err());
}
#[test]
fn garbage_file_fails() {
assert!(backup::open("pw", b"nope").is_err());
assert!(backup::open("pw", b"owbak1").is_err());
let mut blob = backup::seal("pw", &keys(2)).unwrap();
blob.push(0xff);
assert!(backup::open("pw", &blob).is_err());
}
#[test]
fn restore_overwrites_self_keys_friends_stay() {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open_at(dir.path()).expect("open");
store
.upsert_friend(&[9u8; 32], "alice.onion", Some("alice"))
.unwrap();
let old = store.self_identity().unwrap();
let incoming = keys(42);
store
.replace_identity_keys(
&incoming.identity_sk,
&incoming.identity_pk,
&incoming.prekey_sk,
&incoming.prekey_pk,
)
.unwrap();
let me = store.self_identity().unwrap();
assert_eq!(me.identity_sk, incoming.identity_sk);
assert_eq!(me.identity_pk, incoming.identity_pk);
assert_eq!(me.prekey_sk, incoming.prekey_sk);
assert_eq!(me.prekey_pk, incoming.prekey_pk);
assert_eq!(me.onion, old.onion, "onion is locator, not restored");
assert_eq!(store.friend_count().unwrap(), 1);
let f = store.get_friend(&[9u8; 32]).unwrap().unwrap();
assert_eq!(f.petname.as_deref(), Some("alice"));
}
#[test]
fn slash_backup_restore() {
assert_eq!(
parse_cmd("/backup /tmp/id.owbak"),
Some(SlashCmd::Backup {
path: "/tmp/id.owbak".into()
})
);
assert_eq!(
parse_cmd(" /restore /tmp/id.owbak "),
Some(SlashCmd::Restore {
path: "/tmp/id.owbak".into()
})
);
assert_eq!(parse_cmd("/backup"), None);
assert_eq!(parse_cmd("/restore"), None);
}
#[test]
fn backup_requires_typing_not_enter() {
let mut p = BackupPrompt::backup();
assert_eq!(p.on_esc(), BackupDecision::Cancel);
assert_eq!(p.on_char('\n'), BackupDecision::Pending);
for c in "BACKU".chars() {
assert_eq!(p.on_char(c), BackupDecision::Pending);
}
assert_eq!(p.on_char('P'), BackupDecision::Confirm);
}
#[test]
fn restore_requires_typing_restore() {
let mut p = BackupPrompt::restore();
assert_eq!(p.on_char('\n'), BackupDecision::Pending);
for c in "RESTOR".chars() {
assert_eq!(p.on_char(c), BackupDecision::Pending);
}
assert_eq!(p.on_char('E'), BackupDecision::Confirm);
}
#[test]
fn restore_screen_warns_roster_stays() {
let t = restore_screen_text();
assert!(t.contains("does not rewrite the roster"), "{t}");
assert!(t.contains("Type RESTORE to confirm"), "{t}");
let b = backup_screen_text();
assert!(
t.contains("different person") || b.contains("identity"),
"{t}\n{b}"
);
assert!(b.contains("Type BACKUP to confirm"), "{b}");
}