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