diff --git a/services/gam/Cargo.toml b/services/gam/Cargo.toml index c74c4aa6c..f69adc09d 100644 --- a/services/gam/Cargo.toml +++ b/services/gam/Cargo.toml @@ -34,6 +34,11 @@ tts-frontend = {path="../tts"} qrcode = { version = "0.12", default-features = false } +# used in bip39 dialogue +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +sha2 = {path="../engine-sha512"} +digest = "0.9.0" + [target.'cfg(not(any(windows,unix)))'.dependencies] utralib = {path = "../../utralib"} diff --git a/services/gam/locales/i18n.json b/services/gam/locales/i18n.json index f97b7ebc6..f35fccd35 100644 --- a/services/gam/locales/i18n.json +++ b/services/gam/locales/i18n.json @@ -41,6 +41,48 @@ "zh": "", "en-tts": "Checked " }, + "bip39.invalid_word": { + "en": "Invalid word!", + "ja": "無効な単語", + "zh": "无效词", + "en-tts": "Invalid word!" + }, + "bip39.waiting": { + "en": "Waiting for input...", + "ja": "入力を待っています...", + "zh": "等待输入...", + "en-tts": "Waiting for input..." + }, + "bip39.enter_to_complete": { + "en": "Press enter to accept current phrase, or keep typing...", + "ja": "Enterキーを押して現在のフレーズを受け入れるか、入力を続けます...", + "zh": "按 Enter 接受当前短语,或继续输入...", + "en-tts": "Press enter to accept current phrase, or keep typing..." + }, + "bip39.start_typing": { + "en": "Start typing...", + "ja": "入力を開始します...", + "zh": "开始打字……", + "en-tts": "Start typing..." + }, + "bip39.invalid_phrase": { + "en": "Phrase is not valid", + "ja": "フレーズが無効です", + "zh": "短语无效", + "en-tts": "Phrase is not valid" + }, + "bip39.valid_phrase": { + "en": "Valid input:", + "ja": "有効な入力:", + "zh": "有效输入:", + "en-tts": "Valid input:" + }, + "bip39.invalid_bytes": { + "en": "Invalid BIP39 data length", + "ja": "無効なBIP39データ長", + "zh": "BIP39 数据长度无效", + "en-tts": "Invalid BIP39 data length" + }, "progress.increment": { "en": "", "ja": "", diff --git a/services/gam/src/api.rs b/services/gam/src/api.rs index 642587c2e..19cd0e933 100644 --- a/services/gam/src/api.rs +++ b/services/gam/src/api.rs @@ -200,6 +200,11 @@ pub(crate) enum Opcode { SetDebugLevel = 28, Quit = 29, + + /// Bip39 operations -- the GAM has the word list, so to avoid duplicating code it offers a conversion service. + Bip39toBytes = 30, + BytestoBip39 = 31, + Bip39Suggestions = 32, } #[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] @@ -292,3 +297,10 @@ pub struct GidRecord { pub gid: Gid, pub canvas_type: CanvasType, } + +#[derive(Debug, Copy, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Default)] +pub struct Bip39Ipc { + pub data: [u8; 32], + pub data_len: u32, + pub words: [Option>; 24], +} diff --git a/services/gam/src/bip39.rs b/services/gam/src/bip39.rs new file mode 100644 index 000000000..e6ab51fa4 --- /dev/null +++ b/services/gam/src/bip39.rs @@ -0,0 +1,276 @@ +/// Eventually more language support can be added here: +/// +/// In order to integrate this well, we need to re-do the language build +/// system to be based off of af #cfg feature, so that we can pick up +/// the feature in this crate and select the right word list. +/// +/// We don't compile all the word lists in because code size is precious. +/// +/// Each language should simply create its table assigning to be symbol +/// `const BIP39_TABLE: [&'static str; 2048]`. This allows the rest of +/// the code to refer to the table without change, all we do is swap out +/// which language module is included in the two lines below. +pub mod en; +pub use en::*; +use sha2::*; +use digest::Digest; + +#[derive(Debug, Eq, PartialEq)] +pub enum Bip39Error { + InvalidLength, + InvalidChecksum, + InvalidWordAt(usize), +} + +/// This routine takes an array of bytes and attempts to return an array of Bip39 +/// words. If the bytes do not conform to a valid length, we return `Bip39Error::InvalidLength`. +/// A `Vec::` is returned in case the caller wants to do stupid formatting tricks +/// on the words (saving them effort of parsing a single concatenated String). +pub(crate) fn bytes_to_bip39(bytes: &Vec::) -> Result, Bip39Error> { + let mut result = Vec::::new(); + match bytes.len() { + 16 | 20 | 24 | 28 | 32 => (), + _ => return Err(Bip39Error::InvalidLength) + } + let mut hasher = sha2::Sha256::new(); + hasher.update(bytes); + let digest = hasher.finalize(); + let checksum_bits = bytes.len() / 4; + let checksum = digest.as_slice()[0] >> (8 - checksum_bits); + + let mut bits_in_bucket = 0; + let mut bucket = 0u32; + for &b in bytes { + bucket <<= 8; + bucket |= b as u32; + bits_in_bucket += 8; + if bits_in_bucket >= 11 { + let codeword = bucket >> (bits_in_bucket - 11); + bucket &= !((0b111_1111_1111u32) << (bits_in_bucket - 11)); + bits_in_bucket -= 11; + result.push(BIP39_TABLE[codeword as usize].to_string()); + } + } + assert!(bits_in_bucket + checksum_bits == 11); + bucket <<= checksum_bits; + bucket |= checksum as u32; + assert!(bucket < 2048); + result.push(BIP39_TABLE[bucket as usize].to_string()); + Ok(result) +} + +/// The caller must provide a list of words parsed into individual Bip39 words. +/// The words are case-insensitive. However, if any word is invalid, the routine +/// will return `InvalidWordAt(index of invalid word)` at the first invalid word +/// detected (it may not be the only invalid word). +pub(crate) fn bip39_to_bytes(bip39: &Vec::) -> Result, Bip39Error> { + // this implementation favors small runtime memory allocation over performance + // sifting through a list of 2048 words is reasonably fast, even if doing up to 24 times; + // this is especially in comparison to the screen redraw times. We could also create + // a HashSet or something or a tree to do the search faster but in this system, we fight + // for even 4kiB of RAM savings at times. + // The inefficiency is especially small in comparison to the ridiculous SHA256 computation + // that has to happen to checksum the result. + + match bip39.len() { + 12 | 15 | 18 | 21 | 24 => (), + _ => return Err(Bip39Error::InvalidLength) + } + + let mut indices = Vec::::new(); + for (index, bip) in bip39.iter().enumerate() { + if let Some(i) = BIP39_TABLE.iter().position(|&x| x == bip) { + indices.push(i as u32); + } else { + return Err(Bip39Error::InvalidWordAt(index)); + } + } + + // collate into u8 vec + let mut data = Vec::::new(); + let mut bucket = 0u32; + let mut bits_in_bucket = 0; + for index in indices { + // add bits to bucket + bucket = (bucket << 11) | index; + bits_in_bucket += 11; + + while bits_in_bucket >= 8 { + // extract the top 8 bits from the bucket, put it into the result vector + data.push( + (bucket >> (bits_in_bucket - 8)) as u8 + ); + // mask off the "used up" bits + bucket &= !(0b1111_1111 << bits_in_bucket - 8); + + // subtract the used bits out of the bucket + bits_in_bucket -= 8; + } + } + // the bucket should now just contain the checksum + let entered_checksum = if bits_in_bucket == 0 { + // edge case of exactly enough checksum bits to fill a byte (happens in 256-bit case) + data.pop().unwrap() + } else { + bucket as u8 + }; + + let mut hasher = sha2::Sha256::new(); + hasher.update(&data); + let digest = hasher.finalize(); + let checksum_bits = data.len() / 4; + let checksum = digest.as_slice()[0] >> (8 - checksum_bits); + if checksum == entered_checksum { + Ok(data) + } else { + log::warn!("checksum didn't match: {:x} vs {:x}", checksum, entered_checksum); + Err(Bip39Error::InvalidChecksum) + } +} + +pub(crate) const BIP39_SUGGEST_LIMIT: usize = 5; +/// This turns a string into a list of suggestions. If the String is empty, the +/// suggestion list is empty. The suggestion list is limited to BIP39_SUGGEST_LIMIT hints. +pub(crate) fn suggest_bip39(start: &str) -> Vec:: { + let mut ret = Vec::::new(); + // first see if any prefixes match; stop when we find enough + for bip in BIP39_TABLE { + if bip.starts_with(start) { + ret.push(bip.to_string()); + if ret.len() >= BIP39_SUGGEST_LIMIT { + break; + } + } + } + if ret.len() > 0 { + return ret; + } + // no prefixes match, suggest substrings + for bip in BIP39_TABLE { + if bip.contains(start) { + ret.push(bip.to_string()); + if ret.len() >= BIP39_SUGGEST_LIMIT { + break; + } + } + } + ret +} + +/// This routine returns `true` if the given word is a valid BIP39 word. +#[allow(dead_code)] +pub(crate) fn is_valid_bip39(word: &str) -> bool { + let lword = word.to_ascii_lowercase(); + for w in BIP39_TABLE { + if lword == w { + return true + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + /// PAGE_SIZE is required to be a power of two. 0x1000 -> 0x1000 - 1 = 0xFFF, which forms the bitmasks. + fn test_11_to_8() { + let indices = [ + 0b00000110001, + 0b10110011110, + 0b01110010100, + 0b00110110010, + 0b10001011010, + 0b11100111111, + 0b01101010011, + 0b10000011000, + 0b01101011001, + 0b10110011111, + 0b10001001110, + 0b00111100110 + ]; + let refnum = 0b00000110001101100111100111001010000110110010100010110101110011111101101010011100000110000110101100110110011111100010011100011110u128; + + // 00000110001 10110011110 01110010100 00110110010 10001011010 11100111111 01101010011 10000011000 01101011001 10110011111 10001001110 00111100110 + // 0000_0110|001/1_0110|0111_10/01|1100_1010_0/001_10110010 10001011010 11100111111 01101010011 10000011000 01101011001 10110011111 10001001110 00111100110 + // 0000_0110 0011_0110 0111_1001 1100_1010 ... 0001101100101000101101011100111111011010100111000001100001101011001101100111111000100111000111100110 + + let mut refvec = refnum.to_be_bytes().to_vec(); + refvec.push(6); // checksum + + let mut data = Vec::::new(); + let mut bucket = 0u32; + let mut bits_in_bucket = 0; + for index in indices { + // add bits to bucket + bucket = (bucket << 11) | index; + bits_in_bucket += 11; + + while bits_in_bucket >= 8 { + // extract the top 8 bits from the bucket, put it into the result vector + data.push( + (bucket >> (bits_in_bucket - 8)) as u8 + ); + // mask off the "used up" bits + bucket &= !(0b1111_1111 << bits_in_bucket - 8); + + // subtract the used bits out of the bucket + bits_in_bucket -= 8; + } + } + if bits_in_bucket != 0 { + data.push(bucket as u8); + } + assert!(data.len() == refvec.len()); + for (index, (&a, &b)) in refvec.iter().zip(data.iter()).enumerate() { + if a != b { + println!("index {} error: a[{}{:x})] != b[{}({:x})]", index, a, a, b, b); + } else { + println!("index {} match: a[{}({:x})] == b[{}({:x})]", index, a, a, b, b); + } + assert!(a==b); + } + } + #[test] + fn test_bip39_to_bytes() { + let phrase = vec![ + "alert".to_string(), "record".to_string(), "income".to_string(), "curve".to_string(), + "mercy".to_string(), "tree".to_string(), "heavy".to_string(), "loan".to_string(), + "hen".to_string(), "recycle".to_string(), "mean".to_string(), "devote".to_string() + ]; + let refnum = 0b00000110001101100111100111001010000110110010100010110101110011111101101010011100000110000110101100110110011111100010011100011110u128; + let mut refvec = refnum.to_be_bytes().to_vec(); + // refvec.push(6); // checksum + + assert_eq!(Ok(refvec), bip39_to_bytes(&phrase)); + } + #[test] + fn test_is_valid_bip39() { + assert_eq!(is_valid_bip39("alert"), true); + assert_eq!(is_valid_bip39("rEcOrD"), true); + assert_eq!(is_valid_bip39("foobar"), false); + assert_eq!(is_valid_bip39(""), false); + } + #[test] + fn test_suggest_prefix() { + let suggestions = suggest_bip39("ag"); + let reference = vec![ + "again".to_string(), + "age".to_string(), + "agent".to_string(), + "agree".to_string(), + ]; + assert_eq!(suggestions, reference); + } + #[test] + fn test_bytes_to_bip39() { + let refnum = 0b00000110001101100111100111001010000110110010100010110101110011111101101010011100000110000110101100110110011111100010011100011110u128; + let refvec = refnum.to_be_bytes().to_vec(); + let phrase = vec![ + "alert".to_string(), "record".to_string(), "income".to_string(), "curve".to_string(), + "mercy".to_string(), "tree".to_string(), "heavy".to_string(), "loan".to_string(), + "hen".to_string(), "recycle".to_string(), "mean".to_string(), "devote".to_string() + ]; + assert_eq!(bytes_to_bip39(&refvec), Ok(phrase)); + } +} diff --git a/services/gam/src/bip39/en.rs b/services/gam/src/bip39/en.rs new file mode 100644 index 000000000..7a7f8e459 --- /dev/null +++ b/services/gam/src/bip39/en.rs @@ -0,0 +1,2050 @@ +pub const BIP39_TABLE: [&'static str; 2048] = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "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", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "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", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +]; diff --git a/services/gam/src/lib.rs b/services/gam/src/lib.rs index 3cd068a10..f691c23cd 100644 --- a/services/gam/src/lib.rs +++ b/services/gam/src/lib.rs @@ -444,6 +444,66 @@ impl Gam { ) .expect("couldn't set debug level"); } + pub fn bytes_to_bip39(&self, bytes: &Vec::) -> Result, xous::Error> { + match bytes.len() { + 16 | 20 | 24 | 28 | 32 => (), + _ => return Err(xous::Error::InvalidString) + } + let mut ipc = Bip39Ipc::default(); + ipc.data[..bytes.len()].copy_from_slice(&bytes); + ipc.data_len = bytes.len() as u32; + let mut buf = Buffer::into_buf(ipc).or(Err(xous::Error::InternalError))?; + buf.lend_mut(self.conn, Opcode::BytestoBip39.to_u32().unwrap()).or(Err(xous::Error::InternalError)).expect("couldn't send RaiseMenu opcode"); + let result = buf.to_original::().unwrap(); + let mut ret = Vec::::new(); + for word in result.words { + if let Some(w) = word { + ret.push(w.as_str().unwrap().to_string()); + } + } + if ret.len() == 0 { + Err(xous::Error::InvalidString) + } else { + Ok(ret) + } + } + pub fn bip39_to_bytes(&self, bip39: &Vec::) -> Result, xous::Error> { + match bip39.len() { + 12 | 15 | 18 | 21 | 24 => (), + _ => return Err(xous::Error::InvalidString) + } + let mut ipc = Bip39Ipc::default(); + for (word, slot) in bip39.iter().zip(ipc.words.iter_mut()) { + *slot = Some(xous_ipc::String::from_str(word)) + } + let mut buf = Buffer::into_buf(ipc).or(Err(xous::Error::InternalError))?; + buf.lend_mut(self.conn, Opcode::Bip39toBytes.to_u32().unwrap()).or(Err(xous::Error::InternalError)).expect("couldn't send RaiseMenu opcode"); + let result = buf.to_original::().unwrap(); + if result.data_len == 0 { + Err(xous::Error::InvalidString) + } else { + Ok( + result.data[..result.data_len as usize].to_vec() + ) + } + } + pub fn bip39_suggestions(&self, start: &str) -> Result, xous::Error> { + let mut ipc = Bip39Ipc::default(); + // we abuse this struct a bit by shoving the lookup phrase into a u8-array... + let checked_start = start.as_bytes(); + ipc.data[..checked_start.len().min(8)].copy_from_slice(&checked_start[..checked_start.len().min(8)]); + ipc.data_len = checked_start.len().min(8) as u32; + let mut buf = Buffer::into_buf(ipc).or(Err(xous::Error::InternalError))?; + buf.lend_mut(self.conn, Opcode::Bip39Suggestions.to_u32().unwrap()).or(Err(xous::Error::InternalError)).expect("couldn't send RaiseMenu opcode"); + let result = buf.to_original::().unwrap(); + let mut suggestions = Vec::::new(); + for word in result.words { + if let Some(w) = word { + suggestions.push(w.as_str().unwrap().to_string()) + } + } + Ok(suggestions) + } } use core::sync::atomic::{AtomicU32, Ordering}; diff --git a/services/gam/src/main.rs b/services/gam/src/main.rs index 20ebf1d64..9f8539b40 100644 --- a/services/gam/src/main.rs +++ b/services/gam/src/main.rs @@ -12,6 +12,7 @@ mod layouts; use layouts::*; mod contexts; use contexts::*; +mod bip39; use graphics_server::*; use xous_ipc::{Buffer, String}; @@ -614,6 +615,55 @@ fn wrapped_main() -> ! { } xous::return_scalar(msg.sender, 1).expect("couldn't ack self test"); }), + Some(Opcode::Bip39toBytes) => { + let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) }; + let mut spec = buffer.to_original::().unwrap(); + let mut phrase = Vec::::new(); + for maybe_word in spec.words { + if let Some(word) = maybe_word { + phrase.push(word.as_str().unwrap().to_string()); + } + } + match bip39::bip39_to_bytes(&phrase) { + Ok(data) => { + spec.data_len = data.len() as u32; + spec.data[..data.len()].copy_from_slice(&data); + } + Err(_) => { + // zero-length data indicates an error + spec.data_len = 0; + } + } + buffer.replace(spec).unwrap(); + } + Some(Opcode::BytestoBip39) => { + let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) }; + let mut spec = buffer.to_original::().unwrap(); + let data = spec.data[..spec.data_len as usize].to_vec(); + match bip39::bytes_to_bip39(&data) { + Ok(phrase) => { + for (word, returned) in phrase.iter().zip(spec.words.iter_mut()) { + *returned = Some(xous_ipc::String::from_str(word)); + } + } + Err(_) => { + for returned in spec.words.iter_mut() { + *returned = None; + } + } + } + buffer.replace(spec).unwrap(); + } + Some(Opcode::Bip39Suggestions) => { + let mut buffer = unsafe { Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap()) }; + let mut spec = buffer.to_original::().unwrap(); + let start = std::str::from_utf8(&spec.data[..spec.data_len as usize]).unwrap_or(""); + let suggestions = bip39::suggest_bip39(start); + for (word, returned) in suggestions.iter().zip(spec.words.iter_mut()) { + *returned = Some(xous_ipc::String::from_str(word)); + } + buffer.replace(spec).unwrap(); + } Some(Opcode::Quit) => break, None => {log::error!("unhandled message {:?}", msg);} } diff --git a/services/gam/src/modal.rs b/services/gam/src/modal.rs index d765fd453..41aa6d15b 100644 --- a/services/gam/src/modal.rs +++ b/services/gam/src/modal.rs @@ -18,6 +18,8 @@ pub use consoleinput::*; mod image; #[cfg(feature="ditherpunk")] pub use image::*; +mod bip39entry; +pub use bip39entry::*; use enum_dispatch::enum_dispatch; @@ -37,6 +39,7 @@ pub const MAX_ITEMS: usize = 8; #[enum_dispatch(ActionApi)] pub enum ActionType { TextEntry, + Bip39Entry, RadioButtons, CheckBoxes, Slider, @@ -76,6 +79,14 @@ impl ItemName { } } +#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Copy, Clone, Eq, PartialEq, Default)] +pub struct Bip39EntryPayload { + // up to 32 bytes (256 bits) could be entered + pub data: [u8; 32], + // the actual length entered is reported here + pub len: u32, +} + #[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Copy, Clone, Eq, PartialEq, Default)] pub struct TextEntryPayload { dirty: bool, diff --git a/services/gam/src/modal/bip39entry.rs b/services/gam/src/modal/bip39entry.rs new file mode 100644 index 000000000..15f219556 --- /dev/null +++ b/services/gam/src/modal/bip39entry.rs @@ -0,0 +1,394 @@ +use crate::*; +use graphics_server::api::*; +use locales::t; +use glyphstyle::GlyphStyle; + +use xous_ipc::Buffer; +use std::string::String; + +use core::fmt::Write; +use core::cell::Cell; + +pub struct Bip39Entry { + pub is_password: bool, + pub action_conn: xous::CID, + pub action_opcode: u32, + pub accepted_words: Vec::, + pub user_input: String, + pub payload: Option>, + pub suggested_words: Vec::, + suggestion_index: Cell::, + line_height: Cell::, + margin: Cell::, + gam: crate::Gam, +} + +impl Default for Bip39Entry { + fn default() -> Self { + // this is slightly horriffic but we keep the bip39 phrase book inside the GAM and not + // in this client-side library to avoid having 24kiB of phrases being compiled into every + // process that potentially handles graphics. So, this modal needs a dedicated GAM connection + // to handle bip39 processing on key entry. :-/ + let xns = xous_names::XousNames::new().unwrap(); + let gam = crate::Gam::new(&xns).unwrap(); + + Self { + is_password: Default::default(), + action_conn: Default::default(), + action_opcode: Default::default(), + accepted_words: Vec::new(), + user_input: String::new(), + payload: None, + suggested_words: Vec::new(), + suggestion_index: Cell::new(0), + line_height: Cell::new(0), + margin: Cell::new(0), + gam, + } + } +} + +impl Bip39Entry { + pub fn new( + is_password: bool, + action_conn: xous::CID, + action_opcode: u32, + ) -> Self { + + Self { + is_password, + action_conn, + action_opcode, + ..Default::default() + } + } +} + +const NUM_RECCOS: i16 = 5; +const AESTHETIC_GAP: i16 = 5; +const STYLE_OVERRIDE: GlyphStyle = GlyphStyle::Bold; +const ACCEPTED_WORD_LINES: i16 = 4; +const STATUS_LINES: i16 = 3; + +impl ActionApi for Bip39Entry { + fn set_action_opcode(&mut self, op: u32) {self.action_opcode = op} + fn is_password(&self) -> bool { + self.is_password + } + /// The total canvas height is computed with this API call + /// The canvas height is not dynamically adjustable for modals. + fn height(&self, glyph_height: i16, margin: i16) -> i16 { + /* + + cat <-- active entry 1 line + 2x margin + ------------------- + * cat <-- suggestion list (up to 5 lines, blank space if no suggestions) + catalog "INVALID WORD" if active entry line has no matches + catch "Start typing..." if line is blank and BIP39 invalid + category "Presse enter to accept or keep typing..." if line is blank and BIP39 is valid + + ------------------- + Invalid phrase <-- whether or not the words are valid (1 line) + 0x1234 <-- binary data preview (if word is valid, blank if not valid) (1 line) + ------------------- + eager dish museum <-- words accepted so far (6 lines reserved) + pole aisle ... + + up/down arrow: selects which suggestion. auto-inserts suggested text into active entry line + enter/center button: selects the suggestion. can only select valid suggestions + backspace while text is present: remove one letter + backspace while no text is present: remove latest word in bip39 list + + 24 word example: + oven color flag rich custom + crawl century oak decad + dilemma evolve company + original arctic cat clever + truth air chuckle radar + polar silly soda idle + */ + // the glyph_height is an opaque value because the final value depends upon a lookup table + // stored in the graphics_server crate. To obtain this would require a handle to that server + // which is private to the GAM. Thus we receive a copy of this from our caller and stash it + // here for future reference. `glyph_height` can change depending upon the locale; in particular, + // CJK languages have taller glyphs. + self.line_height.set(glyph_to_height_hint(STYLE_OVERRIDE) as i16 + 2); + self.margin.set(6); + + // compute the overall_height of the entry fields + let overall_height = + glyph_height // active entry + + 2*margin // divider line + + glyph_height * NUM_RECCOS // recco list + + 2*margin // divider line + + glyph_height * STATUS_LINES // status line + + 2*margin // divider line + + glyph_height * ACCEPTED_WORD_LINES // words accepted so far + + margin * 2 // top/bottom spacing + ; + + overall_height + } + fn redraw(&self, at_height: i16, modal: &Modal) { + let color = if self.is_password { + PixelColor::Light + } else { + PixelColor::Dark + }; + + let mut current_height = at_height; + let bullet_margin = 17; + + // ------ draw the current text entry ------- + let mut tv = TextView::new( + modal.canvas, + TextBounds::BoundingBox(Rectangle::new( + Point::new(self.margin.get(), current_height), + Point::new(modal.canvas_width - self.margin.get(), current_height + self.line_height.get() + self.margin.get())) + )); + tv.text.clear(); + tv.bounds_computed = None; + tv.draw_border = false; + tv.invert = self.is_password; + tv.style = STYLE_OVERRIDE; + write!(tv, "{}", self.user_input).ok(); + modal.gam.post_textview(&mut tv).expect("couldn't post tv"); + + current_height += self.line_height.get(); + // ------- draw the line under the text entry ----- + modal.gam.draw_line(modal.canvas, Line::new_with_style( + Point::new(self.margin.get(), current_height + AESTHETIC_GAP), // we manually place the line to an aesthetic position + Point::new(modal.canvas_width - self.margin.get(), current_height + AESTHETIC_GAP), + DrawStyle::new(color, color, 1)) + ).expect("couldn't draw entry line"); + + current_height += self.margin.get() * 2; + // ------- draw the suggestion list -------- + let mut draw_at = current_height; // split current_height into a separate variable for this op, because it's a variable length list + if self.suggested_words.len() > 0 { + for (index, recco) in self.suggested_words.iter().enumerate() { + if index >= NUM_RECCOS as usize { + break; + } + if index as i16 == self.suggestion_index.get() { + // draw the dot + let mut tv = TextView::new( + modal.canvas, + TextBounds::BoundingBox(Rectangle::new( + Point::new(self.margin.get() + AESTHETIC_GAP, draw_at - 4), + Point::new(modal.canvas_width - self.margin.get(), draw_at + self.line_height.get() - 4)) + )); + tv.text.clear(); + tv.bounds_computed = None; + tv.draw_border = false; + tv.invert = self.is_password; + write!(tv, "•").unwrap(); // emoji glyph will be summoned in this case + modal.gam.post_textview(&mut tv).expect("couldn't post tv"); + } + let left_text_margin = self.margin.get() + AESTHETIC_GAP + bullet_margin; // space for the bullet point on the left + + // draw the reccommendation text + let mut tv = TextView::new( + modal.canvas, + TextBounds::BoundingBox(Rectangle::new( + Point::new(left_text_margin, draw_at), + Point::new(modal.canvas_width - (self.margin.get() + bullet_margin), draw_at + self.line_height.get())) + )); + tv.ellipsis = true; + tv.invert = self.is_password; + tv.text.clear(); + tv.draw_border =false; + tv.margin = Point::new(0, 0); + write!(tv, "{}", recco).ok(); + modal.gam.post_textview(&mut tv).expect("couldn't post tv"); + + draw_at += self.line_height.get(); + } + } else { + // no suggested words: + // put in some error/guidance text instead of a recco list + let mut guidance = String::new(); + if self.user_input.len() > 0 { + guidance.push_str(t!("bip39.invalid_word", xous::LANG)); + } else { + // no input yet, give some options + if self.payload.is_some() { + guidance.push_str(t!("bip39.enter_to_complete", xous::LANG)); + } else { + guidance.push_str(t!("bip39.start_typing", xous::LANG)); + } + } + let mut tv = TextView::new( + modal.canvas, + TextBounds::CenteredTop(Rectangle::new( + Point::new(self.margin.get(), draw_at), + Point::new(modal.canvas_width - self.margin.get(), draw_at + self.line_height.get() * NUM_RECCOS)) + )); + tv.invert = self.is_password; + tv.text.clear(); + tv.draw_border =false; + write!(tv, "{}", guidance).ok(); + modal.gam.post_textview(&mut tv).expect("couldn't post tv"); + } + + current_height += self.line_height.get() * NUM_RECCOS; + // -------- draw a divider line ---------- + modal.gam.draw_line(modal.canvas, Line::new_with_style( + Point::new(self.margin.get(), current_height + self.margin.get()), + Point::new(modal.canvas_width - self.margin.get(), current_height + self.margin.get()), + DrawStyle::new(color, color, 1)) + ).expect("couldn't draw entry line"); + + current_height += self.margin.get() * 2; + // ------- draw word list ------- + let mut words = String::new(); + for word in self.accepted_words.iter() { + words.push_str(word); + words.push_str(" "); + } + let mut tv = TextView::new( + modal.canvas, + TextBounds::CenteredTop(Rectangle::new( + Point::new(self.margin.get(), current_height), + Point::new(modal.canvas_width - self.margin.get(), current_height + self.line_height.get() * ACCEPTED_WORD_LINES + self.margin.get())) + )); + tv.invert = self.is_password; + tv.ellipsis = true; + tv.text.clear(); + tv.draw_border =false; + tv.style = STYLE_OVERRIDE; + write!(tv, "{}", words).ok(); + modal.gam.post_textview(&mut tv).expect("couldn't post tv"); + + current_height += self.line_height.get() * ACCEPTED_WORD_LINES; + // -------- draw a divider line ---------- + modal.gam.draw_line(modal.canvas, Line::new_with_style( + Point::new(self.margin.get(), current_height + self.margin.get()), + Point::new(modal.canvas_width - self.margin.get(), current_height + self.margin.get()), + DrawStyle::new(color, color, 1)) + ).expect("couldn't draw entry line"); + + current_height += self.margin.get() * 2; + // ------- status -------- + let mut status = String::new(); + if let Some(p) = &self.payload { + status.push_str(t!("bip39.valid_phrase", xous::LANG)); + status.push_str( " "); + status.push_str(&hex::encode(&p)); + } else { + if self.user_input.len() == 0 && self.accepted_words.len() == 0 { + status.push_str(t!("bip39.waiting", xous::LANG)); + } else { + status.push_str(t!("bip39.invalid_phrase", xous::LANG)); + } + } + let mut tv = TextView::new( + modal.canvas, + TextBounds::CenteredTop(Rectangle::new( + Point::new(self.margin.get(), current_height), + Point::new(modal.canvas_width - self.margin.get(), current_height + self.line_height.get() * STATUS_LINES + self.margin.get())) + )); + tv.invert = self.is_password; + tv.text.clear(); + tv.draw_border = false; + tv.ellipsis = true; + write!(tv, "{}", status).ok(); + modal.gam.post_textview(&mut tv).expect("couldn't post tv"); + } + + fn key_action(&mut self, k: char) -> (Option, bool) { + let can_move_downwards = !(self.suggestion_index.get() + 1 == NUM_RECCOS); + let can_move_upwards = !(self.suggestion_index.get() - 1 < 0); + + log::info!("key_action: {}", k); + match k { + '∴' | '\u{d}' => { + if self.user_input.len() == 0 { + if let Some(data) = &self.payload { + let mut ret = Bip39EntryPayload::default(); + ret.data[..data.len()].copy_from_slice(&data); + ret.len = data.len() as u32; + let buf = Buffer::into_buf(ret).expect("couldn't convert message to payload"); + buf.send(self.action_conn, self.action_opcode).map(|_| ()).expect("couldn't send action message"); + return (None, true) + } + } else { + if self.suggested_words.len() > 0 { + if (self.suggestion_index.get() as usize) < self.suggested_words.len() { // should "always" be true, but good to check + self.accepted_words.push(self.suggested_words[self.suggestion_index.get() as usize].to_string()); + match self.gam.bip39_to_bytes(&self.accepted_words) { + Ok(data) => { + self.payload = Some(data) + } + _ => self.payload = None, + } + self.user_input.clear(); + self.suggested_words.clear(); + self.suggestion_index.set(0); + } + } + } + } + '↑' => { + if can_move_upwards { + self.suggestion_index.set(self.suggestion_index.get() - 1); + } + } + '↓' => { + if can_move_downwards { + self.suggestion_index.set(self.suggestion_index.get() + 1); + } + } + '\u{0}' => { + // ignore null messages + } + '\u{8}' => { // backspace + #[cfg(feature="tts")] + { + let xns = xous_names::XousNames::new().unwrap(); + let tts = tts_frontend::TtsFrontend::new(&xns).unwrap(); + tts.tts_blocking(locales::t!("input.delete-tts", xous::LANG)).unwrap(); + } + if self.user_input.len() > 0 { // don't backspace if we have no string. + self.user_input.pop(); + if self.user_input.len() > 0 { + self.suggested_words = self.gam.bip39_suggestions(&self.user_input) + .unwrap_or(Vec::::new()); + } else { + self.suggested_words.clear(); + } + self.suggestion_index.set(0); + } else { + self.accepted_words.pop(); + self.suggested_words.clear(); + self.suggestion_index.set(0); + match self.gam.bip39_to_bytes(&self.accepted_words) { + Ok(data) => { + self.payload = Some(data) + } + _ => self.payload = None, + } + } + } + _ => { // text entry + #[cfg(feature="tts")] + { + let xns = xous_names::XousNames::new().unwrap(); + let tts = tts_frontend::TtsFrontend::new(&xns).unwrap(); + tts.tts_blocking(&k.to_string()).unwrap(); + } + if k.is_ascii_alphabetic() { // ignore any other input, since it's invalid. + let lk = k.to_lowercase(); + for c in lk { + self.user_input.push(c); + } + // now regenerate all the hints + self.suggested_words = self.gam.bip39_suggestions(&self.user_input) + .unwrap_or(Vec::::new()); + self.suggestion_index.set(0); + } + } + } + (None, false) + } +} \ No newline at end of file