//! 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_with_passphrase(dir.path(), "onionwire-test").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}"); }