authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 21:40:53-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 22:15:02-07:00
log4c59412d43f497f29b4c3d4b9695134f634cf17f
tree36a33513df8e6702577e1bd1fe1edfc298201f41
parente1eb16f90d89d57b6c0a1003cb2a9468c121c530
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: Live Share codes are Crockford base32, like 7KQ-4MZ-9XR, in place of words

Two random words made codes like "678-scary-virus". A code is now nine symbols of Crockford base32 (0-9 and A-Z without I, L, O and U), shown in threes: two numbering the relay's room (it numbers 1 to 999, which two symbols hold), six of secret, 30 bits, and a check symbol, the symbols weighted 1 to 8 modulo 31, which refuses a mistyped symbol or two swapped before it spends one of the relay's tries. Reading ignores case, hyphens and spaces and takes I and L for 1, O for 0. With five tries before a code burns, a guess at one code succeeds with odds 5 in 2^30, about 1 in 215 million; every burn draws a new secret, and the relay's lockout leaves one address about ten tries an hour. The relay is unchanged: rooms are still code-<n>. A word code and a new code each read as malformed on the other version, before any network. Assisted-by: claude-opus-5.5

10 files changed, 233 insertions(+), 1390 deletions(-)

arc/sync.md+2-1
......@@ -320,7 +320,8 @@ in the share's room; a guest runs the replica, queue and merge it runs on an SMB
320320them (`Notebook::open_hosted`, `Section::resume_hosted`, `Background::hosted`), so offline
321321queueing, rebases and conflict pages behave as on a share, and the host's files are only ever
322322written by its own storage, OneNote's locks included. A guest meets the host first in the
323room of a short code (`412-violet-otter`, SPAKE2 on its words and any password) through
323room of a short code (`7KQ-4MZ-9XR`: Crockford base32, its room's number, 30 bits of
324secret and a check symbol; SPAKE2 on its secret and any password) through
324325Snowbound's relay or by mDNS, and is welcomed with the share's room and its random secret;
325326stopping or restarting a share retires the secret. A guest's commit is checked on the host's
326327image before it is committed, and a guest can name nothing outside the notebook. Large reads
crates/notebook/README.md+5-3
......@@ -538,10 +538,12 @@ physical power-loss durability.
538538`live::Live::start(hello, room, reach, relay, events)` listens on a TCP port and, with a
539539`Reach`, advertises `_snowbound._tcp` by mDNS on every network or on loopback alone,
540540connecting to the peers in the same `Room` that it finds: a notebook's or a share's random
541secret (`Room::Notebook`), or a code typed on both (`Room::join("4-violet-otter", password)`).
541secret (`Room::Notebook`), or a code typed on both (`Room::join("7KQ-4MZ-9XR", password)`).
542542With a `relay` (`wss://live.example.net`, `crates/relay`) it also joins the room there and
543543meets its peers through it; the end sharing a code (`Room::share`) has the relay number its
544words, `code()` then has the whole code, and it burns a code after too many wrong tries.
544secret, `code()` then has the whole code, and it burns a code after too many wrong tries.
545Codes are Crockford base32 (`live::code`): two symbols numbering the room, six of secret
546(30 bits) and a check symbol that refuses a typo before it spends one of the relay's tries.
545547`connect(address)` meets a peer discovery did not find. Peers meet through SPAKE2 on the
546548room's secret, then every frame is AES-256-GCM under the keys it agreed: its number, which is
547549also its nonce, then a message kind and a CBOR map (`live::wire`). A frame lost, repeated,
......@@ -558,7 +560,7 @@ a notebook's presence room, kept in `.snowbound/live.json` and made where it has
558560`live::share` is Live Share. `Host::start(storage, hello, sharing, name, reach, relay, events)`
559561serves `Notebook::into_storage()` to the peers in the share's room and welcomes whoever knows
560562`Sharing::code` (and its password) from the code's room; `code()` replaces a burned code with
561new words, `guests()` lists who is connected, `touched(paths)` passes the host's own changes
563a new secret, `guests()` lists who is connected, `touched(paths)` passes the host's own changes
562564on, and `stop()` lets every guest go. `join(hello, code, password, reach, relay)` returns the
563565`Welcome` (the share and its secret), or a `Refusal` saying why not: a wrong code, no one
564566sharing it, an expired code, too many wrong tries, or no relay. `Guest::start` joins the share;
crates/notebook/src/live.rs+25-25
......@@ -7,6 +7,7 @@
77//! and one writing. A connection whose frames arrive out of order is dropped and met again
88//! from scratch.
99
10pub mod code;
1011mod relay;
1112pub mod share;
1213pub mod wire;
......@@ -44,10 +45,10 @@ const TRIES: u32 = 5;
4445pub enum Room {
4546 /// A notebook's room, or a share's: a random secret only its members hold.
4647 Notebook([u8; 16]),
47 /// A code typed on both ends, `412-violet-otter`, with a password where one is set: its
48 /// number names it on the network, and its words and password only the two people know.
48 /// A code typed on both ends (`code`), with a password where one is set: its room's
49 /// number names it on the network, and its secret and password only the two people know.
4950 /// Its `owner`, the end sharing it, takes the number from a relay (the one the code has,
50 /// coming back; any free one for words alone) or picks one where it has no relay, and
51 /// coming back; any free one for a secret alone) or picks one where it has no relay, and
5152 /// judges every try, burning the code after too many wrong ones.
5253 Code {
5354 code: String,
......@@ -60,16 +61,16 @@ impl Room {
6061 /// The room a code typed on this end leads to.
6162 pub fn join(code: &str, password: &str) -> Self {
6263 Self::Code {
63 code: code.trim().to_lowercase(),
64 code: code.to_owned(),
6465 password: password.to_owned(),
6566 owner: false,
6667 }
6768 }
6869
69 /// The room of a code this end shares: its words, or a whole code to keep its number.
70 /// The room of a code this end shares: its secret, or a whole code to keep its number.
7071 pub fn share(code: &str, password: &str) -> Self {
7172 Self::Code {
72 code: code.trim().to_lowercase(),
73 code: code.to_owned(),
7374 password: password.to_owned(),
7475 owner: true,
7576 }
......@@ -90,7 +91,7 @@ impl Room {
9091 match self {
9192 Room::Notebook(id) => id.to_vec(),
9293 Room::Code { code, password, .. } => {
93 let mut secret = code_parts(code).1.to_lowercase().into_bytes();
94 let mut secret = code_parts(code).1.into_bytes();
9495 if !password.is_empty() {
9596 secret.push(b'\n');
9697 secret.extend_from_slice(password.as_bytes());
......@@ -105,12 +106,11 @@ impl Room {
105106 }
106107}
107108
108/// A code's number, if it has one, and its words.
109fn code_parts(code: &str) -> (Option<u32>, &str) {
110 let code = code.trim();
111 match code.split_once('-') {
112 Some((number, words)) if number.parse::<u32>().is_ok() => (number.parse().ok(), words),
113 _ => (None, code),
109/// A code's room number, if it has one, and its secret: a whole code, or a secret alone.
110fn code_parts(code: &str) -> (Option<u32>, String) {
111 match code::parse(code) {
112 Some((number, secret)) => (Some(number), secret),
113 None => (None, code.trim().to_uppercase()),
114114 }
115115}
116116
......@@ -309,18 +309,18 @@ impl Live {
309309 address.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
310310 }
311311 let code = match room {
312 Room::Code { code, .. } if room.tag().is_some() => Some(code.clone()),
313 // Off any relay, the end sharing words alone numbers them itself.
314 Room::Code { code, .. } if relay.is_none() => {
315 let mut number = [0; 2];
316 getrandom::fill(&mut number)
317 .map_err(|_| io::Error::other("System random source failed"))?;
318 Some(format!(
319 "{}-{code}",
320 1000 + u16::from_le_bytes(number) % 9000
321 ))
322 }
323 _ => None,
312 Room::Code { code, .. } => match code_parts(code) {
313 (Some(number), secret) => code::format(number, &secret),
314 // Off any relay, the end sharing a secret alone numbers it itself.
315 (None, secret) if relay.is_none() => {
316 let mut number = [0; 4];
317 getrandom::fill(&mut number)
318 .map_err(|_| io::Error::other("System random source failed"))?;
319 code::format(u32::from_le_bytes(number) % code::NAMEPLATES, &secret)
320 }
321 (None, _) => None,
322 },
323 Room::Notebook(_) => None,
324324 };
325325 let shared = Arc::new(Shared {
326326 me,
crates/notebook/src/live/code.rs created+148
......@@ -0,0 +1,148 @@
1//! Live Share's codes in Crockford's base32 (0-9 and A-Z without I, L, O and U), shown
2//! `7KQ-4MZ-9XR`: two symbols naming the code's room, its number on a relay; six of secret
3//! (30 bits), which SPAKE2 meets through; and a check symbol, so a mistyped code is refused
4//! here before it spends one of the relay's few tries. Reading ignores case, hyphens and
5//! spaces, and takes I and L for 1 and O for 0, as Crockford's decoding does.
6//!
7//! The check is the symbols' values weighted 1 to 8, summed modulo 31, the prime under 32, so
8//! it stays one of the code's own symbols: it catches any symbol mistyped and any two
9//! neighbours swapped, but for 0 and Z, whose values differ by 31. Crockford's own check
10//! symbol, modulo 37, adds `*~$=U`, which read badly aloud.
11
12use std::io;
13
14const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
15/// The symbols naming a code's room, and how many rooms they name.
16const NAMEPLATE: usize = 2;
17pub const NAMEPLATES: u32 = 1 << (5 * NAMEPLATE);
18/// The symbols of a code's secret.
19pub const SECRET: usize = 6;
20
21/// A new secret, `SECRET` random symbols.
22pub fn secret() -> io::Result<String> {
23 let mut bytes = [0; 4];
24 getrandom::fill(&mut bytes).map_err(|_| io::Error::other("System random source failed"))?;
25 let bits = u32::from_le_bytes(bytes);
26 Ok((0..SECRET)
27 .map(|at| symbol((bits >> (5 * at)) as u8))
28 .collect())
29}
30
31fn symbol(value: u8) -> char {
32 char::from(ALPHABET[usize::from(value & 31)])
33}
34
35/// A symbol's value as typed, reading I and L as 1 and O as 0.
36fn value(typed: char) -> Option<u8> {
37 let typed = match typed.to_ascii_uppercase() {
38 'I' | 'L' => '1',
39 'O' => '0',
40 typed => typed,
41 };
42 ALPHABET
43 .iter()
44 .position(|symbol| char::from(*symbol) == typed)
45 .map(|at| at as u8)
46}
47
48fn check(values: &[u8]) -> u8 {
49 let sum: u32 = (values.iter().enumerate())
50 .map(|(at, value)| (at as u32 + 1) * u32::from(*value))
51 .sum();
52 (sum % 31) as u8
53}
54
55/// The code for room `nameplate` and `secret`, as shown: `7KQ-4MZ-9XR`. A secret that isn't
56/// `SECRET` symbols has no code.
57pub fn format(nameplate: u32, secret: &str) -> Option<String> {
58 let secret: Vec<u8> = secret.chars().map(value).collect::<Option<_>>()?;
59 if nameplate >= NAMEPLATES || secret.len() != SECRET {
60 return None;
61 }
62 let mut values = vec![(nameplate >> 5) as u8, (nameplate & 31) as u8];
63 values.extend(secret);
64 values.push(check(&values));
65 let symbols: Vec<char> = values.into_iter().map(symbol).collect();
66 Some(
67 symbols
68 .chunks(3)
69 .map(|group| group.iter().collect::<String>())
70 .collect::<Vec<_>>()
71 .join("-"),
72 )
73}
74
75/// A code as typed: its room's number and its secret, where it is a code whose check holds.
76pub fn parse(typed: &str) -> Option<(u32, String)> {
77 let values: Vec<u8> = typed
78 .chars()
79 .filter(|c| *c != '-' && !c.is_whitespace())
80 .map(value)
81 .collect::<Option<_>>()?;
82 let (check_value, values) = values.split_last()?;
83 if values.len() != NAMEPLATE + SECRET || check(values) != *check_value {
84 return None;
85 }
86 let nameplate = (u32::from(values[0]) << 5) | u32::from(values[1]);
87 let secret = values[NAMEPLATE..].iter().copied().map(symbol).collect();
88 Some((nameplate, secret))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn codes_read_back_however_they_are_typed() {
97 let code = format(412, "4MZ9XR").unwrap();
98 assert_eq!(code.len(), 11);
99 assert_eq!(parse(&code), Some((412, "4MZ9XR".into())));
100 let typed = code
101 .to_lowercase()
102 .replace('-', " ")
103 .replace('1', "l")
104 .replace('0', "o");
105 assert_eq!(parse(&typed), Some((412, "4MZ9XR".into())));
106 assert_eq!(parse(&format!(" {code} ")), Some((412, "4MZ9XR".into())));
107 assert!(format(NAMEPLATES, "4MZ9XR").is_none() && format(1, "4MZ9X").is_none());
108 assert!(parse("412-violet-otter").is_none() && parse("").is_none());
109 for _ in 0..100 {
110 let secret = secret().unwrap();
111 assert_eq!(secret.len(), SECRET);
112 assert_eq!(parse(&format(7, &secret).unwrap()), Some((7, secret)));
113 }
114 }
115
116 /// The check refuses any one symbol mistyped, and any two neighbours swapped, but for 0
117 /// and Z.
118 #[test]
119 fn the_check_catches_a_symbol_mistyped_or_two_swapped() {
120 let code: Vec<char> = format(999, "Q4MZ9X")
121 .unwrap()
122 .replace('-', "")
123 .chars()
124 .collect();
125 for at in 0..code.len() {
126 for symbol in ALPHABET.iter().map(|byte| char::from(*byte)) {
127 let mut typed = code.clone();
128 if typed[at] != symbol && !matches!((typed[at], symbol), ('0', 'Z') | ('Z', '0')) {
129 typed[at] = symbol;
130 assert!(
131 parse(&typed.iter().collect::<String>()).is_none(),
132 "{typed:?}"
133 );
134 }
135 }
136 }
137 for at in 0..code.len() - 1 {
138 let mut typed = code.clone();
139 typed.swap(at, at + 1);
140 if typed != code {
141 assert!(
142 parse(&typed.iter().collect::<String>()).is_none(),
143 "{typed:?}"
144 );
145 }
146 }
147 }
148}
crates/notebook/src/live/relay.rs+4-2
......@@ -372,10 +372,12 @@ fn session(
372372 Ok(Notice::Nameplate(number)) => {
373373 *nameplate = Some(number);
374374 tag = Some(format!("code-{number}"));
375 let super::Room::Code { code: words, .. } = &shared.room else {
375 let super::Room::Code { code, .. } = &shared.room else {
376 continue;
377 };
378 let Some(code) = super::code::format(number, &code_parts(code).1) else {
376379 continue;
377380 };
378 let code = format!("{number}-{}", code_parts(words).1);
379381 let mut state = shared.state.lock().unwrap();
380382 if state.code.as_ref() != Some(&code) {
381383 state.code = Some(code);
crates/notebook/src/live/share.rs+11-45
......@@ -6,9 +6,6 @@
66//! the host welcomes it with the share's room and secret; a new share has a new secret, so
77//! stopping retires every guest. Large bodies travel a chunk at a time, each answered before
88//! the next, so a relay never holds much for a slow peer.
9//!
10//! The code's words come from the EFF's short word list
11//! (<https://www.eff.org/dice>, CC BY 3.0 US), `yo-yo` replaced by `yarn`.
129
1310use super::{
1411 Event, Hello, Line, Live, Peer, Presence, Reach, Relayed, Room,
......@@ -55,42 +52,11 @@ const WORKERS: usize = 2;
5552const STARTS: f64 = 100.0;
5653const BURST: f64 = 200.0;
5754
58const WORDS: &str = include_str!("words.txt");
59
60/// Two random words for a code, `violet-otter`.
61pub fn words() -> io::Result<String> {
62 let list: Vec<&str> = WORDS.lines().collect();
63 let mut bytes = [0; 8];
64 getrandom::fill(&mut bytes).map_err(|_| io::Error::other("System random source failed"))?;
65 let [first, second] = [&bytes[..4], &bytes[4..]]
66 .map(|bytes| u32::from_le_bytes(bytes.try_into().expect("4 bytes")) as usize);
67 let first = first % list.len();
68 let mut second = second % (list.len() - 1);
69 if second >= first {
70 second += 1;
71 }
72 Ok(format!("{}-{}", list[first], list[second]))
73}
74
75/// A code as typed, `412 Violet otter`, in the form it is met by, `412-violet-otter`; none
76/// where it is not a number and two words.
55/// A code as typed, `7kq 4mz 9xr`, as it is shown, `7KQ-4MZ-9XR`; none where it is not a
56/// code (`super::code`).
7757pub fn code(typed: &str) -> Option<String> {
78 let parts: Vec<String> = typed
79 .split(|c: char| c.is_whitespace() || c == '-')
80 .filter(|part| !part.is_empty())
81 .map(str::to_lowercase)
82 .collect();
83 match &parts[..] {
84 [number, first, second]
85 if number.parse::<u32>().is_ok()
86 && [first, second]
87 .iter()
88 .all(|word| word.chars().all(|c| c.is_ascii_lowercase())) =>
89 {
90 Some(parts.join("-"))
91 }
92 _ => None,
93 }
58 let (number, secret) = super::code::parse(typed)?;
59 super::code::format(number, &secret)
9460}
9561
9662/// What a host keeps of a share to take it up again after a relaunch.
......@@ -99,13 +65,13 @@ pub struct Sharing {
9965 pub share: [u8; 16],
10066 /// The share room's secret, which every guest welcomed holds.
10167 pub secret: [u8; 16],
102 /// The code: its words, with its number in front once one was given.
68 /// The code, or its secret alone until it has a number (`super::code`).
10369 pub code: String,
10470 pub password: String,
10571}
10672
10773impl Sharing {
108 /// A new share: its own id and secret, and new words.
74 /// A new share: its own id, room secret and code.
10975 pub fn new(password: &str) -> io::Result<Self> {
11076 let mut random = [0; 32];
11177 getrandom::fill(&mut random)
......@@ -113,7 +79,7 @@ impl Sharing {
11379 Ok(Self {
11480 share: random[..16].try_into().expect("16 bytes"),
11581 secret: random[16..].try_into().expect("16 bytes"),
116 code: words()?,
82 code: super::code::secret()?,
11783 password: password.to_owned(),
11884 })
11985 }
......@@ -127,9 +93,9 @@ pub fn location(share: &[u8; 16]) -> String {
12793/// Why joining failed.
12894#[derive(Clone, Debug, PartialEq, Eq)]
12995pub enum Refusal {
130 /// Not a number and two words.
96 /// Not a code, or one mistyped, which spends none of the relay's tries.
13197 Malformed,
132 /// The code's words or password are wrong.
98 /// The code's secret or password is wrong.
13399 Wrong,
134100 /// No one shares with the code's number now.
135101 NoOne,
......@@ -281,13 +247,13 @@ impl Host {
281247 }
282248
283249 /// The code guests type, once it has its number, and none once stopped. A code with too
284 /// many wrong tries is replaced by one with new words.
250 /// many wrong tries is replaced by one with a new secret.
285251 pub fn code(&self) -> Option<String> {
286252 let mut pairing = self.pairing.lock().unwrap();
287253 let pairing = pairing.as_mut()?;
288254 if pairing.burned() {
289255 let mut sharing = self.sharing.lock().unwrap();
290 sharing.code = words().ok()?;
256 sharing.code = super::code::secret().ok()?;
291257 let relay = self.relay.as_deref();
292258 *pairing = pair(
293259 &self.me,
crates/notebook/src/live/tests.rs+13-8
......@@ -1,6 +1,11 @@
11use super::*;
22use std::time::Instant;
33
4/// The code for room `number` and `secret`.
5fn code(number: u32, secret: &str) -> String {
6 super::code::format(number, secret).unwrap()
7}
8
49fn hello(name: &str) -> Hello {
510 Hello::new(name.into(), Some(vec![1, 2, 3])).unwrap()
611}
......@@ -43,7 +48,7 @@ fn caret(offset: u32) -> Presence {
4348/// caret, and see the other leave.
4449#[test]
4550fn peers_meet_and_follow_presence() {
46 let room = Room::join("7-violet-otter", "");
51 let room = Room::join(&code(7, "ABCDEF"), "");
4752 let ada = Live::start(hello("Ada"), &room, None, None, |_| {}).unwrap();
4853 let grace = Live::start(hello("Grace"), &room, None, None, |_| {}).unwrap();
4954 ada.set_presence(caret(1));
......@@ -70,7 +75,7 @@ fn peers_meet_and_follow_presence() {
7075fn another_code_never_meets() {
7176 let ada = Live::start(
7277 hello("Ada"),
73 &Room::join("7-violet-otter", ""),
78 &Room::join(&code(7, "ABCDEF"), ""),
7479 None,
7580 None,
7681 |_| {},
......@@ -78,7 +83,7 @@ fn another_code_never_meets() {
7883 .unwrap();
7984 let mallory = Live::start(
8085 hello("Mallory"),
81 &Room::join("7-violet-ocelot", ""),
86 &Room::join(&code(7, "ABCDEG"), ""),
8287 None,
8388 None,
8489 |_| {},
......@@ -115,7 +120,7 @@ fn later_fields_and_kinds_are_skipped() {
115120 }
116121 );
117122
118 let room = Room::join("4-quiet-heron", "");
123 let room = Room::join(&code(4, "QJETHR"), "");
119124 let grace = Live::start(hello("Grace"), &room, None, None, |_| {}).unwrap();
120125 // A later version: it greets, says something new, then where it is.
121126 let later = thread::spawn(move || {
......@@ -298,7 +303,7 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() {
298303 });
299304 let host = Live::start(
300305 hello("Ada"),
301 &Room::share("violet-otter", ""),
306 &Room::share("ABCDEF", ""),
302307 None,
303308 Some(&url),
304309 |_| {},
......@@ -312,8 +317,8 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() {
312317 assert!(Instant::now() < deadline, "no code");
313318 thread::sleep(Duration::from_millis(20));
314319 };
315 let (number, words) = code.split_once('-').unwrap();
316 assert_eq!(words, "violet-otter");
320 let (number, secret) = super::code::parse(&code).unwrap();
321 assert_eq!(secret, "ABCDEF");
317322 let guest = Live::start(
318323 hello("Grace"),
319324 &Room::join(&code, ""),
......@@ -326,7 +331,7 @@ fn a_relay_numbers_a_code_and_burns_it_after_wrong_tries() {
326331 until(&guest, |peers| peers.len() == 1);
327332
328333 let path = format!("/v1/room/code-{number}");
329 let wrong = Room::join(&format!("{number}-violet-ocelot"), "");
334 let wrong = Room::join(&self::code(number, "ABCDEG"), "");
330335 // An end that typed a wrong code gives up at once; Mallory tries twice, and the second
331336 // wrong try burns the code.
332337 for _ in 0..2 {
crates/notebook/src/live/words.txt deleted-1296
......@@ -1,1296 +0,0 @@
1acid
2acorn
3acre
4acts
5afar
6affix
7aged
8agent
9agile
10aging
11agony
12ahead
13aide
14aids
15aim
16ajar
17alarm
18alias
19alibi
20alien
21alike
22alive
23aloe
24aloft
25aloha
26alone
27amend
28amino
29ample
30amuse
31angel
32anger
33angle
34ankle
35apple
36april
37apron
38aqua
39area
40arena
41argue
42arise
43armed
44armor
45army
46aroma
47array
48arson
49art
50ashen
51ashes
52atlas
53atom
54attic
55audio
56avert
57avoid
58awake
59award
60awoke
61axis
62bacon
63badge
64bagel
65baggy
66baked
67baker
68balmy
69banjo
70barge
71barn
72bash
73basil
74bask
75batch
76bath
77baton
78bats
79blade
80blank
81blast
82blaze
83bleak
84blend
85bless
86blimp
87blink
88bloat
89blob
90blog
91blot
92blunt
93blurt
94blush
95boast
96boat
97body
98boil
99bok
100bolt
101boned
102boney
103bonus
104bony
105book
106booth
107boots
108boss
109botch
110both
111boxer
112breed
113bribe
114brick
115bride
116brim
117bring
118brink
119brisk
120broad
121broil
122broke
123brook
124broom
125brush
126buck
127bud
128buggy
129bulge
130bulk
131bully
132bunch
133bunny
134bunt
135bush
136bust
137busy
138buzz
139cable
140cache
141cadet
142cage
143cake
144calm
145cameo
146canal
147candy
148cane
149canon
150cape
151card
152cargo
153carol
154carry
155carve
156case
157cash
158cause
159cedar
160chain
161chair
162chant
163chaos
164charm
165chase
166cheek
167cheer
168chef
169chess
170chest
171chew
172chief
173chili
174chill
175chip
176chomp
177chop
178chow
179chuck
180chump
181chunk
182churn
183chute
184cider
185cinch
186city
187civic
188civil
189clad
190claim
191clamp
192clap
193clash
194clasp
195class
196claw
197clay
198clean
199clear
200cleat
201cleft
202clerk
203click
204cling
205clink
206clip
207cloak
208clock
209clone
210cloth
211cloud
212clump
213coach
214coast
215coat
216cod
217coil
218coke
219cola
220cold
221colt
222coma
223come
224comic
225comma
226cone
227cope
228copy
229coral
230cork
231cost
232cot
233couch
234cough
235cover
236cozy
237craft
238cramp
239crane
240crank
241crate
242crave
243crawl
244crazy
245creme
246crepe
247crept
248crib
249cried
250crisp
251crook
252crop
253cross
254crowd
255crown
256crumb
257crush
258crust
259cub
260cult
261cupid
262cure
263curl
264curry
265curse
266curve
267curvy
268cushy
269cut
270cycle
271dab
272dad
273daily
274dairy
275daisy
276dance
277dandy
278darn
279dart
280dash
281data
282date
283dawn
284deaf
285deal
286dean
287debit
288debt
289debug
290decaf
291decal
292decay
293deck
294decor
295decoy
296deed
297delay
298denim
299dense
300dent
301depth
302derby
303desk
304dial
305diary
306dice
307dig
308dill
309dime
310dimly
311diner
312dingy
313disco
314dish
315disk
316ditch
317ditzy
318dizzy
319dock
320dodge
321doing
322doll
323dome
324donor
325donut
326dose
327dot
328dove
329down
330dowry
331doze
332drab
333drama
334drank
335draw
336dress
337dried
338drift
339drill
340drive
341drone
342droop
343drove
344drown
345drum
346dry
347duck
348duct
349dude
350dug
351duke
352duo
353dusk
354dust
355duty
356dwarf
357dwell
358eagle
359early
360earth
361easel
362east
363eaten
364eats
365ebay
366ebony
367ebook
368echo
369edge
370eel
371eject
372elbow
373elder
374elf
375elk
376elm
377elope
378elude
379elves
380email
381emit
382empty
383emu
384enter
385entry
386envoy
387equal
388erase
389error
390erupt
391essay
392etch
393evade
394even
395evict
396evil
397evoke
398exact
399exit
400fable
401faced
402fact
403fade
404fall
405false
406fancy
407fang
408fax
409feast
410feed
411femur
412fence
413fend
414ferry
415fetal
416fetch
417fever
418fiber
419fifth
420fifty
421film
422filth
423final
424finch
425fit
426five
427flag
428flaky
429flame
430flap
431flask
432fled
433flick
434fling
435flint
436flip
437flirt
438float
439flock
440flop
441floss
442flyer
443foam
444foe
445fog
446foil
447folic
448folk
449food
450fool
451found
452fox
453foyer
454frail
455frame
456fray
457fresh
458fried
459frill
460frisk
461from
462front
463frost
464froth
465frown
466froze
467fruit
468gag
469gains
470gala
471game
472gap
473gas
474gave
475gear
476gecko
477geek
478gem
479genre
480gift
481gig
482gills
483given
484giver
485glad
486glass
487glide
488gloss
489glove
490glow
491glue
492goal
493going
494golf
495gong
496good
497gooey
498goofy
499gore
500gown
501grab
502grain
503grant
504grape
505graph
506grasp
507grass
508grave
509gravy
510gray
511green
512greet
513grew
514grid
515grief
516grill
517grip
518grit
519groom
520grope
521growl
522grub
523grunt
524guide
525gulf
526gulp
527gummy
528guru
529gush
530gut
531guy
532habit
533half
534halo
535halt
536happy
537harm
538hash
539hasty
540hatch
541hate
542haven
543hazel
544hazy
545heap
546heat
547heave
548hedge
549hefty
550help
551herbs
552hers
553hub
554hug
555hula
556hull
557human
558humid
559hump
560hung
561hunk
562hunt
563hurry
564hurt
565hush
566hut
567ice
568icing
569icon
570icy
571igloo
572image
573ion
574iron
575islam
576issue
577item
578ivory
579ivy
580jab
581jam
582jaws
583jazz
584jeep
585jelly
586jet
587jiffy
588job
589jog
590jolly
591jolt
592jot
593joy
594judge
595juice
596juicy
597july
598jumbo
599jump
600junky
601juror
602jury
603keep
604keg
605kept
606kick
607kilt
608king
609kite
610kitty
611kiwi
612knee
613knelt
614koala
615kung
616ladle
617lady
618lair
619lake
620lance
621land
622lapel
623large
624lash
625lasso
626last
627latch
628late
629lazy
630left
631legal
632lemon
633lend
634lens
635lent
636level
637lever
638lid
639life
640lift
641lilac
642lily
643limb
644limes
645line
646lint
647lion
648lip
649list
650lived
651liver
652lunar
653lunch
654lung
655lurch
656lure
657lurk
658lying
659lyric
660mace
661maker
662malt
663mama
664mango
665manor
666many
667map
668march
669mardi
670marry
671mash
672match
673mate
674math
675moan
676mocha
677moist
678mold
679mom
680moody
681mop
682morse
683most
684motor
685motto
686mount
687mouse
688mousy
689mouth
690move
691movie
692mower
693mud
694mug
695mulch
696mule
697mull
698mumbo
699mummy
700mural
701muse
702music
703musky
704mute
705nacho
706nag
707nail
708name
709nanny
710nap
711navy
712near
713neat
714neon
715nerd
716nest
717net
718next
719niece
720ninth
721nutty
722oak
723oasis
724oat
725ocean
726oil
727old
728olive
729omen
730onion
731only
732ooze
733opal
734open
735opera
736opt
737otter
738ouch
739ounce
740outer
741oval
742oven
743owl
744ozone
745pace
746pagan
747pager
748palm
749panda
750panic
751pants
752panty
753paper
754park
755party
756pasta
757patch
758path
759patio
760payer
761pecan
762penny
763pep
764perch
765perky
766perm
767pest
768petal
769petri
770petty
771photo
772plank
773plant
774plaza
775plead
776plot
777plow
778pluck
779plug
780plus
781poach
782pod
783poem
784poet
785pogo
786point
787poise
788poker
789polar
790polio
791polka
792polo
793pond
794pony
795poppy
796pork
797poser
798pouch
799pound
800pout
801power
802prank
803press
804print
805prior
806prism
807prize
808probe
809prong
810proof
811props
812prude
813prune
814pry
815pug
816pull
817pulp
818pulse
819puma
820punch
821punk
822pupil
823puppy
824purr
825purse
826push
827putt
828quack
829quake
830query
831quiet
832quill
833quilt
834quit
835quota
836quote
837rabid
838race
839rack
840radar
841radio
842raft
843rage
844raid
845rail
846rake
847rally
848ramp
849ranch
850range
851rank
852rant
853rash
854raven
855reach
856react
857ream
858rebel
859recap
860relax
861relay
862relic
863remix
864repay
865repel
866reply
867rerun
868reset
869rhyme
870rice
871rich
872ride
873rigid
874rigor
875rinse
876riot
877ripen
878rise
879risk
880ritzy
881rival
882river
883roast
884robe
885robin
886rock
887rogue
888roman
889romp
890rope
891rover
892royal
893ruby
894rug
895ruin
896rule
897runny
898rush
899rust
900rut
901sadly
902sage
903said
904saint
905salad
906salon
907salsa
908salt
909same
910sandy
911santa
912satin
913sauna
914saved
915savor
916sax
917say
918scale
919scam
920scan
921scare
922scarf
923scary
924scoff
925scold
926scoop
927scoot
928scope
929score
930scorn
931scout
932scowl
933scrap
934scrub
935scuba
936scuff
937sect
938sedan
939self
940send
941sepia
942serve
943set
944seven
945shack
946shade
947shady
948shaft
949shaky
950sham
951shape
952share
953sharp
954shed
955sheep
956sheet
957shelf
958shell
959shine
960shiny
961ship
962shirt
963shock
964shop
965shore
966shout
967shove
968shown
969showy
970shred
971shrug
972shun
973shush
974shut
975shy
976sift
977silk
978silly
979silo
980sip
981siren
982sixth
983size
984skate
985skew
986skid
987skier
988skies
989skip
990skirt
991skit
992sky
993slab
994slack
995slain
996slam
997slang
998slash
999slate
1000slaw
1001sled
1002sleek
1003sleep
1004sleet
1005slept
1006slice
1007slick
1008slimy
1009sling
1010slip
1011slit
1012slob
1013slot
1014slug
1015slum
1016slurp
1017slush
1018small
1019smash
1020smell
1021smile
1022smirk
1023smog
1024snack
1025snap
1026snare
1027snarl
1028sneak
1029sneer
1030sniff
1031snore
1032snort
1033snout
1034snowy
1035snub
1036snuff
1037speak
1038speed
1039spend
1040spent
1041spew
1042spied
1043spill
1044spiny
1045spoil
1046spoke
1047spoof
1048spool
1049spoon
1050sport
1051spot
1052spout
1053spray
1054spree
1055spur
1056squad
1057squat
1058squid
1059stack
1060staff
1061stage
1062stain
1063stall
1064stamp
1065stand
1066stank
1067stark
1068start
1069stash
1070state
1071stays
1072steam
1073steep
1074stem
1075step
1076stew
1077stick
1078sting
1079stir
1080stock
1081stole
1082stomp
1083stony
1084stood
1085stool
1086stoop
1087stop
1088storm
1089stout
1090stove
1091straw
1092stray
1093strut
1094stuck
1095stud
1096stuff
1097stump
1098stung
1099stunt
1100suds
1101sugar
1102sulk
1103surf
1104sushi
1105swab
1106swan
1107swarm
1108sway
1109swear
1110sweat
1111sweep
1112swell
1113swept
1114swim
1115swing
1116swipe
1117swirl
1118swoop
1119swore
1120syrup
1121tacky
1122taco
1123tag
1124take
1125tall
1126talon
1127tamer
1128tank
1129taper
1130taps
1131tarot
1132tart
1133task
1134taste
1135tasty
1136taunt
1137thank
1138thaw
1139theft
1140theme
1141thigh
1142thing
1143think
1144thong
1145thorn
1146those
1147throb
1148thud
1149thumb
1150thump
1151thus
1152tiara
1153tidal
1154tidy
1155tiger
1156tile
1157tilt
1158tint
1159tiny
1160trace
1161track
1162trade
1163train
1164trait
1165trap
1166trash
1167tray
1168treat
1169tree
1170trek
1171trend
1172trial
1173tribe
1174trick
1175trio
1176trout
1177truce
1178truck
1179trump
1180trunk
1181try
1182tug
1183tulip
1184tummy
1185turf
1186tusk
1187tutor
1188tutu
1189tux
1190tweak
1191tweet
1192twice
1193twine
1194twins
1195twirl
1196twist
1197uncle
1198uncut
1199undo
1200unify
1201union
1202unit
1203untie
1204upon
1205upper
1206urban
1207used
1208user
1209usher
1210utter
1211value
1212vapor
1213vegan
1214venue
1215verse
1216vest
1217veto
1218vice
1219video
1220view
1221viral
1222virus
1223visa
1224visor
1225vixen
1226vocal
1227voice
1228void
1229volt
1230voter
1231vowel
1232wad
1233wafer
1234wager
1235wages
1236wagon
1237wake
1238walk
1239wand
1240wasp
1241watch
1242water
1243wavy
1244wheat
1245whiff
1246whole
1247whoop
1248wick
1249widen
1250widow
1251width
1252wife
1253wifi
1254wilt
1255wimp
1256wind
1257wing
1258wink
1259wipe
1260wired
1261wiry
1262wise
1263wish
1264wispy
1265wok
1266wolf
1267womb
1268wool
1269woozy
1270word
1271work
1272worry
1273wound
1274woven
1275wrath
1276wreck
1277wrist
1278xerox
1279yahoo
1280yam
1281yard
1282year
1283yeast
1284yelp
1285yield
1286yarn
1287yodel
1288yoga
1289yoyo
1290yummy
1291zebra
1292zero
1293zesty
1294zippy
1295zone
1296zoom
crates/notebook/tests/live_share.rs+23-8
......@@ -37,6 +37,12 @@ fn relay(config: relay::server::Config) -> String {
3737 url
3838}
3939
40/// Another secret than `secret`, as a guess makes one.
41fn mistaken(secret: &str) -> String {
42 let first = if secret.starts_with('A') { 'B' } else { 'A' };
43 format!("{first}{}", &secret[1..])
44}
45
4046fn hello(name: &str) -> Hello {
4147 Hello::new(name.into(), None).unwrap()
4248}
......@@ -338,13 +344,24 @@ fn wrong_codes_are_refused_and_counted() {
338344 let sharing = Sharing::new("").unwrap();
339345 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
340346 let code = code(&host);
341 let number = code.split('-').next().unwrap();
342 let wrong = format!("{number}-violet-ocelot");
347 let (number, secret) = notebook::live::code::parse(&code).unwrap();
348 let wrong = notebook::live::code::format(number, &mistaken(&secret)).unwrap();
343349 let join = |code: &str| share::join(hello("Mallory"), code, "", None, Some(&url));
344350 assert_eq!(join("not a code").unwrap_err(), Refusal::Malformed);
351 // A symbol mistyped fails its check here, spending none of the two tries before a burn.
352 let typo = format!(
353 "{}{}",
354 if code.starts_with('7') { '8' } else { '7' },
355 &code[1..]
356 );
357 assert_eq!(join(&typo).unwrap_err(), Refusal::Malformed);
358 assert_eq!(
359 join(&code.to_lowercase().replace('-', " ")).map(|_| ()),
360 Ok(())
361 );
345362 assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong);
346363 assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong);
347 // The code burned, and the host shares new words, under a number of their own.
364 // The code burned, and the host shares a new secret, under a number of its own.
348365 until("the code never changed", || {
349366 host.code()
350367 .is_some_and(|now| now != code && share::code(&now).is_some())
......@@ -354,11 +371,9 @@ fn wrong_codes_are_refused_and_counted() {
354371 join(&code).unwrap_err(),
355372 Refusal::Expired | Refusal::NoOne
356373 ));
357 let number = fresh.split('-').next().unwrap();
358 assert_eq!(
359 join(&format!("{number}-violet-ocelot")).unwrap_err(),
360 Refusal::Wrong
361 );
374 let (number, secret) = notebook::live::code::parse(&fresh).unwrap();
375 let wrong = notebook::live::code::format(number, &mistaken(&secret)).unwrap();
376 assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong);
362377 // A third wrong code in a minute locks this network out, even from the right code.
363378 assert!(matches!(
364379 join(&fresh).unwrap_err(),
crates/snowbound/src/share.rs+2-2
......@@ -61,7 +61,7 @@ enum Status {
6161/// What to tell someone whose code didn't open a notebook.
6262fn refusal(refusal: &Refusal) -> String {
6363 match refusal {
64 Refusal::Malformed => "Enter the code as it was given, like 412-violet-otter.".into(),
64 Refusal::Malformed => "Check the code. It looks like 7KQ-4MZ-9XR.".into(),
6565 Refusal::Wrong => "That code or password doesn’t open a notebook. Check it with the \
6666 person sharing."
6767 .into(),
......@@ -449,7 +449,7 @@ impl State {
449449 text(ui, "what", "Enter the code from the person sharing.", false);
450450 labelled(ui, "Code:", |ui| {
451451 let spec = field_spec(ui);
452 ui::text_field(ui, code_field(), &mut dialog.code, "412-violet-otter", spec);
452 ui::text_field(ui, code_field(), &mut dialog.code, "7KQ-4MZ-9XR", spec);
453453 if let Some(node) = ui.access(code_field()) {
454454 node.set_label("Code");
455455 }