From e4894ba238db9072ce9ef8b36459ae084254a1c1 Mon Sep 17 00:00:00 2001 From: TheCryptoDonkey Date: Fri, 25 Sep 2026 09:10:26 +0100 Subject: [PATCH 1/2] feat: show the board's request words and restrict labels to printable ASCII The board now shows a four-word request code on its "ADD UNLOCK PHONE" card, derived from the phone's one-off enrolment pubkey. The owner must compare it against the phone's own copy before holding the button, since the phone made the key and a browser's copy proves nothing. Cambium now derives and shows the same words next to the enrol code, using spoken-token's en-v1 word list copied verbatim from the firmware. The firmware also now refuses any label that is not printable ASCII, on the cable as well as over the relay. fitLabel transliterates accented and fullwidth characters to their plain ASCII base via NFKD decomposition and drops everything else, rather than only trimming to the byte limit, so it can never hand the board something it will reject. --- .../forgesworn/cambium/unlock/Enrolment.kt | 43 ++- .../forgesworn/cambium/unlock/SpokenWords.kt | 271 ++++++++++++++++++ .../cambium/unlock/UnlockEnrolActivity.kt | 5 + .../main/res/layout/activity_unlock_enrol.xml | 23 ++ app/src/main/res/values/strings.xml | 1 + .../cambium/unlock/EnrolmentTest.kt | 44 ++- .../cambium/unlock/SpokenWordsTest.kt | 22 ++ 7 files changed, 396 insertions(+), 13 deletions(-) create mode 100644 app/src/main/kotlin/dev/forgesworn/cambium/unlock/SpokenWords.kt create mode 100644 app/src/test/kotlin/dev/forgesworn/cambium/unlock/SpokenWordsTest.kt diff --git a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/Enrolment.kt b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/Enrolment.kt index eca58a0..433202e 100644 --- a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/Enrolment.kt +++ b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/Enrolment.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.net.URLDecoder import java.net.URLEncoder +import java.text.Normalizer /** * What Cambium shows (as a QR and as text) when it asks to become an unlock phone: @@ -40,14 +41,21 @@ data class EnrolmentCode( private val HEX64 = Regex("^[0-9a-f]{64}$") private val HEX32 = Regex("^[0-9a-f]{32}$") - /** Trims [raw] to the board's label limit without splitting a UTF-8 sequence. */ + /** + * Fits [raw] to the board's label limit as printable ASCII (0x20-0x7E), which is all the + * firmware now accepts for a label -- on the cable as well as over the relay, so this must + * hold whatever the board's own model name string throws at it. NFKD decomposition first, + * so an accented letter or a fullwidth character (e.g. "é", "swim") reduces to its plain + * ASCII base rather than being dropped outright; anything left outside the printable range + * (control characters, emoji, combining marks NFKD peeled off) is simply removed. A label + * built entirely of non-ASCII input still falls back to "phone", matching the empty-input + * case. + */ fun fitLabel(raw: String): String { - val trimmed = raw.trim().ifEmpty { "phone" } - var end = trimmed.length - while (trimmed.substring(0, end).toByteArray(Charsets.UTF_8).size > MAX_LABEL_BYTES) end-- - // Never leave half of a surrogate pair behind. - if (end > 0 && Character.isHighSurrogate(trimmed[end - 1])) end-- - return trimmed.substring(0, end) + val ascii = Normalizer.normalize(raw, Normalizer.Form.NFKD).filter { it.code in 0x20..0x7E } + val trimmed = ascii.trim().ifEmpty { "phone" } + // Every remaining character is single-byte ASCII, so length and byte count agree. + return trimmed.take(MAX_LABEL_BYTES).trim().ifEmpty { "phone" } } fun parse(text: String): EnrolmentCode? { @@ -102,6 +110,27 @@ fun checkCode(ephemeralPubkeyHex: String): String? { private const val CHECK_CONTEXT = "heartwood-unlock:enrol-check" +/** + * The four words the board shows on its "ADD UNLOCK PHONE" card before the press, derived from + * this phone's own one-off enrolment pubkey P: spoken-token's `deriveToken(P, + * 'heartwood-unlock:enrol-request', 0, { format: 'words', count: 4 })`, i.e. word `i` is + * `WORDLIST[uint16_be(digest[2i..2i+2]) % 2048]` of `HMAC-SHA256(P, utf8(context) || counter_be32)`. + * Cambium must show the same words: the phone is the trusted side (it made P), so the owner + * compares the board's words against *this* screen, not a browser's copy of the enrolment code, + * which proves nothing. Held to the firmware's own frozen vectors in `EnrolmentTest`. + */ +fun requestWords(enrolPubkeyHex: String): String? { + val key = enrolPubkeyHex.hexToBytesOrNull()?.takeIf { it.size == 32 } ?: return null + val mac = javax.crypto.Mac.getInstance("HmacSHA256").apply { init(javax.crypto.spec.SecretKeySpec(key, "HmacSHA256")) } + val digest = mac.doFinal(REQUEST_CONTEXT.toByteArray(Charsets.UTF_8) + ByteArray(4)) + return (0 until 4).joinToString(" ") { i -> + val index = ((digest[2 * i].toInt() and 0xFF) shl 8) or (digest[2 * i + 1].toInt() and 0xFF) + SpokenWords.WORDLIST[index % SpokenWords.WORDLIST.size] + } +} + +private const val REQUEST_CONTEXT = "heartwood-unlock:enrol-request" + /** The sealed hand-off's plaintext, as the phone opens it: `{v:1, id, s, relays}`. */ class HandOff(val id: Long, val slotSecret: ByteArray, val relays: List) { fun wipe() = slotSecret.fill(0) diff --git a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/SpokenWords.kt b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/SpokenWords.kt new file mode 100644 index 0000000..531809b --- /dev/null +++ b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/SpokenWords.kt @@ -0,0 +1,271 @@ +package dev.forgesworn.cambium.unlock + +/** + * spoken-token's en-v1 word list (forgesworn/spoken-token, MIT): 2048 words of 3 to 8 lower-case + * letters, chosen to be told apart when read aloud. Copied verbatim from heartwood-esp32's + * `common/src/spoken_words.txt` -- the firmware compiles in the same bytes -- so this must be + * regenerated together with that file if it ever changes, never edited independently. + * + * SHA-256 of the words joined with "\n" (no trailing newline) is pinned by + * `SpokenWordsTest`: `0334930ebdfbc76e81ec914515d7567ca85738a6bf3069249d97df951d44661c`. + */ +object SpokenWords { + val WORDLIST: List = listOf( + "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", + "access", "accident", "account", "accuse", "achieve", "acid", "acorn", "acoustic", + "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", + "add", "addict", "address", "adjust", "admiral", "admit", "adult", "advance", + "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", + "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", + "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", + "alpha", "alpine", "already", "also", "always", "amateur", "amazing", "amber", + "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", + "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", + "anvil", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", + "april", "apron", "arch", "arctic", "area", "arena", "argue", "arm", + "armed", "armor", "army", "around", "arrange", "arrive", "arrow", "art", + "artefact", "artist", "artwork", "ask", "aspect", "asset", "assist", "assume", + "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", + "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", + "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", + "baby", "bachelor", "bacon", "badge", "badger", "bag", "bakery", "balance", + "balcony", "ball", "balm", "bamboo", "banana", "banjo", "banner", "bar", + "barely", "bargain", "barrel", "base", "basic", "basil", "basket", "battle", + "beach", "beacon", "bean", "beauty", "because", "become", "beef", "beetle", + "before", "begin", "behave", "behind", "belfry", "believe", "below", "belt", + "bench", "benefit", "berry", "best", "better", "between", "beyond", "bicycle", + "bid", "bike", "bind", "biology", "birch", "bird", "birth", "bishop", + "bitter", "black", "blame", "blanket", "bleak", "bless", "bloom", "blossom", + "blouse", "blue", "blur", "blush", "board", "boat", "bobcat", "body", + "boil", "bone", "bonfire", "bonus", "book", "boost", "border", "boring", + "borrow", "boss", "bottom", "bounce", "bouquet", "box", "boy", "bracket", + "brain", "branch", "brand", "brass", "brave", "bread", "breaker", "breeze", + "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "bronze", + "brook", "broom", "brother", "brown", "brush", "bubble", "buckle", "buddy", + "budget", "buffalo", "bugle", "build", "bulb", "bulk", "bumble", "bundle", + "bunker", "burger", "burrow", "burst", "bus", "bushel", "business", "busy", + "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", + "cairn", "cake", "call", "calm", "camel", "camera", "camp", "can", + "canal", "cancel", "candy", "canoe", "canopy", "canvas", "canyon", "capable", + "cape", "capital", "captain", "car", "caravan", "carbon", "card", "cargo", + "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", + "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", + "cave", "cedar", "ceiling", "celery", "cellar", "cement", "census", "century", + "cereal", "certain", "chair", "chalk", "champion", "change", "chapter", "charge", + "charter", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", + "chest", "chestnut", "chicken", "chief", "child", "chimney", "choice", "choose", + "chuckle", "chunk", "churn", "cider", "cigar", "cinnamon", "circle", "citizen", + "city", "civil", "claim", "clam", "clap", "clarify", "claw", "clay", + "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", + "clip", "cloak", "clock", "clog", "close", "cloth", "cloud", "clown", + "club", "clump", "cluster", "clutch", "coach", "coast", "cobalt", "cocoa", + "coconut", "code", "codex", "coffee", "coil", "coin", "collect", "color", + "column", "combine", "come", "comet", "comfort", "comic", "common", "company", + "concert", "condor", "conduct", "confirm", "congress", "connect", "consider", "consul", + "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", + "cork", "corn", "cornet", "correct", "cosmos", "cost", "cotton", "couch", + "cougar", "country", "couple", "course", "cousin", "cover", "coyote", "crack", + "cradle", "craft", "cram", "crane", "crater", "crawl", "cream", "credit", + "creek", "crew", "cricket", "crisp", "critic", "croft", "crop", "cross", + "crouch", "crowd", "crown", "crucial", "cruise", "crumble", "crunch", "cry", + "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", + "curve", "cushion", "custom", "cute", "cycle", "cypress", "dad", "dagger", + "dahlia", "damp", "damsel", "dance", "danger", "dapple", "daring", "dash", + "daughter", "dawn", "day", "deal", "debate", "decade", "december", "decide", + "decline", "decorate", "decrease", "defense", "define", "defy", "degree", "delay", + "deliver", "delta", "demand", "demise", "denial", "denim", "dentist", "depart", + "depend", "deposit", "depot", "depth", "deputy", "derive", "describe", "desert", + "design", "desk", "detail", "detect", "develop", "device", "devote", "diagram", + "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", + "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", + "dish", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", + "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", + "door", "dorsal", "double", "dove", "draft", "drafter", "dragon", "drake", + "drama", "drastic", "draw", "dream", "dress", "drift", "drifter", "drill", + "drink", "drive", "drop", "droplet", "drum", "drummer", "dry", "duck", + "dulcet", "dune", "dungeon", "during", "dusk", "dust", "dutch", "duty", + "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", + "east", "easy", "echo", "ecology", "economy", "edge", "edgeway", "edit", + "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", + "elegant", "element", "elephant", "elevator", "elite", "elm", "else", "embark", + "ember", "embody", "embrace", "emerald", "emerge", "emotion", "employ", "empower", + "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", + "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", + "enroll", "ensign", "ensure", "enter", "entire", "entry", "envelope", "episode", + "epoch", "equal", "equip", "era", "erase", "erode", "erosion", "error", + "escape", "essay", "essence", "estate", "estuary", "eternal", "ether", "ethics", + "everest", "evidence", "evil", "evolve", "exact", "example", "exchange", "excite", + "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exist", "exit", + "exotic", "expand", "expect", "explain", "express", "extend", "extra", "eye", + "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "falcon", + "fall", "fallow", "false", "fame", "family", "famous", "fancy", "fantasy", + "farm", "fashion", "fat", "father", "fathom", "fatigue", "favorite", "feature", + "february", "federal", "fee", "feed", "feel", "female", "fence", "fennel", + "fern", "festival", "fetch", "fever", "fiber", "fiction", "fiddle", "field", + "figure", "file", "film", "filter", "final", "finch", "find", "finger", + "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", + "fix", "fjord", "flag", "flagon", "flame", "flannel", "flash", "flat", + "flavor", "flicker", "flight", "flint", "flip", "float", "flock", "floor", + "floret", "fluid", "flush", "flutter", "fly", "foal", "foam", "focus", + "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", + "forge", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", + "found", "foundry", "fox", "foxglove", "fragile", "frame", "frequent", "fresco", + "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", + "fruit", "fuel", "fun", "funny", "furnace", "furrow", "future", "gadget", + "gain", "galaxy", "gallery", "galley", "game", "gap", "garage", "garbage", + "garden", "garland", "garlic", "garment", "garnet", "gas", "gasp", "gate", + "gather", "gauge", "gaze", "gazelle", "general", "genius", "genre", "gentle", + "genuine", "gesture", "geyser", "giant", "gibbon", "gift", "giggle", "ginger", + "giraffe", "girl", "give", "glacier", "glad", "glance", "glare", "glass", + "glen", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", + "glue", "goat", "goblet", "goddess", "gold", "golden", "good", "goose", + "gopher", "gorge", "gorilla", "gospel", "gossip", "govern", "gown", "grab", + "grace", "grain", "granite", "grant", "grape", "grass", "gravity", "great", + "green", "grid", "grocery", "group", "grow", "grunt", "guard", "guess", + "guide", "guilt", "guitar", "guppy", "gust", "gym", "habit", "half", + "hamlet", "hammer", "hammock", "hamster", "hand", "happy", "harbor", "hard", + "harness", "harvest", "hat", "have", "hawk", "hawthorn", "head", "health", + "heart", "hearth", "heavy", "hedgehog", "height", "hello", "helmet", "help", + "hen", "herald", "hermit", "hero", "heron", "hickory", "hidden", "high", + "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", + "hole", "holiday", "hollow", "home", "homeward", "honey", "hood", "hope", + "horizon", "horn", "hornet", "horse", "hospital", "host", "hotel", "hour", + "hover", "howler", "hub", "huge", "human", "humble", "humor", "hundred", + "hungry", "hunt", "hunter", "hurdle", "hurry", "husband", "hybrid", "ice", + "icon", "idea", "identify", "idle", "igloo", "ignore", "ill", "image", + "imitate", "immune", "impact", "improve", "inch", "include", "income", "increase", + "index", "indicate", "indigo", "indoor", "industry", "infant", "inflict", "inform", + "inhale", "inherit", "initial", "inject", "inkwell", "inlet", "inmate", "inner", + "innocent", "input", "inquiry", "insect", "inside", "inspire", "install", "intact", + "interest", "into", "invest", "invite", "involve", "inward", "iris", "iron", + "island", "issue", "item", "ivory", "jacket", "jade", "jaguar", "jar", + "jasmine", "javelin", "jazz", "jeans", "jelly", "jersey", "jewel", "job", + "join", "joke", "jostle", "journal", "journey", "joy", "jubilee", "judge", + "juice", "jumble", "jump", "junco", "jungle", "junior", "juniper", "just", + "kangaroo", "kayak", "keen", "keep", "keeper", "kelp", "kennel", "kernel", + "kestrel", "ketchup", "kettle", "key", "kick", "kid", "kidney", "kind", + "kindle", "kingdom", "kinglet", "kipper", "kiss", "kit", "kitchen", "kite", + "kitten", "kiwi", "knapsack", "knee", "knife", "knock", "lab", "label", + "labor", "ladder", "lady", "lake", "lamp", "language", "lantern", "lapis", + "laptop", "larch", "large", "later", "latin", "laugh", "laundry", "laurel", + "lava", "lavender", "law", "lawn", "layer", "lazy", "leader", "leaf", + "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", + "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", + "liar", "liberty", "library", "license", "lichen", "life", "lift", "light", + "like", "limit", "linden", "link", "linnet", "lion", "liquid", "list", + "little", "live", "lizard", "llama", "load", "loan", "lobster", "local", + "lock", "locust", "lodge", "logic", "long", "loom", "loop", "lottery", + "lotus", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", + "lumen", "lunar", "lunch", "luxury", "machine", "mackerel", "magic", "magnet", + "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", + "mansion", "mantis", "manual", "maple", "marble", "march", "margin", "marine", + "market", "marriage", "marsh", "marten", "mask", "masonry", "mass", "master", + "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", + "mean", "measure", "mechanic", "medal", "media", "melody", "melt", "member", + "memory", "mention", "menu", "mercy", "merge", "merit", "merlin", "merry", + "mesa", "mesh", "message", "metal", "method", "micron", "middle", "midnight", + "milk", "millet", "million", "mimic", "mind", "minimum", "minnow", "minor", + "minute", "miracle", "mirage", "mirror", "miss", "mistake", "mix", "mixed", + "mixture", "moat", "mobile", "model", "modify", "mohawk", "mom", "moment", + "monarch", "mongrel", "monitor", "monkey", "month", "moon", "moose", "moral", + "more", "morning", "mortar", "mosaic", "mosquito", "mother", "motion", "motor", + "mountain", "mouse", "move", "movie", "much", "muffin", "mullet", "multiply", + "muscle", "museum", "mushroom", "music", "muslin", "mussel", "must", "mustang", + "mutual", "myrtle", "myself", "mystery", "myth", "naive", "name", "napkin", + "narrow", "narwhal", "nation", "nature", "near", "neck", "nectar", "need", + "negative", "neither", "nephew", "nest", "nester", "net", "nettle", "network", + "neutral", "never", "news", "newt", "next", "nice", "nimble", "noble", + "noggin", "noise", "nomad", "nominee", "noodle", "normal", "north", "nose", + "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", + "nurse", "nut", "nutmeg", "oak", "oakmoss", "oasis", "obey", "object", + "oblige", "observe", "obsidian", "obtain", "ocean", "octave", "october", "odor", + "off", "offer", "office", "often", "oil", "okay", "old", "olive", + "olympic", "omit", "once", "onion", "online", "only", "onyx", "opal", + "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", + "orchid", "order", "ordinary", "organ", "orient", "original", "oriole", "orphan", + "osprey", "ostrich", "other", "otter", "outdoor", "outer", "outpost", "output", + "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", + "ozone", "pact", "paddle", "page", "pagoda", "palace", "palm", "panda", + "panel", "panther", "paper", "parade", "parent", "park", "parrot", "party", + "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", + "payment", "peanut", "peasant", "pelican", "pen", "pencil", "people", "pepper", + "perfect", "permit", "person", "phone", "photo", "phrase", "physical", "piano", + "picnic", "picture", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", + "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", + "pledge", "pluck", "plug", "poem", "poet", "point", "polar", "pole", + "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", + "post", "potato", "pottery", "powder", "power", "practice", "praise", "predict", + "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", + "print", "priority", "private", "prize", "process", "produce", "profit", "program", + "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", + "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "puppy", "purchase", + "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", + "quantum", "quarter", "question", "quick", "quiz", "quote", "rabbit", "race", + "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", + "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", + "raw", "razor", "ready", "real", "reason", "rebuild", "recall", "receive", + "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", + "regular", "relax", "release", "relief", "rely", "remain", "remember", "remove", + "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", + "require", "rescue", "resource", "response", "result", "retire", "retreat", "return", + "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", + "rich", "ride", "ridge", "rifle", "ring", "ripple", "risk", "ritual", + "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", + "roof", "rookie", "room", "rose", "rotate", "round", "route", "royal", + "rubber", "rug", "rule", "run", "runway", "rural", "saddle", "sadness", + "safe", "salad", "salmon", "salon", "salt", "salute", "same", "sample", + "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", + "scan", "school", "science", "scorpion", "scout", "screen", "script", "scrub", + "search", "season", "seat", "second", "secret", "section", "security", "seed", + "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", + "service", "session", "settle", "setup", "seven", "shadow", "shallow", "share", + "shed", "shell", "sheriff", "shield", "shift", "shine", "shoe", "shoot", + "shop", "short", "shoulder", "shove", "shrimp", "shuffle", "shy", "sibling", + "sick", "side", "sight", "sign", "silent", "silk", "silly", "silver", + "similar", "simple", "since", "sister", "situate", "six", "size", "skate", + "sketch", "ski", "skill", "skin", "skirt", "slab", "slam", "sleep", + "slice", "slide", "slight", "slim", "small", "smart", "smile", "smooth", + "snack", "snake", "snow", "soap", "soccer", "social", "sock", "soda", + "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", + "soon", "sort", "soul", "sound", "soup", "source", "south", "space", + "spare", "spatial", "speak", "special", "speed", "spell", "spend", "sphere", + "spice", "spider", "spike", "spin", "spirit", "spoil", "sponsor", "spoon", + "sport", "spot", "spray", "spread", "spring", "spy", "square", "squirrel", + "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", + "state", "stay", "steak", "stem", "step", "stereo", "stick", "still", + "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", + "strong", "student", "style", "subject", "submit", "subway", "success", "such", + "sudden", "sugar", "suggest", "suit", "summer", "sunny", "sunset", "super", + "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", + "sustain", "swap", "swarm", "sweet", "swift", "swim", "swing", "switch", + "sword", "symbol", "syrup", "system", "table", "tackle", "tag", "talent", + "talk", "tape", "task", "taste", "taxi", "teach", "team", "tell", + "tennis", "term", "test", "text", "thank", "theme", "then", "theory", + "they", "this", "thought", "thrive", "throw", "thumb", "thunder", "ticket", + "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", + "tissue", "title", "toast", "today", "toddler", "together", "toilet", "token", + "tomato", "tomorrow", "tone", "tonight", "tool", "tooth", "top", "topic", + "topple", "torch", "tortoise", "toss", "total", "tourist", "tower", "town", + "toy", "track", "trade", "traffic", "train", "transfer", "trap", "travel", + "tray", "treat", "trend", "tribe", "trick", "trigger", "trim", "trip", + "trophy", "truck", "true", "trumpet", "truth", "try", "tuition", "tunnel", + "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", + "two", "type", "typical", "umbrella", "unable", "unaware", "uncle", "uncover", + "under", "undo", "unfair", "unfold", "uniform", "unique", "unit", "universe", + "unknown", "unlock", "until", "unusual", "update", "upgrade", "uphold", "upon", + "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", + "useless", "usual", "utility", "vague", "valid", "valley", "valve", "vapor", + "various", "vast", "vehicle", "velvet", "vendor", "venue", "verb", "verify", + "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", + "video", "village", "vintage", "violin", "virtual", "visa", "visit", "visual", + "vital", "vivid", "vocal", "voice", "volcano", "vote", "voyage", "wagon", + "walk", "wall", "walnut", "want", "warm", "warrior", "wash", "wasp", + "water", "wave", "way", "wealth", "weasel", "web", "wedding", "weekend", + "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", + "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", + "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", + "wise", "wish", "witness", "wolf", "woman", "wonder", "wool", "word", + "work", "world", "worry", "worth", "wreck", "wrestle", "yard", "year", + "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo", + ) +} diff --git a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt index 5363538..bf404df 100644 --- a/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt +++ b/app/src/main/kotlin/dev/forgesworn/cambium/unlock/UnlockEnrolActivity.kt @@ -100,6 +100,7 @@ class UnlockEnrolActivity : AppCompatActivity() { val rendezvous = ByteArray(16).also { SecureRandom().nextBytes(it) }.toHex() val code = EnrolmentCode(pubkeyHex, rendezvous, EnrolmentCode.fitLabel(Build.MODEL), relays).encode() binding.enrolCode.text = code + binding.enrolWords.text = requestWords(pubkeyHex).orEmpty() binding.enrolQr.setImageBitmap(BarcodeEncoder().encodeBitmap(code, BarcodeFormat.QR_CODE, 720, 720)) binding.enrolCopyButton.setOnClickListener { getSystemService(ClipboardManager::class.java).setPrimaryClip(ClipData.newPlainText("Heartwood unlock enrolment", code)) @@ -177,6 +178,8 @@ class UnlockEnrolActivity : AppCompatActivity() { pending = handOff binding.enrolQr.isVisible = false binding.enrolCode.isVisible = false + binding.enrolWords.isVisible = false + binding.enrolWordsHint.isVisible = false binding.enrolCopyButton.isVisible = false binding.enrolStatus.text = getString(R.string.enrol_received) binding.enrolCheckCode.text = checkCode(envelope.ephemeralPubkeyHex).orEmpty() @@ -277,6 +280,8 @@ class UnlockEnrolActivity : AppCompatActivity() { binding.enrolBody.isVisible = false binding.enrolQr.isVisible = false binding.enrolCode.isVisible = false + binding.enrolWords.isVisible = false + binding.enrolWordsHint.isVisible = false binding.enrolCopyButton.isVisible = false binding.enrolStatus.text = message } diff --git a/app/src/main/res/layout/activity_unlock_enrol.xml b/app/src/main/res/layout/activity_unlock_enrol.xml index c861b91..9d0531c 100644 --- a/app/src/main/res/layout/activity_unlock_enrol.xml +++ b/app/src/main/res/layout/activity_unlock_enrol.xml @@ -50,6 +50,29 @@ android:textSize="10sp" tools:text="heartwood-unlock:enrol?v=1&p=…" /> + + + +