#[rustfmt::skip]
pub const ADJECTIVES: [&str; 256] = [
"amber", "ancient", "arctic", "autumn", "azure", "balmy", "bitter", "blank",
"bold", "brave", "brick", "bright", "brisk", "bronze", "buoyant", "calm",
"candid", "carved", "cedar", "chalky", "cheerful", "chilly", "chosen", "civic",
"clean", "clear", "clever", "cobalt", "coral", "costly", "cosmic", "crimson",
"crisp", "cryptic", "curious", "daily", "damp", "dapper", "daring", "dawn",
"deep", "dense", "dewy", "distant", "dormant", "dotted", "downy", "drifting",
"dusky", "eager", "early", "earnest", "eastern", "easy", "elder", "electric",
"elegant", "ember", "empty", "endless", "equal", "even", "exact", "faint",
"famous", "fancy", "feral", "fertile", "fierce", "final", "flaxen", "fleet",
"flinty", "fluent", "foggy", "fond", "formal", "frank", "frosty", "frugal",
"gallant", "gentle", "giant", "gilded", "glad", "glassy", "gleaming", "golden",
"graceful", "grand", "granite", "grassy", "gravel", "greater", "green", "grey",
"hollow", "honest", "humble", "husky", "icy", "idle", "indigo", "inland",
"inner", "ivory", "jagged", "jolly", "joyful", "keen", "kindly", "lacy",
"lanky", "lasting", "latter", "lavish", "lean", "level", "light", "lilac",
"limber", "linen", "little", "lively", "lofty", "lonely", "loyal", "lucid",
"lunar", "magnetic", "marble", "marine", "mellow", "merry", "mild", "mindful",
"minor", "misty", "modest", "molten", "mossy", "muted", "narrow", "native",
"nautical", "neat", "nimble", "noble", "northern", "novel", "oaken", "olive",
"opal", "orderly", "outer", "pale", "patient", "peach", "pearl", "placid",
"plain", "pleasant", "plucky", "polar", "polished", "prairie", "prime", "private",
"prompt", "proud", "prudent", "pure", "quaint", "quick", "quiet", "rapid",
"rare", "raven", "ready", "regal", "restless", "rich", "rising", "roasted",
"robust", "rocky", "rosy", "royal", "ruddy", "rugged", "rustic", "sable",
"sacred", "sandy", "scarlet", "seaside", "secret", "serene", "shady", "shallow",
"sharp", "sheer", "shining", "shrewd", "silent", "silken", "silver", "simple",
"singing", "sleepy", "slender", "smoky", "smooth", "snowy", "solar", "solemn",
"solid", "somber", "southern", "sparse", "spiced", "splendid", "spotless", "spry",
"stable", "staid", "starry", "steady", "steep", "stern", "still", "stony",
"stout", "straight", "striking", "subtle", "sudden", "sunlit", "sunny", "supple",
"swift", "tall", "tame", "tangy", "tender", "thrifty", "tidal", "tidy",
"timely", "tiny", "towering", "tranquil", "true", "twilight", "twin", "umber",
];
#[rustfmt::skip]
pub const NOUNS: [&str; 256] = [
"acorn", "alcove", "almond", "anchor", "anvil", "apple", "arbor", "archer",
"armour", "arrow", "ashes", "aspen", "atlas", "aurora", "avenue", "awning",
"badger", "ballad", "bamboo", "banner", "barley", "basin", "beacon", "beagle",
"bedrock", "beetle", "bellows", "bergamot", "birch", "bison", "bistro", "blanket",
"bluff", "bobcat", "bonfire", "boulder", "bramble", "brandy", "bridge", "bristle",
"brook", "bugle", "bullfinch", "burrow", "cabin", "cactus", "cairn", "canopy",
"canyon", "caravan", "cardinal", "carrot", "cascade", "cedar", "cellar", "chalice",
"chapel", "chestnut", "chimney", "cinder", "cistern", "citadel", "clover", "cobble",
"compass", "condor", "copper", "coral", "cornice", "cottage", "coyote", "cradle",
"crater", "cricket", "crocus", "crossing", "crowbar", "cypress", "daffodil", "dahlia",
"dairy", "damson", "delta", "derrick", "dingo", "dolphin", "domino", "dormouse",
"dovecote", "dragon", "drumlin", "dune", "eagle", "echo", "eddy", "elkhorn",
"ember", "emerald", "estuary", "falcon", "fathom", "fennel", "fernlead", "ferry",
"fiddle", "finch", "firefly", "fjord", "flagon", "flannel", "flint", "foghorn",
"forest", "fountain", "foxglove", "fulcrum", "furrow", "gable", "gallery", "gander",
"garnet", "gazebo", "geyser", "ginger", "glacier", "glade", "granary", "granite",
"grotto", "gull", "gully", "hamlet", "harbor", "harvest", "hawthorn", "hazel",
"heather", "hedgerow", "hemlock", "heron", "hickory", "hollow", "hornet", "hostel",
"iceberg", "iguana", "inkwell", "ironwood", "island", "ivory", "jackdaw", "jasmine",
"jetty", "juniper", "kestrel", "kettle", "keystone", "kingfisher", "kiosk", "kittiwake",
"lagoon", "lantern", "larkspur", "lattice", "lavender", "ledger", "lemon", "lichen",
"lighthouse", "lilac", "limestone", "linnet", "lintel", "lobster", "locket", "lodestone",
"lupine", "lyrebird", "magnolia", "mallard", "mandolin", "mangrove", "mantle", "maple",
"marina", "marmot", "marsh", "meadow", "medley", "menhir", "mesa", "millrace",
"mineral", "minnow", "mistral", "mitten", "moorland", "mortar", "mosaic", "mustang",
"myrtle", "nectar", "nettle", "newel", "nightjar", "nutmeg", "oakleaf", "obelisk",
"ocelot", "okapi", "orchard", "oriole", "osprey", "otter", "outcrop", "oxbow",
"paddock", "pagoda", "palisade", "pampas", "pangolin", "pantry", "papyrio", "parapet",
"parsnip", "partridge", "pasture", "pavilion", "peacock", "pebble", "pelican", "pendant",
"penguin", "peony", "pewter", "pheasant", "pigeon", "pillar", "pinnacle", "piston",
"plateau", "plover", "plumage", "poppy", "porcupine", "portico", "possum", "prairie",
"primrose", "puffin", "pumice", "quarry", "quartz", "quilt", "quiver", "rampart",
];
pub fn fingerprint_of(sign_key_b64: &str, box_key_b64: Option<&str>) -> String {
match box_key_b64 {
Some(b) => pairs(&format!("{}\u{1f}{}", sign_key_b64.trim(), b.trim()), 4).join(" · "),
None => pairs(sign_key_b64, 4).join(" · "),
}
}
pub fn fingerprint(sign_key_b64: &str) -> String {
fingerprint_of(sign_key_b64, None)
}
pub fn label(sign_key_b64: &str) -> String {
pairs(sign_key_b64, 1).remove(0)
}
pub fn echo_matches(echo: &str, fingerprint: &str) -> bool {
let norm = |s: &str| -> Vec<String> {
s.to_lowercase()
.split(|c: char| !c.is_ascii_alphabetic())
.filter(|w| !w.is_empty())
.map(|w| w.to_string())
.collect()
};
let a = norm(echo);
!a.is_empty() && a == norm(fingerprint)
}
pub fn pairs(sign_key_b64: &str, n: usize) -> Vec<String> {
let h = crate::bundle::sha256_hex(sign_key_b64.trim().as_bytes());
let bytes: Vec<u8> = (0..h.len() / 2)
.map(|i| u8::from_str_radix(&h[i * 2..i * 2 + 2], 16).unwrap_or(0))
.collect();
(0..n)
.map(|i| {
let a = ADJECTIVES[bytes[i * 2] as usize];
let b = NOUNS[bytes[i * 2 + 1] as usize];
format!("{a} {b}")
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
#[test]
fn the_lists_are_full_and_have_no_repeats() {
assert_eq!(ADJECTIVES.len(), 256);
assert_eq!(NOUNS.len(), 256);
assert_eq!(ADJECTIVES.iter().collect::<BTreeSet<_>>().len(), 256);
assert_eq!(NOUNS.iter().collect::<BTreeSet<_>>().len(), 256);
for w in ADJECTIVES.iter().chain(NOUNS.iter()) {
assert!(
w.chars().all(|c| c.is_ascii_lowercase()),
"{w} is not plain lowercase ascii, so it cannot be read aloud reliably"
);
assert!((3..=11).contains(&w.len()), "{w} is a bad length to say");
}
}
#[test]
fn the_label_is_the_first_pair_of_the_fingerprint() {
let k = "yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=";
assert!(fingerprint(k).starts_with(&label(k)));
assert_eq!(fingerprint(k).split(" · ").count(), 4);
assert_eq!(label(k).split(' ').count(), 2);
let b = "7amQkpwKszbeF801n5EzMRm4jQlx3BylW8c59lsDcX0=";
let fp = fingerprint_of(k, Some(b));
assert!(
!fp.starts_with(&label(k)),
"the label is a prefix of a sealable key's fingerprint, so either the \
fingerprint stopped covering the encryption key or the two hashes \
collided -- and every piece of output that distinguishes them is now wrong"
);
}
#[test]
fn an_echo_matches_on_words_not_on_punctuation() {
let fp = fingerprint("yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=");
assert!(echo_matches(&fp, &fp));
assert!(echo_matches(&fp.replace(" · ", ", "), &fp));
assert!(echo_matches(&fp.to_uppercase(), &fp));
assert!(echo_matches(&format!(" {fp} "), &fp));
let three = fp.split(" · ").take(3).collect::<Vec<_>>().join(" · ");
assert!(!echo_matches(&three, &fp));
assert!(!echo_matches("", &fp));
let mut parts: Vec<&str> = fp.split(" · ").collect();
parts.reverse();
assert!(!echo_matches(&parts.join(" · "), &fp));
}
#[test]
fn different_keys_say_different_things() {
let a = fingerprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
let b = fingerprint("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=");
assert_ne!(a, b);
assert_eq!(
a,
fingerprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
);
}
#[test]
fn surrounding_whitespace_does_not_change_the_words() {
let k = "yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=";
assert_eq!(fingerprint(k), fingerprint(&format!(" {k}\n")));
}
#[test]
fn the_words_cover_the_encryption_key_too() {
let sign = "yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=";
let good = "7amQkpwKszbeF801n5EzMRm4jQlx3BylW8c59lsDcX0=";
let evil = "uqYqbyJEhhofgimN/j016/IpGKrK3lp/1CN7JdUHYAc=";
assert_ne!(
fingerprint_of(sign, Some(good)),
fingerprint_of(sign, Some(evil)),
"substituting the encryption key did not change the words"
);
assert_ne!(fingerprint_of(sign, Some(good)), fingerprint_of(sign, None));
assert_ne!(
fingerprint_of("ab", Some("c")),
fingerprint_of("a", Some("bc"))
);
}
#[test]
fn whitespace_in_either_half_does_not_change_the_words() {
let k = "yomSmC3TalJ5IM+H0yvkhhWVab4ujiAVIQFR4WvdXiM=";
let b = "7amQkpwKszbeF801n5EzMRm4jQlx3BylW8c59lsDcX0=";
assert_eq!(
fingerprint_of(k, Some(b)),
fingerprint_of(&format!(" {k}\n"), Some(&format!(" {b} ")))
);
}
}