From 1d475fd9c2044804feac041335d5ef851b83dade Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Sat, 29 Jun 2024 17:50:14 +0200 Subject: [PATCH] feat: display mnemonic on wallet creation and support for login --- nextgraph/src/local_broker.rs | 37 +++++ ng-app/src-tauri/src/lib.rs | 26 +++ ng-app/src/api.ts | 8 +- ng-app/src/lib/Login.svelte | 223 +++++++++++++++++--------- ng-app/src/routes/WalletCreate.svelte | 34 +++- ng-app/src/worker.js | 15 +- ng-repo/src/errors.rs | 1 + ng-sdk-js/src/lib.rs | 47 ++++++ ng-wallet/src/bip39.rs | 26 +++ ng-wallet/src/lib.rs | 7 +- ng-wallet/src/types.rs | 3 + 11 files changed, 332 insertions(+), 95 deletions(-) diff --git a/nextgraph/src/local_broker.rs b/nextgraph/src/local_broker.rs index 79725c0..edf7e60 100644 --- a/nextgraph/src/local_broker.rs +++ b/nextgraph/src/local_broker.rs @@ -40,6 +40,7 @@ use ng_verifier::types::*; use ng_verifier::verifier::Verifier; use ng_wallet::emojis::encode_pazzle; +use ng_wallet::bip39::encode_mnemonic; use ng_wallet::{create_wallet_first_step_v0, create_wallet_second_step_v0, types::*}; #[cfg(not(target_family = "wasm"))] @@ -1555,6 +1556,27 @@ pub fn wallet_open_with_pazzle( Ok(opened_wallet) } +#[doc(hidden)] +/// This is a bit hard to use as the mnemonic words are encoded in u16. +/// prefer the function wallet_open_with_mnemonic_words +pub fn wallet_open_with_mnemonic( + wallet: &Wallet, + mnemonic: Vec, + pin: [u8; 4], +) -> Result { + if mnemonic.len() != 12 { + return Err(NgError::InvalidMnemonic); + } + // Convert from vec to array. + let mut mnemonic_arr = [0u16; 12]; + for (place, element) in mnemonic_arr.iter_mut().zip(mnemonic.iter()) { + *place = *element; + } + let opened_wallet = ng_wallet::open_wallet_with_mnemonic(wallet, mnemonic_arr, pin)?; + + Ok(opened_wallet) +} + /// Opens a wallet by providing an ordered list of words, and the pin. /// /// If you are opening a wallet that is already known to the LocalBroker, you must then call [wallet_was_opened]. @@ -1569,6 +1591,21 @@ pub fn wallet_open_with_pazzle_words( wallet_open_with_pazzle(wallet, encode_pazzle(pazzle_words)?, pin) } + +/// Opens a wallet by providing an ordered list of mnemonic words, and the pin. +/// +/// If you are opening a wallet that is already known to the LocalBroker, you must then call [wallet_was_opened]. +/// Otherwise, if you are importing, then you must call [wallet_import]. +pub fn wallet_open_with_mnemonic_words( + wallet: &Wallet, + mnemonic: &Vec, + pin: [u8; 4], +) -> Result { + let encoded: Vec = encode_mnemonic(mnemonic)?; + + wallet_open_with_mnemonic(wallet, encoded, pin) +} + /// Imports a wallet into the LocalBroker so the user can then access its content. /// /// the wallet should have been previous opened with [wallet_open_with_pazzle_words]. diff --git a/ng-app/src-tauri/src/lib.rs b/ng-app/src-tauri/src/lib.rs index 8a28099..20db194 100644 --- a/ng-app/src-tauri/src/lib.rs +++ b/ng-app/src-tauri/src/lib.rs @@ -86,6 +86,30 @@ async fn wallet_open_with_pazzle( Ok(wallet) } +#[tauri::command(rename_all = "snake_case")] +async fn wallet_open_with_mnemonic( + wallet: Wallet, + mnemonic: Vec, + pin: [u8; 4], + _app: tauri::AppHandle, +) -> Result { + let wallet = nextgraph::local_broker::wallet_open_with_mnemonic(&wallet, mnemonic, pin) + .map_err(|e| e.to_string())?; + Ok(wallet) +} + +#[tauri::command(rename_all = "snake_case")] +async fn wallet_open_with_mnemonic_words( + wallet: Wallet, + mnemonic_words: Vec, + pin: [u8; 4], + _app: tauri::AppHandle, +) -> Result { + let wallet = nextgraph::local_broker::wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, pin) + .map_err(|e| e.to_string())?; + Ok(wallet) +} + #[tauri::command(rename_all = "snake_case")] async fn wallet_get_file(wallet_name: String, app: tauri::AppHandle) -> Result<(), String> { let ser = nextgraph::local_broker::wallet_get_file(&wallet_name) @@ -493,6 +517,8 @@ impl AppBuilder { wallet_gen_shuffle_for_pazzle_opening, wallet_gen_shuffle_for_pin, wallet_open_with_pazzle, + wallet_open_with_mnemonic, + wallet_open_with_mnemonic_words, wallet_was_opened, wallet_create, wallet_read_file, diff --git a/ng-app/src/api.ts b/ng-app/src/api.ts index 9d7da81..0426a6c 100644 --- a/ng-app/src/api.ts +++ b/ng-app/src/api.ts @@ -16,6 +16,7 @@ const mapping = { "wallet_gen_shuffle_for_pazzle_opening": ["pazzle_length"], "wallet_gen_shuffle_for_pin": [], "wallet_open_with_pazzle": ["wallet","pazzle","pin"], + "wallet_open_with_mnemonic_words": ["wallet","mnemonic_words","pin"], "wallet_was_opened": ["opened_wallet"], "wallet_create": ["params"], "wallet_read_file": ["file"], @@ -169,7 +170,7 @@ const handler = { return false; } else if (path[0] === "get_local_url") { return false; - } else if (path[0] === "wallet_open_with_pazzle") { + } else if (path[0] === "wallet_open_with_pazzle" || path[0] === "wallet_open_with_mnemonic_words") { let arg:any = {}; args.map((el,ix) => arg[mapping[path[0]][ix]]=el) let img = Array.from(new Uint8Array(arg.wallet.V0.content.security_img)); @@ -177,9 +178,8 @@ const handler = { arg.wallet = {V0:{id:arg.wallet.V0.id, sig:arg.wallet.V0.sig, content:{}}}; Object.assign(arg.wallet.V0.content,old_content); arg.wallet.V0.content.security_img = img; - return tauri.invoke(path[0],arg) - } - else { + return tauri.invoke(path[0],arg); + } else { let arg = {}; args.map((el,ix) => arg[mapping[path[0]][ix]]=el) return tauri.invoke(path[0],arg) diff --git a/ng-app/src/lib/Login.svelte b/ng-app/src/lib/Login.svelte index 95b64b3..5eb2022 100644 --- a/ng-app/src/lib/Login.svelte +++ b/ng-app/src/lib/Login.svelte @@ -25,6 +25,8 @@ Backspace, ArrowPath, LockOpen, + Key, + CheckCircle, } from "svelte-heros-v2"; //import Worker from "../worker.js?worker&inline"; export let wallet; @@ -63,9 +65,15 @@ loaded = true; } - function letsgo() { + function start_with_pazzle() { loaded = false; step = "pazzle"; + unlockWith = "pazzle"; + } + function start_with_mnemonic() { + loaded = false; + step = "mnemonic"; + unlockWith = "mnemonic"; } let emojis2 = []; @@ -92,6 +100,10 @@ let trusted = false; + let mnemonic = ""; + + let unlockWith: "pazzle" | "mnemonic" | undefined; + function order() { step = "order"; ordered = []; @@ -114,10 +126,8 @@ let cat_idx = shuffle.category_indices[pazzlePage]; let cat = emojis[emoji_cat[cat_idx]]; let idx = shuffle.emoji_indices[pazzlePage][val]; - //console.log(cat_idx, emoji_cat[cat_idx], idx, cat[idx].code); selection[pazzlePage] = { cat: cat_idx, index: idx }; - console.debug(selection, cat, cat_idx, idx, val); if (pazzlePage == pazzle_length - 1) { order(); @@ -130,22 +140,23 @@ step = "opening"; let pazzle = []; - for (const emoji of ordered) { pazzle.push((emoji.cat << 4) + emoji.index); } - //console.log(pazzle); - //console.log(wallet); + const mnemonic_words = mnemonic.split(" "); // open the wallet try { if (tauri_platform) { - let opened_wallet = await ng.wallet_open_with_pazzle( - wallet, - pazzle, - pin_code - ); + let opened_wallet = + unlockWith === "pazzle" + ? await ng.wallet_open_with_pazzle(wallet, pazzle, pin_code) + : await ng.wallet_open_with_mnemonic_words( + wallet, + mnemonic_words, + pin_code + ); // try { // let client = await ng.wallet_was_opened(opened_wallet); // opened_wallet.V0.client = client; @@ -180,7 +191,11 @@ myWorker.onmessage = async (msg) => { //console.log("Message received from worker", msg.data); if (msg.data.loaded) { - myWorker.postMessage({ wallet, pazzle, pin_code }); + if (unlockWith === "pazzle") { + myWorker.postMessage({ wallet, pazzle, pin_code }); + } else { + myWorker.postMessage({ wallet, mnemonic_words, pin_code }); + } //console.log("postMessage"); } else if (msg.data.success) { //console.log(msg.data); @@ -229,6 +244,7 @@ async function select_order(val) { ordered.push(val); val.sel = ordered.length; + console.debug("ordered", ordered); selection = selection; if (ordered.length == pazzle_length - 1) { let last = selection.find((emoji) => !emoji.sel); @@ -294,8 +310,15 @@ class:max-w-[600px]={!mobile} > {#if step == "load"} -
-

How to open your wallet, step by step:

+
+

How to open your wallet:

+

By your Pazzle

  • For each one of the 9 categories of images, you will be presented with @@ -331,74 +354,86 @@ on the digits.
-
-
- {#if !loaded} - Loading pazzle... - - + By your 12 word Mnemonic (passphrase) + +
    +
  • + Enter your twelve word mnemonic in the input field. The words must be + separated by spaces. +
  • +
  • Enter the PIN code that you chose when you created your wallet.
  • +
+ +
+ {#if !loaded} + Loading wallet... + - - - {:else} -
- - -
- {/if} -
- {#if for_import} -
- Do you trust this device?
-
- Yes, save my wallet on this device -
-

- If you do, if this device is yours or is used by few trusted persons - of your family or workplace, and you would like to login again from - this device in the future, then you can save your wallet on this - device. To the contrary, if this device is public and shared by - strangers, do not save your wallet here. {#if !tauri_platform}By - selecting this option, you agree to save some cookies on your - browser.{/if}
-

+ + {:else} + + {#if for_import} +
+ Do you trust this device?
+
+ Yes, save my wallet on this device +
+

+ If you do, if this device is yours or is used by few trusted + persons of your family or workplace, and you would like to login + again from this device in the future, then you can save your + wallet on this device. To the contrary, if this device is public + and shared by strangers, do not save your wallet here. {#if !tauri_platform}By + selecting this option, you agree to save some cookies on your + browser.{/if}
+

+
+ {/if} + +
+ + + Open with Mnemonic instead + + +
+ {/if}
- {/if} - +
+ {:else if step == "pazzle" || step == "order" || step == "pin" || step == "mnemonic"}
{/each} + {:else if step == "mnemonic"} +
+ + +
+ +
+
{:else if step == "order"}

Click your emojis in the correct order @@ -638,11 +700,16 @@ .sel { position: absolute; + display: flex; width: 100%; - top: 45%; + height: 100%; + top: 0; left: 0; font-size: 100px; font-weight: 700; + justify-content: center; + align-items: center; + padding-top: 25%; } .sel-emoji { diff --git a/ng-app/src/routes/WalletCreate.svelte b/ng-app/src/routes/WalletCreate.svelte index 34619fb..be9de2b 100644 --- a/ng-app/src/routes/WalletCreate.svelte +++ b/ng-app/src/routes/WalletCreate.svelte @@ -1529,18 +1529,38 @@ with the name
{download_name}
- Please move it to a safe and durable place.

+ Please move it to a safe and durable place.

{/if} - Here is your Pazzle:

+ + Here is your Pazzle (The order of + each image is + important): +
+
{#each display_pazzle(ready.pazzle) as emoji} {emoji}
{/each} +
+ +

+ + And here is your mnemonic:
+ "{ready.mnemonic_str.join( + " " + )}"

+ +

+ You can use both the pazzle and the mnemonic to unlock your wallet. The + pazzle is easier to remember. The mnemonic is convenient, when you use + a secure password manager which can copy it to the corresponding wallet + unlock field. Copy both on a piece of paper. Use that until you memorized + it, then throw it away.

- Copy it on a piece of paper. Use that until you memorized it, then throw - it away.
The order of each image is important!
- Now click on "Continue to Login" and select your wallet.

It is important that you login with this wallet at least once from - this {#if tauri_platform}device{:else}browser tab{/if},
+ Now click on "Continue to Login" and select your wallet.
It is + important that you login with this wallet at least once from this {#if tauri_platform}device{:else}browser + tab{/if},
while connected to the internet, so your personal site can be created on your broker.

diff --git a/ng-app/src/worker.js b/ng-app/src/worker.js index 7da87a0..c70b842 100644 --- a/ng-app/src/worker.js +++ b/ng-app/src/worker.js @@ -7,11 +7,20 @@ onmessage = (e) => { //console.log("Message received by worker", e.data); (async function() { try { - let secret_wallet = await ng.wallet_open_with_pazzle( + let secret_wallet; + if (e.data.pazzle) { + secret_wallet = await ng.wallet_open_with_pazzle( + e.data.wallet, + e.data.pazzle, + e.data.pin_code + ); + } else if (e.data.mnemonic_words) { + secret_wallet = await ng.wallet_open_with_mnemonic_words( e.data.wallet, - e.data.pazzle, + e.data.mnemonic_words, e.data.pin_code - ); + ); + } postMessage({success:secret_wallet}); } catch (e) { postMessage({error:e}); diff --git a/ng-repo/src/errors.rs b/ng-repo/src/errors.rs index 9a6886d..8f67688 100644 --- a/ng-repo/src/errors.rs +++ b/ng-repo/src/errors.rs @@ -37,6 +37,7 @@ pub enum NgError { InvalidArgument, PermissionDenied, InvalidPazzle, + InvalidMnemonic, CommitLoadError(CommitLoadError), ObjectParseError(ObjectParseError), StorageError(StorageError), diff --git a/ng-sdk-js/src/lib.rs b/ng-sdk-js/src/lib.rs index 73161d0..bc602c1 100644 --- a/ng-sdk-js/src/lib.rs +++ b/ng-sdk-js/src/lib.rs @@ -134,6 +134,53 @@ pub fn wallet_open_with_pazzle( } } +#[wasm_bindgen] +pub fn wallet_open_with_mnemonic( + wallet: JsValue, + mnemonic: Vec, + pin: JsValue, +) -> Result { + let encrypted_wallet = serde_wasm_bindgen::from_value::(wallet) + .map_err(|_| "Deserialization error of wallet")?; + let pin = serde_wasm_bindgen::from_value::<[u8; 4]>(pin) + .map_err(|_| "Deserialization error of pin")?; + let res = nextgraph::local_broker::wallet_open_with_mnemonic(&encrypted_wallet, mnemonic, pin); + match res { + Ok(r) => Ok(r + .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true)) + .unwrap()), + Err(e) => Err(e.to_string().into()), + } +} + +#[wasm_bindgen] +pub fn wallet_open_with_mnemonic_words( + wallet: JsValue, + mnemonic_words: Array, + pin: JsValue, +) -> Result { + let encrypted_wallet = serde_wasm_bindgen::from_value::(wallet) + .map_err(|_| "Deserialization error of wallet")?; + let pin = serde_wasm_bindgen::from_value::<[u8; 4]>(pin) + .map_err(|_| "Deserialization error of pin")?; + let mnemonic_vec: Vec = mnemonic_words + .iter() + .map(|word| word.as_string().unwrap()) + .collect(); + + let res = nextgraph::local_broker::wallet_open_with_mnemonic_words( + &encrypted_wallet, + &mnemonic_vec, + pin, + ); + match res { + Ok(r) => Ok(r + .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true)) + .unwrap()), + Err(e) => Err(e.to_string().into()), + } +} + #[wasm_bindgen] pub fn wallet_update(wallet_id: JsValue, operations: JsValue) -> Result { let _wallet = serde_wasm_bindgen::from_value::(wallet_id) diff --git a/ng-wallet/src/bip39.rs b/ng-wallet/src/bip39.rs index 253e560..dc02674 100644 --- a/ng-wallet/src/bip39.rs +++ b/ng-wallet/src/bip39.rs @@ -1,3 +1,6 @@ +use ng_repo::errors::NgError; +use std::collections::HashMap; + #[allow(non_upper_case_globals)] pub const bip39_wordlist: [&str; 2048] = [ "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", @@ -212,3 +215,26 @@ pub const bip39_wordlist: [&str; 2048] = [ "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo", ]; + +lazy_static! { + pub static ref BIP39_WORD_MAP: HashMap = { + let mut m = HashMap::new(); + for (i, word) in bip39_wordlist.iter().enumerate() { + m.insert(word.to_string(), i as u16); + } + m + }; +} + +/// Taking a list of bip39 words, returns a list of u16 codes +pub fn encode_mnemonic(words: &Vec) -> Result, NgError> { + let mut res = vec![]; + for word in words { + res.push( + *BIP39_WORD_MAP + .get(word.as_str()) + .ok_or(NgError::InvalidMnemonic)?, + ); + } + Ok(res) +} diff --git a/ng-wallet/src/lib.rs b/ng-wallet/src/lib.rs index b3369dc..6a69594 100644 --- a/ng-wallet/src/lib.rs +++ b/ng-wallet/src/lib.rs @@ -362,7 +362,7 @@ pub fn open_wallet_with_pazzle( } pub fn open_wallet_with_mnemonic( - wallet: Wallet, + wallet: &Wallet, mut mnemonic: [u16; 12], mut pin: [u8; 4], ) -> Result { @@ -388,7 +388,7 @@ pub fn open_wallet_with_mnemonic( mnemonic_key.zeroize(); Ok(SensitiveWallet::V0(dec_encrypted_block( - v0.content.encrypted, + v0.content.encrypted.clone(), master_key, v0.content.peer_id, v0.content.nonce, @@ -786,6 +786,7 @@ pub async fn create_wallet_second_step_v0( wallet_file, pazzle, mnemonic: mnemonic.clone(), + mnemonic_str: display_mnemonic(&mnemonic), wallet_name: params.wallet_name.clone(), client: params.client.clone(), user, @@ -893,7 +894,7 @@ mod test { let _opening_mnemonic = Instant::now(); - let _w = open_wallet_with_mnemonic(Wallet::V0(v0.clone()), res.mnemonic, pin.clone()) + let _w = open_wallet_with_mnemonic(&Wallet::V0(v0.clone()), res.mnemonic, pin.clone()) .expect("open with mnemonic"); //log_debug!("encrypted part {:?}", w); diff --git a/ng-wallet/src/types.rs b/ng-wallet/src/types.rs index 15dfb06..2705aaa 100644 --- a/ng-wallet/src/types.rs +++ b/ng-wallet/src/types.rs @@ -1303,6 +1303,8 @@ pub struct CreateWalletResultV0 { /// randomly generated mnemonic. It is an alternate way to open the wallet. /// A BIP39 list of 12 words. We argue that the Pazzle is easier to remember than this. pub mnemonic: [u16; 12], + /// The words of the mnemonic, in a human readable form. + pub mnemonic_str: Vec, #[zeroize(skip)] /// a string identifying uniquely the wallet pub wallet_name: String, @@ -1368,6 +1370,7 @@ pub enum NgWalletError { InvalidPin, InvalidPazzle, InvalidPazzleLength, + InvalidMnemonic, InvalidSecurityImage, InvalidSecurityText, SubmissionError,