Identity key stays put. Typing ROTATE publishes a new Arti HS, hard-cuts the old nickname, persists self.onion, and pushes loc to reachable friends.
132 lines
4.1 KiB
Rust
132 lines
4.1 KiB
Rust
//! M4: loc frames, F4 confirm typing, identity stays put.
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use onionwire::Store;
|
|
use onionwire::loc::{self, Loc};
|
|
use onionwire::tui::{RotateDecision, RotatePrompt};
|
|
use rand::rngs::OsRng;
|
|
|
|
fn pk(tag: u8) -> [u8; 32] {
|
|
let mut k = [0u8; 32];
|
|
k[0] = tag;
|
|
k
|
|
}
|
|
|
|
fn store() -> (tempfile::TempDir, Store) {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
let store = Store::open_at(dir.path()).expect("open");
|
|
(dir, store)
|
|
}
|
|
|
|
#[test]
|
|
fn signed_loc_updates_friend_onion_keeps_fingerprint() {
|
|
let (_dir, store) = store();
|
|
let sk = SigningKey::generate(&mut OsRng);
|
|
let pk = sk.verifying_key().to_bytes();
|
|
store
|
|
.upsert_friend(&pk, "old.onion", Some("alice"))
|
|
.unwrap();
|
|
store.set_friend_prekey(&pk, &[1u8; 32]).unwrap();
|
|
let before = store.get_friend(&pk).unwrap().unwrap();
|
|
|
|
let ts = 2_000_000_000;
|
|
let loc = loc::sign(&sk.to_bytes(), "new.onion", ts).expect("sign");
|
|
assert!(
|
|
store.apply_loc(&pk, &loc.onion, loc.ts, &loc.sig).unwrap(),
|
|
"newer signed loc must apply"
|
|
);
|
|
|
|
let after = store.get_friend(&pk).unwrap().unwrap();
|
|
assert_eq!(after.onion, "new.onion");
|
|
assert_eq!(after.fingerprint, before.fingerprint);
|
|
assert_eq!(after.petname.as_deref(), Some("alice"));
|
|
assert_eq!(store.friend_count().unwrap(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn unsigned_loc_does_not_change_row() {
|
|
let (_dir, store) = store();
|
|
store.upsert_friend(&pk(1), "old.onion", Some("c")).unwrap();
|
|
let loc = Loc {
|
|
onion: "evil.onion".into(),
|
|
ts: 2_000_000_000,
|
|
sig: vec![],
|
|
};
|
|
assert!(
|
|
!store
|
|
.apply_loc(&pk(1), &loc.onion, loc.ts, &loc.sig)
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
store.get_friend(&pk(1)).unwrap().unwrap().onion,
|
|
"old.onion"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_key_loc_does_not_change_row() {
|
|
let (_dir, store) = store();
|
|
let owner = SigningKey::generate(&mut OsRng);
|
|
let owner_pk = owner.verifying_key().to_bytes();
|
|
store.upsert_friend(&owner_pk, "old.onion", None).unwrap();
|
|
let other = SigningKey::generate(&mut OsRng);
|
|
let loc = loc::sign(&other.to_bytes(), "hijack.onion", 2_000_000_000).unwrap();
|
|
assert!(
|
|
!store
|
|
.apply_loc(&owner_pk, &loc.onion, loc.ts, &loc.sig)
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
store.get_friend(&owner_pk).unwrap().unwrap().onion,
|
|
"old.onion"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stale_ts_loc_does_not_change_row() {
|
|
let (_dir, store) = store();
|
|
let sk = SigningKey::generate(&mut OsRng);
|
|
let pk = sk.verifying_key().to_bytes();
|
|
store.upsert_friend(&pk, "old.onion", None).unwrap();
|
|
let loc = loc::sign(&sk.to_bytes(), "new.onion", 1).unwrap();
|
|
assert!(!store.apply_loc(&pk, &loc.onion, loc.ts, &loc.sig).unwrap());
|
|
assert_eq!(store.get_friend(&pk).unwrap().unwrap().onion, "old.onion");
|
|
}
|
|
|
|
#[test]
|
|
fn loc_frame_roundtrip_and_chat_is_not_loc() {
|
|
let loc = loc::sign(&[7u8; 32], "abc.onion", 42).unwrap();
|
|
let bytes = loc::encode(&loc);
|
|
let parsed = loc::decode(&bytes).expect("loc frame");
|
|
assert_eq!(parsed.onion, "abc.onion");
|
|
assert_eq!(parsed.ts, 42);
|
|
assert_eq!(parsed.sig, loc.sig);
|
|
assert!(loc::decode(b"hello wire").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn f4_requires_typing_rotate_not_enter() {
|
|
let mut p = RotatePrompt::new();
|
|
assert_eq!(p.on_esc(), RotateDecision::Cancel);
|
|
assert_eq!(
|
|
p.on_char('\n'),
|
|
RotateDecision::Pending,
|
|
"Enter must not rotate"
|
|
);
|
|
for c in "ROTA".chars() {
|
|
assert_eq!(p.on_char(c), RotateDecision::Pending);
|
|
}
|
|
assert_eq!(p.on_char('T'), RotateDecision::Pending);
|
|
assert_eq!(p.on_char('E'), RotateDecision::Confirm);
|
|
}
|
|
|
|
#[test]
|
|
fn rotate_prompt_text_matches_spec() {
|
|
let t = onionwire::tui::rotate_screen_text();
|
|
assert!(t.contains("Rotate onion address?"));
|
|
assert!(t.contains("Your identity key stays the same."));
|
|
assert!(t.contains("Online friends get a signed location update."));
|
|
assert!(t.contains("Offline friends CANNOT find you until they rescan your QR."));
|
|
assert!(t.contains("Type ROTATE to confirm"));
|
|
assert!(t.contains("Esc to cancel"));
|
|
}
|