//! M2 QR: roundtrip, garbage → Err, same-k rescan does not duplicate. use std::sync::{Mutex, MutexGuard}; use onionwire::qr; use onionwire::Store; static ENV_LOCK: Mutex<()> = Mutex::new(()); struct TempHome { _dir: tempfile::TempDir, _guard: MutexGuard<'static, ()>, } impl TempHome { fn new() -> Self { let dir = tempfile::tempdir().expect("tempdir"); let guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); unsafe { std::env::set_var("ONIONWIRE_HOME", dir.path()); std::env::set_var("ONIONWIRE_STORE_PASSPHRASE", "onionwire-test"); } Self { _dir: dir, _guard: guard, } } } impl Drop for TempHome { fn drop(&mut self) { unsafe { std::env::remove_var("ONIONWIRE_HOME"); std::env::remove_var("ONIONWIRE_STORE_PASSPHRASE"); } } } fn v3onion(tag: &str) -> String { let mut addr = vec![b'a'; 56]; let bytes: Vec = tag .bytes() .map(|b| match b { b'a'..=b'z' | b'2'..=b'7' => b, _ => b'a', }) .collect(); let n = bytes.len().min(56); addr[..n].copy_from_slice(&bytes[..n]); format!("{}.onion", String::from_utf8(addr).expect("base32")) } fn invite_fields(raw: &str) -> (String, String, String, String) { let rest = raw.strip_prefix("onionwire:v1:").expect("prefix"); let mut k = String::new(); let mut o = String::new(); let mut spk = String::new(); let mut sig = String::new(); for part in rest.split(':') { if let Some(v) = part.strip_prefix("k=") { k = v.to_string(); } else if let Some(v) = part.strip_prefix("o=") { o = v.to_string(); } else if let Some(v) = part.strip_prefix("spk=") { spk = v.to_string(); } else if let Some(v) = part.strip_prefix("sig=") { sig = v.to_string(); } } (k, o, spk, sig) } #[test] fn encode_decode_roundtrip() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let spk = [7u8; 32]; let onion = v3onion("alice-locator"); let raw = qr::encode(&me.identity_sk, &onion, &spk).expect("encode"); assert!(raw.starts_with("onionwire:v1:k="), "payload prefix: {raw}"); assert!(raw.contains(":o=")); assert!(raw.contains(":spk=")); assert!(raw.contains(":sig=")); let p = qr::decode(&raw).expect("decode"); assert_eq!(p.pubkey, me.identity_pk); assert_eq!(p.onion, onion); assert_eq!(p.signed_prekey, spk); } #[test] fn garbage_is_err() { assert!(qr::decode("not a qr").is_err()); assert!(qr::decode("").is_err()); assert!(qr::decode("onionwire:v1:k=aa").is_err()); assert!(qr::decode("🔥").is_err()); } #[test] fn missing_sig_is_err() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).expect("encode"); let stripped = raw.rsplit_once(":sig=").expect("sig field").0; assert!(qr::decode(stripped).is_err()); assert!(qr::decode(&format!("{stripped}:sig=")).is_err()); } #[test] fn bad_sig_is_err() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).expect("encode"); let (head, sig) = raw.rsplit_once(":sig=").expect("sig field"); let mut chars: Vec = sig.chars().collect(); let last = chars.last_mut().expect("sig chars"); *last = if *last == '0' { '1' } else { '0' }; let flipped: String = chars.into_iter().collect(); assert!(qr::decode(&format!("{head}:sig={flipped}")).is_err()); } #[test] fn same_k_rescan_updates_onion_not_duplicate() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let spk = [9u8; 32]; let p1 = qr::decode(&qr::encode(&me.identity_sk, &v3onion("old"), &spk).unwrap()).unwrap(); store .upsert_friend(&p1.pubkey, &p1.onion, Some("alice")) .unwrap(); let p2 = qr::decode(&qr::encode(&me.identity_sk, &v3onion("new"), &spk).unwrap()).unwrap(); store.upsert_friend(&p2.pubkey, &p2.onion, None).unwrap(); assert_eq!(store.friend_count().unwrap(), 1); let f = store.get_friend(&p2.pubkey).unwrap().unwrap(); assert_eq!(f.onion, v3onion("new")); assert_eq!(f.petname.as_deref(), Some("alice")); } #[test] fn unknown_k_decode_does_not_insert() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).unwrap(); let _p = qr::decode(&raw).unwrap(); assert_eq!(store.friend_count().unwrap(), 0); } #[test] fn shifted_o_spk_is_err() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let onion = v3onion("honest"); let spk = [0xab; 32]; let raw = qr::encode(&me.identity_sk, &onion, &spk).expect("encode"); let (k, o, spk_hex, sig) = invite_fields(&raw); assert_eq!(spk_hex.len(), 64); let mutant = format!( "onionwire:v1:k={k}:o={o}{}:spk={}:sig={sig}", &spk_hex[..8], &spk_hex[8..] ); assert!( qr::decode(&mutant).is_err(), "shifted o/spk must not verify" ); } #[test] fn encode_rejects_spk_not_32() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let onion = v3onion("spk"); assert!(qr::encode(&me.identity_sk, &onion, &[1u8; 31]).is_err()); assert!(qr::encode(&me.identity_sk, &onion, &[1u8; 33]).is_err()); assert!(qr::encode(&me.identity_sk, &onion, &[]).is_err()); } #[test] fn encode_rejects_garbage_onion() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let spk = [2u8; 32]; assert!(qr::encode(&me.identity_sk, "not-an-onion", &spk).is_err()); assert!(qr::encode(&me.identity_sk, "alice.onion", &spk).is_err()); assert!(qr::encode(&me.identity_sk, &format!("{}.onion", "A".repeat(56)), &spk).is_err()); } #[test] fn decode_rejects_spk_len_and_garbage_onion() { let _home = TempHome::new(); let store = Store::open().expect("open"); let me = store.self_identity().expect("self"); let raw = qr::encode(&me.identity_sk, &v3onion("ok"), &[3u8; 32]).expect("encode"); let (k, o, spk, sig) = invite_fields(&raw); let short_spk = format!("onionwire:v1:k={k}:o={o}:spk={}:sig={sig}", &spk[..62]); assert!(qr::decode(&short_spk).is_err()); let garbage_o = format!("onionwire:v1:k={k}:o=nope.onion:spk={spk}:sig={sig}"); assert!(qr::decode(&garbage_o).is_err()); } #[test] fn huge_hex_is_err_quickly() { let huge = format!( "onionwire:v1:k={}:o=x:spk=yy:sig=zz", "aa".repeat(1024 * 1024) ); let t = std::time::Instant::now(); assert!(qr::decode(&huge).is_err()); assert!( t.elapsed() < std::time::Duration::from_millis(250), "decode must fail closed before a multi-MB alloc" ); }