mirror of
https://github.com/saymrwulf/xous-core.git
synced 2026-11-08 04:56:43 +00:00
add bip39 primitives to the GAM, including an interactive entry dialog
This commit is contained in:
parent
5cc51ebdaf
commit
4087312a2c
9 changed files with 2900 additions and 0 deletions
|
|
@ -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"}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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<xous_ipc::String::<8>>; 24],
|
||||
}
|
||||
|
|
|
|||
276
services/gam/src/bip39.rs
Normal file
276
services/gam/src/bip39.rs
Normal file
|
|
@ -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::<String>` 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::<u8>) -> Result<Vec::<String>, Bip39Error> {
|
||||
let mut result = Vec::<String>::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::<String>) -> Result<Vec::<u8>, 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::<u32>::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::<u8>::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::<String> {
|
||||
let mut ret = Vec::<String>::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::<u8>::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));
|
||||
}
|
||||
}
|
||||
2050
services/gam/src/bip39/en.rs
Normal file
2050
services/gam/src/bip39/en.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -444,6 +444,66 @@ impl Gam {
|
|||
)
|
||||
.expect("couldn't set debug level");
|
||||
}
|
||||
pub fn bytes_to_bip39(&self, bytes: &Vec::<u8>) -> Result<Vec::<std::string::String>, 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::<Bip39Ipc, _>().unwrap();
|
||||
let mut ret = Vec::<std::string::String>::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::<std::string::String>) -> Result<Vec::<u8>, 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::<Bip39Ipc, _>().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<Vec::<std::string::String>, 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::<Bip39Ipc, _>().unwrap();
|
||||
let mut suggestions = Vec::<std::string::String>::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};
|
||||
|
|
|
|||
|
|
@ -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::<Bip39Ipc, _>().unwrap();
|
||||
let mut phrase = Vec::<std::string::String>::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::<Bip39Ipc, _>().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::<Bip39Ipc, _>().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);}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
394
services/gam/src/modal/bip39entry.rs
Normal file
394
services/gam/src/modal/bip39entry.rs
Normal file
|
|
@ -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::<String>,
|
||||
pub user_input: String,
|
||||
pub payload: Option<Vec::<u8>>,
|
||||
pub suggested_words: Vec::<String>,
|
||||
suggestion_index: Cell::<i16>,
|
||||
line_height: Cell::<i16>,
|
||||
margin: Cell::<i16>,
|
||||
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<ValidatorErr>, 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::<String>::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::<String>::new());
|
||||
self.suggestion_index.set(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, false)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue