switching to password based login

refactor
Niko PLP 17 hours ago
parent 26f6f3dc2a
commit 158169af27
  1. 2
      app/ui-common/package.json
  2. 4
      app/ui-common/src/api.ts
  3. 22
      app/ui-common/src/lib/Login.svelte
  4. 10
      app/ui-common/src/lib/components/PasswordInput.svelte
  5. 18
      app/ui-common/src/locales/en.json
  6. 1780
      app/ui-common/src/routes/WalletCreate.svelte
  7. 77
      app/ui-common/src/routes/WalletLogin.svelte
  8. 2
      engine/broker/auth/package.json
  9. 218
      engine/wallet/src/lib.rs
  10. 59
      engine/wallet/src/types.rs
  11. 2
      infra/ngaccount/web/src/routes/Create.svelte
  12. 2
      infra/ngnet/bootstrap/package.json
  13. 2
      infra/ngnet/redir/package.json
  14. 24
      pnpm-lock.yaml
  15. 22
      sdk/js/lib-wasm/src/lib.rs
  16. 37
      sdk/rust/src/local_broker.rs

@ -55,7 +55,7 @@
"shx": "^0.3.4",
"svelte": "^3.54.0",
"svelte-check": "^3.0.0",
"svelte-heros-v2": "^0.10.12",
"svelte-heros-v2": "^1.3.0",
"svelte-preprocess": "^5.0.3",
"svelte-time": "^0.8.0",
"tailwindcss": "^3.3.1",

@ -25,12 +25,12 @@ export default api;
export const NG_EU_BSP = "https://nextgraph.eu";
export const NG_EU_BSP_REGISTER = import.meta.env.PROD
? "https://account.nextgraph.eu/#/create"
? import.meta.env.NG_ENV_ALT ? "https://pnm.allelo.eco" : "https://account.nextgraph.eu/#/create"
: "http://account-dev.nextgraph.eu:5173/#/create";
export const NG_ONE_BSP = "https://nextgraph.one";
export const NG_ONE_BSP_REGISTER = import.meta.env.PROD
? "https://account.nextgraph.one/#/create"
? import.meta.env.NG_ENV_ALT ? "https://account.allelo.eco/#/create" : "https://account.nextgraph.one/#/create"
: "http://account-dev.nextgraph.one:5173/#/create";
export const APP_ACCOUNT_REGISTERED_SUFFIX = "/#/user/registered";

@ -54,7 +54,6 @@
load_svg();
//console.log(wallet);
await init();
});
async function init() {
@ -256,7 +255,11 @@
}
} catch (e) {
console.error(e);
if (e.message && e.message.includes("constructor") || (typeof e === "string" && e.includes("constructor") )) e = "BrowserTooOld";
if (
(e.message && e.message.includes("constructor")) ||
(typeof e === "string" && e.includes("constructor"))
)
e = "BrowserTooOld";
error = e;
step = "end";
dispatch("error", { error: e });
@ -272,7 +275,10 @@
async function on_pin_key(val) {
pin_code = [...pin_code, val];
if (pin_code.length == 4) {
setTimeout(()=>window.document.getElementById("confirm_pin_btn").focus(),50);
setTimeout(
() => window.document.getElementById("confirm_pin_btn").focus(),
50
);
}
}
@ -590,7 +596,10 @@
class:h-[160px]={!mobile}
class:h-[93px]={mobile}
class:text-8xl={!mobile}
on:click={async () => {window.document.activeElement.blur(); await on_pin_key(num)}}
on:click={async () => {
window.document.activeElement.blur();
await on_pin_key(num);
}}
disabled={pin_code.length >= 4}
>
<span>{num}</span>
@ -606,7 +615,10 @@
class:h-[160px]={!mobile}
class:h-[93px]={mobile}
class:text-8xl={!mobile}
on:click={async () => {window.document.activeElement.blur();await on_pin_key(shuffle_pin[9])}}
on:click={async () => {
window.document.activeElement.blur();
await on_pin_key(shuffle_pin[9]);
}}
disabled={pin_code.length >= 4}
>
<span>{shuffle_pin[9]}</span>

@ -18,8 +18,8 @@
export let auto_complete: string | undefined = undefined;
import { createEventDispatcher } from "svelte";
export let show: boolean = false;
let input;
export let autofocus = false;
export let input = undefined;
let type: "password" | "text" = "password";
$: type = show ? "text" : "password";
@ -44,7 +44,7 @@
if (e.key == "Enter" || e.keyCode == 13) {
dispatch("enter");
}
}
};
</script>
<div class="relative">
@ -55,7 +55,7 @@
{placeholder}
{id}
{type}
autofocus
{autofocus}
on:input={handleInput}
class={`${className} pr-12 text-md block`}
autocomplete={auto_complete}
@ -63,7 +63,7 @@
/>
<div
class={`${classNameToggle} absolute inset-y-0 pr-3 flex items-center text-sm leading-5`}
class={`${classNameToggle} clickable absolute inset-y-0 pr-3 flex items-center text-sm leading-5`}
>
<svg
fill="none"

@ -372,8 +372,10 @@
"open_with_pazzle": "Open With Pazzle instead",
"login_cancel": "Cancel Login",
"open_with_mnemonic": "Open with Mnemonic",
"open": "Open my wallet",
"enter_mnemonic": "Enter your 12 words mnemonic",
"mnemonic_placeholder": "12 words separated by spaces",
"enter_password": "Enter your password",
"select_emoji": "Select your image for category:<br />{category}",
"order_emojis": "Select each image in the correct order",
"enter_pin": "Enter your PIN code",
@ -439,7 +441,13 @@
"self_hosted_broker": "Self-hosted broker",
"ng_box": "NG Box (owned or invited)",
"install_app": "Install the app",
"registration_success": "You have been successfully registered to {broker}",
"registration_success": "You are creating an account at {broker}",
"choose_username": {
"title": "Now choose your username and password",
"warning": "Please note that we do not offer password recovery mechanism for now.<br/> We won't be able to help you if you forget your password."
},
"type_username_placeholder": "Type in your username",
"type_password_placeholder": "Type in your password",
"choose_pin": {
"title": "Let's start creating your wallet by choosing a PIN code",
"description": "We recommend you to choose a PIN code that you already know very well. <br />We at NextGraph will never see your PIN.",
@ -591,7 +599,7 @@
"InvalidSignature": "The signature is invalid.",
"IncompleteSignature": "The signature is incomplete.",
"SerializationError": "The data could not be serialized.",
"EncryptionError": "Your wallet could not be opened. You probably did a mistake.",
"EncryptionError": "Your wallet could not be opened. You probably did a mistake when entering your credentials.",
"DecryptionError": "Error with decryption.",
"InvalidValue": "The value is invalid.",
"ConnectionNotFound": "The connection was not found.",
@ -659,9 +667,13 @@
"WsError": "WebSocket error",
"cannot_load_this_file": "Cannot load this file",
"graph_not_found": "Graph not found",
"SocialQueryAlreadyStarted": "Social Query already started",
"ContactAlreadyExists": "Contact already added to your account",
"ContactNotFound": "You don't have any contact. We cannot start the Social Query",
"InvalidProfile": "Your profile is incomplete. You should add a name before you can share your profile with others",
"no_wasm_on_old_safari": "Your Safari browser is too old (version before 14.1). As a result we cannot load Automerge, needed for this document. Please upgrade your macOS or iOS system",
"BrowserTooOld": "Your browser is too old. Please upgrade it, use another browser, or install our native app. If you are using jshelter or another javascript protection mechanism, please deactivate it as we need access to the WebWorker facility of your browser.",
"NoLocalStorage": "You have disabled local storage in your browser. Please allow the current website (and https://nextgraph.net website) to store data in this browser as otherwise we cannot proceed with Wallet creation. After allowing storage, please refresh the current page."
"NoLocalStorage": "You have disabled local storage in your browser. Please allow the current website (and also https://nextgraph.net website) to store data in this browser as otherwise we cannot proceed with Wallet creation. After allowing storage, please refresh the current page."
},
"auth":{
"select_broker":"<b>{origin}</b><br/>wants to access your wallet<br/><br/>Please select your broker in the list below:",

File diff suppressed because it is too large Load Diff

@ -35,11 +35,18 @@
display_error,
wallet_from_import,
redirect_after_login,
redirect_if_wallet_is
redirect_if_wallet_is,
} from "../store";
import { CheckBadge, ExclamationTriangle, QrCode, Cloud } from "svelte-heros-v2";
import {
CheckBadge,
ExclamationTriangle,
QrCode,
Cloud,
ArrowRightEndOnRectangle,
} from "svelte-heros-v2";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
let mobile = tauri_platform == "android" || tauri_platform == "ios";
let wallet;
let selected;
@ -108,21 +115,23 @@
wallet = $wallet_from_import;
importing = true;
}
});
async function loggedin() {
step = "loggedin";
if ($redirect_after_login) {
if (!$redirect_if_wallet_is || $redirect_if_wallet_is == $active_wallet?.id) {
let redir=$redirect_after_login;
$redirect_after_login=undefined;
$redirect_if_wallet_is=undefined;
push("#"+redir);
if (
!$redirect_if_wallet_is ||
$redirect_if_wallet_is == $active_wallet?.id
) {
let redir = $redirect_after_login;
$redirect_after_login = undefined;
$redirect_if_wallet_is = undefined;
push("#" + redir);
} else {
$redirect_after_login=undefined;
$redirect_if_wallet_is=undefined;
$redirect_after_login = undefined;
$redirect_if_wallet_is = undefined;
push("#/");
}
} else {
@ -150,10 +159,24 @@
try {
if (importing) {
step = "loggedin";
$redirect_after_login=undefined;
$redirect_if_wallet_is=undefined;
$redirect_after_login = undefined;
$redirect_if_wallet_is = undefined;
let in_memory = !event.detail.trusted;
//console.log("IMPORTING", in_memory, event.detail.wallet, wallet);
// TODO : register bootstrap when importing
// if (!in_memory && !tauri_platform) {
// let bootstrap_iframe_msgs =
// await ng.get_bootstrap_iframe_msgs_for_brokers(
// event.detail.wallet.V0.brokers
// );
// let res = await register_bootstrap(bootstrap_iframe_msgs);
// if (res !== true) {
// throw new Error(
// "We could not save your bootstrap information at nextgraph.net. This is needed for links and third-party webapps to work properly. so we are stopping here. Reason: " +
// res
// );
// }
// }
let client = await ng.wallet_import(
wallet,
event.detail.wallet,
@ -190,7 +213,9 @@
event.detail.wallet.V0.client = client;
}
} catch (e) {
if (importing) {wallet = undefined;}
if (importing) {
wallet = undefined;
}
importing = false;
error = e;
step = "open";
@ -352,6 +377,27 @@
select(wallet_entry[0]);
}}
>
{#if wallet_entry[1].wallet.V0.content.password}
<div class="pt-5">
<ArrowRightEndOnRectangle
class="w-16 h-16"
style="display:inline;"
/>
<div>
{#if mobile}Tap{:else}Click{/if} here to login with your wallet
</div>
</div>
<div class="p-5">
<button
tabindex="-1"
style="overflow-wrap: anywhere;"
class="mt-1 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-1.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
{wallet_entry[1].wallet.V0.content.security_txt}
</button>
</div>
{:else}
<span class="securitytxt"
>{wallet_entry[1].wallet.V0.content.security_txt}
</span>
@ -362,6 +408,7 @@
wallet_entry[1].wallet.V0.content.security_img
)}
/>
{/if}
</div>
{/each}
<div class="wallet-box">
@ -373,7 +420,7 @@
class:mt-2.5={!without_create}
class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-1.5 text-center inline-flex items-center justify-center dark:focus:ring-primary-100/55 mb-2"
>
<Cloud class="w-8 h-8 mr-2 -ml-1" tabindex="-1"/>
<Cloud class="w-8 h-8 mr-2 -ml-1" tabindex="-1" />
{$t("pages.wallet_login.with_username")}
</button>
</a>
@ -412,7 +459,7 @@
tabindex="-1"
class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-1.5 text-center inline-flex items-center justify-center dark:focus:ring-primary-100/55 mb-2"
>
<QrCode class="w-8 h-8 mr-2 -ml-1" tabindex="-1"/>
<QrCode class="w-8 h-8 mr-2 -ml-1" tabindex="-1" />
{$t("pages.wallet_login.import_qr")}
</button>
</a>

@ -28,7 +28,7 @@
"vite": "^4.3.9",
"postcss": "^8.4.23",
"postcss-load-config": "^4.0.1",
"svelte-heros-v2": "^0.10.12",
"svelte-heros-v2": "^1.3.0",
"svelte-preprocess": "^5.0.3",
"tailwindcss": "^3.3.1",
"autoprefixer": "^10.4.14",

@ -332,13 +332,18 @@ pub fn open_wallet_with_pazzle(
match wallet {
Wallet::V0(v0) => {
let login = v0
.content
.pazzle
.as_ref()
.ok_or(NgWalletError::LoginMethodNotSupported)?;
pazzle.extend_from_slice(&pin);
let mut pazzle_key = derive_key_from_pass(pazzle, v0.content.salt_pazzle, v0.id);
let mut pazzle_key = derive_key_from_pass(pazzle, login.salt, v0.id);
// pazzle is zeroized in derive_key_from_pass
pin.zeroize();
let master_key = dec_master_key(
v0.content.enc_master_key_pazzle,
login.enc_master_key,
&pazzle_key,
v0.content.master_nonce,
v0.id,
@ -374,16 +379,21 @@ pub fn open_wallet_with_mnemonic(
match wallet {
Wallet::V0(v0) => {
let login = v0
.content
.mnemonic
.as_ref()
.ok_or(NgWalletError::LoginMethodNotSupported)?;
let mut mnemonic_key = derive_key_from_pass(
[transmute_to_bytes(&mnemonic), &pin].concat(),
v0.content.salt_mnemonic,
login.salt,
v0.id,
);
mnemonic.zeroize();
pin.zeroize();
let master_key = dec_master_key(
v0.content.enc_master_key_mnemonic,
login.enc_master_key,
&mnemonic_key,
v0.content.master_nonce,
v0.id,
@ -403,6 +413,48 @@ pub fn open_wallet_with_mnemonic(
}
}
pub fn open_wallet_with_password(
wallet: &Wallet,
mut pass: String,
) -> Result<SensitiveWallet, NgWalletError> {
verify(&wallet.content_as_bytes(), wallet.sig(), wallet.id())
.map_err(|_e| NgWalletError::InvalidSignature)?;
let mut password = pass.trim().to_string();
pass.zeroize();
match wallet {
Wallet::V0(v0) => {
let login = v0
.content
.password
.as_ref()
.ok_or(NgWalletError::LoginMethodNotSupported)?;
let mut password_key =
derive_key_from_pass(password.as_bytes().to_vec(), login.salt, v0.id);
password.zeroize();
let master_key = dec_master_key(
login.enc_master_key,
&password_key,
v0.content.master_nonce,
v0.id,
)?;
password_key.zeroize();
Ok(SensitiveWallet::V0(dec_encrypted_block(
v0.content.encrypted.clone(),
master_key,
v0.content.peer_id,
v0.content.nonce,
v0.content.timestamp,
v0.id,
)?))
}
_ => unimplemented!(),
}
}
pub fn display_mnemonic(mnemonic: &[u16; 12]) -> Vec<String> {
let res: Vec<String> = mnemonic
.into_iter()
@ -446,7 +498,7 @@ pub fn gen_shuffle_for_pin() -> Vec<u8> {
pub fn create_wallet_first_step_v0(
params: CreateWalletV0,
) -> Result<CreateWalletIntermediaryV0, NgWalletError> {
// pazzle_length can only be 9, 12, or 15
// pazzle_length can only be 0, 9, 12, or 15
if params.pazzle_length != 9
//&& params.pazzle_length != 12
//&& params.pazzle_length != 15
@ -462,47 +514,49 @@ pub fn create_wallet_first_step_v0(
// return Err(NgWalletError::InvalidPin);
// }
if params.pazzle_length == 0 && !params.mnemonic && params.password.is_none() {
return Err(NgWalletError::NoLoginMethod);
}
if let Some(pin) = params.pin {
// each digit shouldnt be greater than 9
if params.pin[0] > 9 || params.pin[1] > 9 || params.pin[2] > 9 || params.pin[3] > 9 {
if pin[0] > 9 || pin[1] > 9 || pin[2] > 9 || pin[3] > 9 {
return Err(NgWalletError::InvalidPin);
}
// check for same digit doesnt appear 3 times
if (params.pin[0] == params.pin[1] && params.pin[0] == params.pin[2])
|| (params.pin[0] == params.pin[1] && params.pin[0] == params.pin[3])
|| (params.pin[0] == params.pin[2] && params.pin[0] == params.pin[3])
|| (params.pin[1] == params.pin[2] && params.pin[1] == params.pin[3])
if (pin[0] == pin[1] && pin[0] == pin[2])
|| (pin[0] == pin[1] && pin[0] == pin[3])
|| (pin[0] == pin[2] && pin[0] == pin[3])
|| (pin[1] == pin[2] && pin[1] == pin[3])
{
return Err(NgWalletError::InvalidPin);
}
// check for ascending series
if params.pin[1] == params.pin[0] + 1
&& params.pin[2] == params.pin[1] + 1
&& params.pin[3] == params.pin[2] + 1
{
if pin[1] == pin[0] + 1 && pin[2] == pin[1] + 1 && pin[3] == pin[2] + 1 {
return Err(NgWalletError::InvalidPin);
}
// check for descending series
if params.pin[3] >= 3
&& params.pin[2] == params.pin[3] - 1
&& params.pin[1] == params.pin[2] - 1
&& params.pin[0] == params.pin[1] - 1
{
if pin[3] >= 3 && pin[2] == pin[3] - 1 && pin[1] == pin[2] - 1 && pin[0] == pin[1] - 1 {
return Err(NgWalletError::InvalidPin);
}
} else if params.pazzle_length > 0 || params.mnemonic {
return Err(NgWalletError::MnemonicOrPazzleNeedAPin);
}
// check validity of security text
let words: Vec<_> = params.security_txt.split_whitespace().collect();
let new_string = words.join(" ");
let count = new_string.chars().count();
if count < 10 || count > 100 {
if count < 2 || count > 100 {
return Err(NgWalletError::InvalidSecurityText);
}
// check validity of image
let decoded_img = ImageReader::new(Cursor::new(&params.security_img))
let img_vec = if let Some(security_img) = &params.security_img {
let decoded_img = ImageReader::new(Cursor::new(security_img))
.with_guessed_format()
.map_err(|_e| NgWalletError::InvalidSecurityImage)?
.decode()
@ -517,13 +571,17 @@ pub fn create_wallet_first_step_v0(
} else {
decoded_img.resize_to_fill(400, 400, FilterType::Triangle)
};
let buffer: Vec<u8> = Vec::with_capacity(100000);
let mut cursor = Cursor::new(buffer);
resized_img
.write_to(&mut cursor, ImageOutputFormat::Jpeg(72))
.map_err(|_e| NgWalletError::InvalidSecurityImage)?;
Some(cursor.into_inner())
} else {
None
};
// creating the wallet keys
let (wallet_privkey, wallet_id) = generate_keypair();
@ -541,10 +599,12 @@ pub fn create_wallet_first_step_v0(
client,
user_privkey,
in_memory: !params.local_save,
security_img: cursor.into_inner(),
security_img: img_vec,
security_txt: new_string,
pazzle_length: params.pazzle_length,
pin: params.pin,
password: params.password.as_ref().map(|p| p.trim().to_string()),
mnemonic: params.mnemonic,
send_bootstrap: params.send_bootstrap,
send_wallet: params.send_wallet,
result_with_wallet_file: params.result_with_wallet_file,
@ -583,6 +643,7 @@ pub async fn create_wallet_second_step_v0(
let mut ran = thread_rng();
let pazzle = if params.pazzle_length > 0 {
let mut category_indices: Vec<u8> = (0..params.pazzle_length).collect();
category_indices.shuffle(&mut ran);
@ -592,16 +653,24 @@ pub async fn create_wallet_second_step_v0(
//*i = ran.gen_range(0, 15) + (category_indices[ix] << 4);
*i = between.sample(&mut ran) + (category_indices[ix] << 4);
}
//log_debug!("pazzle {:?}", pazzle);
Some(pazzle)
} else {
None
};
let mnemonic = if params.mnemonic {
let between = Uniform::try_from(0..2048).unwrap();
let mut mnemonic = [0u16; 12];
for i in &mut mnemonic {
//*i = ran.gen_range(0, 2048);
*i = between.sample(&mut ran);
}
//log_debug!("mnemonic {:?}", display_mnemonic(&mnemonic));
Some(mnemonic)
} else {
None
};
//slice_as_array!(&mnemonic, [String; 12])
//.ok_or(NgWalletError::InternalError)?
@ -676,29 +745,37 @@ pub async fn create_wallet_second_step_v0(
let mut master_key = [0u8; 32];
getrandom::fill(&mut master_key).map_err(|_e| NgWalletError::InternalError)?;
let pazzle_login = if let Some(pazzle) = &pazzle {
let mut salt_pazzle = [0u8; 16];
let mut enc_master_key_pazzle = [0u8; 48];
if params.pazzle_length > 0 {
//log_debug!("salt_pazzle {:?}", salt_pazzle);
getrandom::fill(&mut salt_pazzle).map_err(|_e| NgWalletError::InternalError)?;
let mut pazzle_key = derive_key_from_pass(
[pazzle.clone(), params.pin.to_vec()].concat(),
[pazzle.clone(), params.pin.unwrap().to_vec()].concat(),
salt_pazzle,
wallet_id,
);
enc_master_key_pazzle = enc_master_key(&master_key, &pazzle_key, 0, wallet_id)?;
let enc_master_key_pazzle = enc_master_key(&master_key, &pazzle_key, 0, wallet_id)?;
pazzle_key.zeroize();
}
Some(LoginMethod {
salt: salt_pazzle,
enc_master_key: enc_master_key_pazzle,
})
} else {
None
};
let mnemonic_login = if let Some(mnemonic) = mnemonic {
let mut salt_mnemonic = [0u8; 16];
getrandom::fill(&mut salt_mnemonic).map_err(|_e| NgWalletError::InternalError)?;
//log_debug!("salt_pazzle {:?}", salt_pazzle);
//log_debug!("salt_mnemonic {:?}", salt_mnemonic);
let mut mnemonic_key = derive_key_from_pass(
[transmute_to_bytes(&mnemonic), &params.pin].concat(),
[transmute_to_bytes(&mnemonic), &params.pin.unwrap()].concat(),
salt_mnemonic,
wallet_id,
);
@ -706,6 +783,34 @@ pub async fn create_wallet_second_step_v0(
let enc_master_key_mnemonic = enc_master_key(&master_key, &mnemonic_key, 0, wallet_id)?;
mnemonic_key.zeroize();
Some(LoginMethod {
salt: salt_mnemonic,
enc_master_key: enc_master_key_mnemonic,
})
} else {
None
};
let password = if let Some(password) = &params.password {
let mut salt_password = [0u8; 16];
getrandom::fill(&mut salt_password).map_err(|_e| NgWalletError::InternalError)?;
//log_debug!("salt_password {:?}", salt_password);
let mut password_key =
derive_key_from_pass(password.as_bytes().to_vec(), salt_password, wallet_id);
let enc_master_key_password = enc_master_key(&master_key, &password_key, 0, wallet_id)?;
password_key.zeroize();
Some(LoginMethod {
salt: salt_password,
enc_master_key: enc_master_key_password,
})
} else {
None
};
let timestamp = now_timestamp();
let encrypted = enc_wallet_log(
@ -720,13 +825,15 @@ pub async fn create_wallet_second_step_v0(
master_key.zeroize();
let wallet_content = WalletContentV0 {
security_img: params.security_img.clone(),
security_img: params
.security_img
.as_ref()
.map(|b| serde_bytes::ByteBuf::from(b.as_slice())),
security_txt: params.security_txt.clone(),
pazzle_length: params.pazzle_length,
salt_pazzle,
salt_mnemonic,
enc_master_key_pazzle,
enc_master_key_mnemonic,
mnemonic: mnemonic_login,
pazzle: pazzle_login,
password,
master_nonce: 0,
timestamp,
peer_id: PubKey::nil(),
@ -736,7 +843,7 @@ pub async fn create_wallet_second_step_v0(
let ser_wallet = serde_bare::to_vec(&wallet_content).unwrap();
let sig = sign(&params.wallet_privkey, &wallet_id, &ser_wallet).unwrap();
let sig: Sig = sign(&params.wallet_privkey, &wallet_id, &ser_wallet).unwrap();
let wallet_v0 = WalletV0 {
// ID
@ -774,7 +881,7 @@ pub async fn create_wallet_second_step_v0(
wallet_file,
pazzle,
mnemonic: mnemonic.clone(),
mnemonic_str: display_mnemonic(&mnemonic),
mnemonic_str: mnemonic.map_or(vec![], |m| display_mnemonic(&m)),
wallet_name: params.wallet_name.clone(),
client: params.client.clone(),
user,
@ -833,10 +940,12 @@ mod test {
let _creation = Instant::now();
let res = create_wallet_first_step_v0(CreateWalletV0::new(
img_buffer,
Some(img_buffer),
" know yourself ".to_string(),
pin,
Some(pin),
9,
None,
true,
false,
false,
BootstrapContentV0::new_localhost(PubKey::nil()),
@ -863,16 +972,23 @@ mod test {
let _ = file.write_all(&ser_wallet);
log_debug!("wallet id: {}", res.wallet.id());
log_debug!("pazzle {:?}", display_pazzle_one(&res.pazzle));
log_debug!("mnemonic {:?}", display_mnemonic(&res.mnemonic));
log_debug!(
"pazzle {:?}",
display_pazzle_one(res.pazzle.as_ref().expect("no pazzle"))
);
log_debug!(
"mnemonic {:?}",
display_mnemonic(&res.mnemonic.expect("no mnemonic"))
);
log_debug!("pin {:?}", pin);
if let Wallet::V0(v0) = &res.wallet {
log_debug!("security text: {:?}", v0.content.security_txt);
let img = v0.content.security_img.as_ref().expect("no securit image");
let mut file =
File::create("tests/generated_security_image.jpg").expect("open write file");
let _ = file.write_all(&v0.content.security_img);
let _ = file.write_all(img);
let f = File::open("tests/generated_security_image.jpg.compare")
.expect("open of generated_security_image.jpg.compare");
@ -883,11 +999,15 @@ mod test {
.read_to_end(&mut generated_security_image_compare)
.expect("read of generated_security_image.jpg.compare");
assert_eq!(v0.content.security_img, generated_security_image_compare);
assert_eq!(img, &generated_security_image_compare);
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.expect("no mnemonic"),
pin.clone(),
)
.expect("open with mnemonic");
//log_debug!("encrypted part {:?}", w);
@ -898,7 +1018,11 @@ mod test {
if v0.content.pazzle_length > 0 {
let _opening_pazzle = Instant::now();
let _w = open_wallet_with_pazzle(&Wallet::V0(v0.clone()), res.pazzle.clone(), pin)
let _w = open_wallet_with_pazzle(
&Wallet::V0(v0.clone()),
res.pazzle.as_ref().expect("no pazzle").clone(),
pin,
)
.expect("open with pazzle");
log_info!(
"opening of wallet with pazzle took: {} ms",

@ -660,27 +660,32 @@ impl SensitiveWalletV0 {
}
}
/// Login method
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoginMethod {
pub salt: [u8; 16],
// encrypted master keys.
// AD = wallet_id
#[serde(with = "BigArray")]
pub enc_master_key: [u8; 48],
}
/// Wallet content Version 0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WalletContentV0 {
#[serde(with = "serde_bytes")]
pub security_img: Vec<u8>,
pub security_img: Option<serde_bytes::ByteBuf>,
pub security_txt: String,
/// can be 9, 12 or 15 (or 0, in this case salt_pazzle and enc_master_key_pazzle are filled with zeros and should not be used)
/// can be 9, 12 or 15 (or 0, if pazzle is deactivated)
pub pazzle_length: u8,
pub salt_pazzle: [u8; 16],
pub pazzle: Option<LoginMethod>,
pub salt_mnemonic: [u8; 16],
pub mnemonic: Option<LoginMethod>,
// encrypted master keys. first is encrypted with pazzle, second is encrypted with mnemonic
// AD = wallet_id
#[serde(with = "BigArray")]
pub enc_master_key_pazzle: [u8; 48],
#[serde(with = "BigArray")]
pub enc_master_key_mnemonic: [u8; 48],
pub password: Option<LoginMethod>,
// nonce for the encryption of masterkey
// incremented only if the masterkey changes
@ -1243,7 +1248,7 @@ pub struct CreateWalletV0 {
/// Please be aware that other users who are sharing the same device, will be able to see this image.
#[zeroize(skip)]
#[serde(with = "serde_bytes")]
pub security_img: Vec<u8>,
pub security_img: Option<Vec<u8>>,
/// A string of characters of minimum length 10.
/// This phrase will be presented to the user every time they are about to enter their pazzle and PIN in order to unlock their wallet.
/// It should be something the user will remember, but not something too personal.
@ -1256,10 +1261,15 @@ pub struct CreateWalletV0 {
/// The PIN and the rest of the Wallet will never be sent to NextGraph or any other third party (check the source code if you don't believe us).
/// It cannot be a series like 1234 or 8765. The same digit cannot repeat more than once. By example 4484 is invalid.
/// Try to avoid birth date, last digits of phone number, or zip code for privacy concern
pub pin: [u8; 4],
pub pin: Option<[u8; 4]>,
/// For now, only 9 is supported. 12 and 15 are planned.
/// A value of 0 will deactivate the pazzle mechanism on this Wallet, and only the mnemonic could be used to open it.
pub pazzle_length: u8,
pub password: Option<String>,
pub mnemonic: bool,
#[zeroize(skip)]
/// Not implemented yet. Will send the bootstrap to our cloud servers, if needed
pub send_bootstrap: bool,
@ -1294,10 +1304,12 @@ pub struct CreateWalletV0 {
impl CreateWalletV0 {
pub fn new(
security_img: Vec<u8>,
security_img: Option<Vec<u8>>,
security_txt: String,
pin: [u8; 4],
pin: Option<[u8; 4]>,
pazzle_length: u8,
password: Option<String>,
mnemonic: bool,
send_bootstrap: bool,
send_wallet: bool,
core_bootstrap: BootstrapContentV0,
@ -1313,6 +1325,8 @@ impl CreateWalletV0 {
security_txt,
pin,
pazzle_length,
password,
mnemonic,
send_bootstrap,
send_wallet,
core_bootstrap,
@ -1351,10 +1365,10 @@ pub struct CreateWalletResultV0 {
/// The binary file that can be saved to disk and given to the user
pub wallet_file: Vec<u8>,
/// randomly generated pazzle
pub pazzle: Vec<u8>,
pub pazzle: Option<Vec<u8>>,
/// 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],
pub mnemonic: Option<[u16; 12]>,
/// The words of the mnemonic, in a human readable form.
pub mnemonic_str: Vec<String>,
#[zeroize(skip)]
@ -1400,13 +1414,17 @@ pub struct CreateWalletIntermediaryV0 {
pub in_memory: bool,
#[zeroize(skip)]
pub security_img: Vec<u8>,
pub security_img: Option<Vec<u8>>,
pub security_txt: String,
pub pazzle_length: u8,
pub pin: [u8; 4],
pub mnemonic: bool,
pub password: Option<String>,
pub pin: Option<[u8; 4]>,
#[zeroize(skip)]
pub send_bootstrap: bool,
@ -1439,6 +1457,9 @@ pub enum NgWalletError {
NoCreateWalletPresent,
InvalidBootstrap,
SerializationError,
MnemonicOrPazzleNeedAPin,
NoLoginMethod,
LoginMethodNotSupported,
}
impl From<NgWalletError> for NgError {

@ -75,7 +75,7 @@
window.location.href = result.url;
} else {
wait = true;
window.history.go(-1);
window.location.href = document.referrer;
}
}
}

@ -23,7 +23,7 @@
"svelte": "^3.58.0",
"postcss": "^8.4.23",
"postcss-load-config": "^4.0.1",
"svelte-heros-v2": "^0.10.12",
"svelte-heros-v2": "^1.3.0",
"svelte-preprocess": "^5.0.3",
"tailwindcss": "^3.3.1",
"vite-plugin-svelte-svg": "^2.2.1",

@ -26,7 +26,7 @@
"vite": "^4.3.9",
"postcss": "^8.4.23",
"postcss-load-config": "^4.0.1",
"svelte-heros-v2": "^0.10.12",
"svelte-heros-v2": "^1.3.0",
"svelte-preprocess": "^5.0.3",
"tailwindcss": "^3.3.1",
"autoprefixer": "^10.4.14",

@ -203,8 +203,8 @@ importers:
specifier: ^3.0.0
version: 3.8.6(@babel/core@7.28.4)(postcss-load-config@4.0.2(postcss@8.5.6))(postcss@8.5.6)(svelte@3.59.2)
svelte-heros-v2:
specifier: ^0.10.12
version: 0.10.12(svelte@3.59.2)
specifier: ^1.3.0
version: 1.3.0(svelte@3.59.2)
svelte-preprocess:
specifier: ^5.0.3
version: 5.1.4(@babel/core@7.28.4)(postcss-load-config@4.0.2(postcss@8.5.6))(postcss@8.5.6)(svelte@3.59.2)(typescript@4.9.5)
@ -279,8 +279,8 @@ importers:
specifier: ^3.58.0
version: 3.59.2
svelte-heros-v2:
specifier: ^0.10.12
version: 0.10.12(svelte@3.59.2)
specifier: ^1.3.0
version: 1.3.0(svelte@3.59.2)
svelte-preprocess:
specifier: ^5.0.3
version: 5.1.4(@babel/core@7.28.4)(postcss-load-config@4.0.2(postcss@8.5.6))(postcss@8.5.6)(svelte@3.59.2)(typescript@5.9.2)
@ -453,8 +453,8 @@ importers:
specifier: ^3.58.0
version: 3.59.2
svelte-heros-v2:
specifier: ^0.10.12
version: 0.10.12(svelte@3.59.2)
specifier: ^1.3.0
version: 1.3.0(svelte@3.59.2)
svelte-preprocess:
specifier: ^5.0.3
version: 5.1.4(@babel/core@7.28.4)(postcss-load-config@4.0.2(postcss@8.5.6))(postcss@8.5.6)(svelte@3.59.2)(typescript@5.9.2)
@ -520,8 +520,8 @@ importers:
specifier: ^3.58.0
version: 3.59.2
svelte-heros-v2:
specifier: ^0.10.12
version: 0.10.12(svelte@3.59.2)
specifier: ^1.3.0
version: 1.3.0(svelte@3.59.2)
svelte-preprocess:
specifier: ^5.0.3
version: 5.1.4(@babel/core@7.28.4)(postcss-load-config@4.0.2(postcss@8.5.6))(postcss@8.5.6)(svelte@3.59.2)(typescript@5.9.2)
@ -6546,10 +6546,10 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte-heros-v2@0.10.12:
resolution: {integrity: sha512-0wspy0z9UFS9f/iPKQQ1JDHlNY6e7h+LVW+wJ0qJnuWDpvsJllmoCX2g0frYbMPDWZJEwh2pkO25Dp3lDGCxGQ==}
svelte-heros-v2@1.3.0:
resolution: {integrity: sha512-H+s2Z907WU8sLG/dOYGfiIq7mxtACm6LM+A8jdcDCWtjyyoOmtL2waZEKKXsLrcwO5g5/D6i0TqSs0UJuchRoA==}
peerDependencies:
svelte: ^3.54.0 || ^4.0.0
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0
svelte-hmr@0.15.3:
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
@ -14507,7 +14507,7 @@ snapshots:
transitivePeerDependencies:
- picomatch
svelte-heros-v2@0.10.12(svelte@3.59.2):
svelte-heros-v2@1.3.0(svelte@3.59.2):
dependencies:
svelte: 3.59.2

@ -175,6 +175,18 @@ pub fn privkey_to_string(privkey: JsValue) -> Result<String, JsValue> {
Ok(format!("{p}"))
}
pub fn wallet_open_with_password(wallet: JsValue, password: String) -> Result<JsValue, JsValue> {
let encrypted_wallet = serde_wasm_bindgen::from_value::<Wallet>(wallet)
.map_err(|_| "Deserialization error of wallet")?;
let res = nextgraph::local_broker::wallet_open_with_password(&encrypted_wallet, password);
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_pazzle(
wallet: JsValue,
@ -924,7 +936,7 @@ static INIT_LOCAL_BROKER: Lazy<Box<ConfigInitFn>> = Lazy::new(|| {
pub async fn wallet_create(params: JsValue) -> Result<JsValue, JsValue> {
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let mut params = serde_wasm_bindgen::from_value::<CreateWalletV0>(params)
.map_err(|_| "Deserialization error of args")?;
.map_err(|e| format!("Deserialization error of args {e}"))?;
params.result_with_wallet_file = true;
let res = nextgraph::local_broker::wallet_create_v0(params).await;
match res {
@ -2143,10 +2155,12 @@ pub async fn gen_wallet_for_test(ngd_peer_id: String) -> Result<JsValue, String>
let peer_id_of_server_broker = decode_key(&ngd_peer_id).map_err(|e: NgError| e.to_string())?;
let wallet_result = wallet_create_v0(CreateWalletV0 {
security_img: Vec::from(EMPTY_IMG),
security_img: None,
security_txt: "testsecurityphrase".to_string(),
pin: [1, 2, 1, 2],
pin: Some([1, 2, 1, 2]),
pazzle_length: 9,
mnemonic: true,
password: None,
send_bootstrap: false,
send_wallet: false,
result_with_wallet_file: false,
@ -2161,7 +2175,7 @@ pub async fn gen_wallet_for_test(ngd_peer_id: String) -> Result<JsValue, String>
.expect("wallet_create_v0");
let mut mnemonic_words = Vec::with_capacity(12);
display_mnemonic(&wallet_result.mnemonic)
display_mnemonic(&wallet_result.mnemonic.unwrap())
.iter()
.for_each(|word| {
mnemonic_words.push(word.clone());

@ -1589,6 +1589,8 @@ pub async fn wallet_create_v0(params: CreateWalletV0) -> Result<CreateWalletResu
return Err(NgError::CannotSaveWhenInMemoryConfig);
}
let in_memory = !params.local_save;
let has_pazzle = params.pazzle_length > 0;
let has_mnemonic = params.mnemonic;
let intermediate = create_wallet_first_step_v0(params)?;
let lws: LocalWalletStorageV0 = (&intermediate).into();
@ -1619,9 +1621,13 @@ pub async fn wallet_create_v0(params: CreateWalletV0) -> Result<CreateWalletResu
let (mut res, site, brokers) =
create_wallet_second_step_v0(intermediate, &mut session.verifier).await?;
if with_pdf {
let wallet_recovery =
wallet_to_wallet_recovery(&res.wallet, res.pazzle.clone(), res.mnemonic, pin);
if with_pdf && pin.is_some() && has_mnemonic && has_pazzle {
let wallet_recovery = wallet_to_wallet_recovery(
&res.wallet,
res.pazzle.clone().unwrap(),
res.mnemonic.clone().unwrap(),
pin.unwrap(),
);
if let Ok(pdf_buffer) = wallet_recovery_pdf(wallet_recovery, 600).await {
res.pdf_file = pdf_buffer;
@ -1706,7 +1712,7 @@ pub fn wallet_to_wallet_recovery(
match wallet {
Wallet::V0(v0) => {
let mut content = v0.content.clone();
content.security_img = vec![];
content.security_img = None;
content.security_txt = String::new();
NgQRCodeWalletRecoveryV0 {
wallet: serde_bare::to_vec(&content).unwrap(),
@ -2145,6 +2151,16 @@ pub async fn wallet_get_file(wallet_name: &String) -> Result<Vec<u8>, NgError> {
}
}
#[doc(hidden)]
pub fn wallet_open_with_password(
wallet: &Wallet,
password: String,
) -> Result<SensitiveWallet, NgError> {
let opened_wallet = ng_wallet::open_wallet_with_password(wallet, password)?;
Ok(opened_wallet)
}
#[doc(hidden)]
/// This is a bit hard to use as the pazzle words are encoded in unsigned bytes.
/// prefer the function wallet_open_with_pazzle_words
@ -3045,10 +3061,12 @@ mod test {
let peer_id_of_server_broker = PubKey::nil();
let wallet_result = wallet_create_v0(CreateWalletV0 {
security_img,
security_img: Some(security_img),
security_txt: "know yourself".to_string(),
pin: [1, 2, 1, 2],
pin: Some([1, 2, 1, 2]),
pazzle_length: 9,
password: None,
mnemonic: true,
send_bootstrap: false,
send_wallet: false,
result_with_wallet_file: true,
@ -3063,9 +3081,10 @@ mod test {
.await
.expect("wallet_create_v0");
let pazzle = display_pazzle(&wallet_result.pazzle);
let pazzle_vec = wallet_result.pazzle.clone().unwrap();
let pazzle = display_pazzle(&pazzle_vec);
let mut pazzle_words = vec![];
println!("Your pazzle is: {:?}", wallet_result.pazzle);
println!("Your pazzle is: {:?}", pazzle_vec);
for emoji in pazzle {
println!(" {}:\t{}", emoji.0, emoji.1);
pazzle_words.push(emoji.1.to_string());
@ -3080,7 +3099,7 @@ mod test {
println!("Your mnemonic is:");
let mut mnemonic_words = vec![];
display_mnemonic(&wallet_result.mnemonic)
display_mnemonic(&wallet_result.mnemonic.unwrap())
.iter()
.for_each(|word| {
mnemonic_words.push(word.clone());

Loading…
Cancel
Save