feat: display mnemonic on wallet creation and support for login

pull/22/head
Laurin Weger 6 months ago
parent 7f24dfecd2
commit 1d475fd9c2
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 37
      nextgraph/src/local_broker.rs
  2. 26
      ng-app/src-tauri/src/lib.rs
  3. 8
      ng-app/src/api.ts
  4. 167
      ng-app/src/lib/Login.svelte
  5. 34
      ng-app/src/routes/WalletCreate.svelte
  6. 11
      ng-app/src/worker.js
  7. 1
      ng-repo/src/errors.rs
  8. 47
      ng-sdk-js/src/lib.rs
  9. 26
      ng-wallet/src/bip39.rs
  10. 7
      ng-wallet/src/lib.rs
  11. 3
      ng-wallet/src/types.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<u16>,
pin: [u8; 4],
) -> Result<SensitiveWallet, NgError> {
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<String>,
pin: [u8; 4],
) -> Result<SensitiveWallet, NgError> {
let encoded: Vec<u16> = 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].

@ -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<u16>,
pin: [u8; 4],
_app: tauri::AppHandle,
) -> Result<SensitiveWallet, String> {
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<String>,
pin: [u8; 4],
_app: tauri::AppHandle,
) -> Result<SensitiveWallet, String> {
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,

@ -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)

@ -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,20 +140,21 @@
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(
let opened_wallet =
unlockWith === "pazzle"
? await ng.wallet_open_with_pazzle(wallet, pazzle, pin_code)
: await ng.wallet_open_with_mnemonic_words(
wallet,
pazzle,
mnemonic_words,
pin_code
);
// try {
@ -180,7 +191,11 @@
myWorker.onmessage = async (msg) => {
//console.log("Message received from worker", msg.data);
if (msg.data.loaded) {
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"}
<div class=" max-w-xl lg:px-8 mx-auto px-4 mt-10">
<h2 class="pb-5 text-xl">How to open your wallet, step by step:</h2>
<div
class="flex flex-col justify-center p-4"
class:min-w-[310px]={mobile}
class:min-w-[500px]={!mobile}
class:max-w-[360px]={mobile}
class:max-w-[600px]={!mobile}
>
<h2 class="pb-5 text-xl self-start">How to open your wallet:</h2>
<h3 class="pb-2 text-lg self-start">By your Pazzle</h3>
<ul class="mb-8 ml-3 space-y-4 text-left list-decimal">
<li>
For each one of the 9 categories of images, you will be presented with
@ -331,25 +354,28 @@
on the digits.
</li>
</ul>
</div>
<h3 class="pb-2 text-lg self-start">
By your 12 word Mnemonic (passphrase)
</h3>
<ul class="mb-8 ml-3 space-y-4 text-left list-decimal">
<li>
Enter your twelve word mnemonic in the input field. The words must be
separated by spaces.
</li>
<li>Enter the PIN code that you chose when you created your wallet.</li>
</ul>
<div class=" max-w-xl lg:px-8 mx-auto px-4 text-primary-700">
{#if !loaded}
Loading pazzle...
Loading wallet...
<svg
class="animate-spin my-4 h-14 w-14 mx-auto"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
>
<path
class="opacity-75"
fill="currentColor"
@ -357,28 +383,7 @@
/>
</svg>
{:else}
<div class="flex justify-center space-x-12 mt-4 mb-4">
<button
on:click={cancel}
class="mt-1 mb-2 bg-red-100 hover:bg-red-100/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><XCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>Cancel</button
>
<button
on:click={letsgo}
class="mt-1 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<PuzzlePiece
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>
Open my wallet now!
</button>
</div>
{/if}
</div>
<!-- Save wallet? -->
{#if for_import}
<div class="max-w-xl lg:px-8 mx-auto px-4 mb-8">
<span class="text-xl">Do you trust this device? </span> <br />
@ -388,17 +393,47 @@
>
</div>
<p class="text-sm">
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
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}<br />
</p>
</div>
{/if}
<!-- The following have navigation buttons and fixed layout -->
<div class="flex flex-col justify-centerspace-x-12 mt-4 mb-4">
<button
on:click={start_with_pazzle}
class="mt-1 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<PuzzlePiece
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>
Open with Pazzle!
</button>
<a
on:click={start_with_mnemonic}
class="mt-1 text-lg px-5 py-2.5 text-center inline-flex items-center mb-2 underline cursor-pointer"
>
Open with Mnemonic instead
</a>
<button
on:click={cancel}
class="mt-1 mb-2 bg-red-100 hover:bg-red-100/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><XCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>Cancel</button
>
</div>
{/if}
</div>
</div>
<!-- The following steps have navigation buttons and fixed layout -->
{:else if step == "pazzle" || step == "order" || step == "pin" || step == "mnemonic"}
<div
class="flex flex-col justify-center h-screen p-4"
@ -432,6 +467,33 @@
{/each}
</div>
{/each}
{:else if step == "mnemonic"}
<form on:submit|preventDefault={start_pin}>
<label
for="mnemonic-input"
class="block mb-2 text-xl text-gray-900 dark:text-white"
>Your 12 word mnemonic</label
>
<input
type="password"
id="mnemonic-input"
placeholder="Enter your 12 word mnemonic here separated by spaces"
bind:value={mnemonic}
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
<div class="flex">
<button
type="submit"
class="mt-1 ml-auto text-white bg-primary-700 disabled:opacity-65 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
on:click={start_pin}
disabled={mnemonic.split(" ").length !== 12}
><CheckCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 group-hover:text-gray-900 dark:group-hover:text-white"
/>Confirm</button
>
</div>
</form>
{:else if step == "order"}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-2">
<span class="text-xl">Click your emojis in the correct order</span>
@ -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 {

@ -1529,18 +1529,38 @@
with the name<br /><span class="text-black">
{download_name}</span
><br />
Please move it to a safe and durable place.<br /><br />
<span class="font-bold"
>Please move it to a safe and durable place.</span
><br /><br />
{/if}
Here is your Pazzle:<br /><br />
<!-- Pazzle -->
Here is your Pazzle (The <span class="font-bold">order</span> of
each image is
<span class="font-bold">important</span>):
<br />
<br />
{#each display_pazzle(ready.pazzle) as emoji}
<span>{emoji}</span><br />
{/each}
<br />
<br /><br />
<!-- Mnemonic -->
And here is your mnemonic:<br />
<span class="select-none">"</span>{ready.mnemonic_str.join(
" "
)}<span class="select-none">"</span> <br /><br />
<br /><br />
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.
<br /><br />
Copy it on a piece of paper. Use that until you memorized it, then throw
it away.<br /> The order of each image is important!<br />
Now click on "Continue to Login" and select your wallet.<br /><br
/>It is important that you login with this wallet at least once from
this {#if tauri_platform}device{:else}browser tab{/if},<br />
Now click on "Continue to Login" and select your wallet.<br />It is
important that you login with this wallet at least once from this {#if tauri_platform}device{:else}browser
tab{/if},<br />
while connected to the internet, so your personal site can be created
on your broker.<br /><br />
<a href="/wallet/login" use:link>

@ -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.mnemonic_words,
e.data.pin_code
);
}
postMessage({success:secret_wallet});
} catch (e) {
postMessage({error:e});

@ -37,6 +37,7 @@ pub enum NgError {
InvalidArgument,
PermissionDenied,
InvalidPazzle,
InvalidMnemonic,
CommitLoadError(CommitLoadError),
ObjectParseError(ObjectParseError),
StorageError(StorageError),

@ -134,6 +134,53 @@ pub fn wallet_open_with_pazzle(
}
}
#[wasm_bindgen]
pub fn wallet_open_with_mnemonic(
wallet: JsValue,
mnemonic: Vec<u16>,
pin: JsValue,
) -> Result<JsValue, JsValue> {
let encrypted_wallet = serde_wasm_bindgen::from_value::<Wallet>(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<JsValue, JsValue> {
let encrypted_wallet = serde_wasm_bindgen::from_value::<Wallet>(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<String> = 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<JsValue, JsValue> {
let _wallet = serde_wasm_bindgen::from_value::<WalletId>(wallet_id)

@ -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<String, u16> = {
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<String>) -> Result<Vec<u16>, NgError> {
let mut res = vec![];
for word in words {
res.push(
*BIP39_WORD_MAP
.get(word.as_str())
.ok_or(NgError::InvalidMnemonic)?,
);
}
Ok(res)
}

@ -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<SensitiveWallet, NgWalletError> {
@ -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);

@ -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<String>,
#[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,

Loading…
Cancel
Save