diff --git a/Cargo.lock b/Cargo.lock index f550fea..1ab0110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2797,14 +2797,13 @@ checksum = "f850fafca79ebacd70eab9d80cb75a33aeda38bde8f3dd784c1837cdf0bde631" [[package]] name = "json-patch" -version = "1.0.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658" +checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" dependencies = [ "serde", "serde_json", "thiserror", - "treediff", ] [[package]] @@ -2942,9 +2941,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" dependencies = [ "value-bag", ] @@ -3264,6 +3263,7 @@ dependencies = [ "async-trait", "base64-url", "futures", + "lazy_static", "ng-client-ws", "ng-net", "ng-repo", @@ -3271,9 +3271,12 @@ dependencies = [ "ng-verifier", "ng-wallet", "once_cell", + "qrcode", "serde_bare", + "serde_bytes", "serde_json", "web-time", + "whoami", "zeroize", ] @@ -3293,7 +3296,9 @@ dependencies = [ "sys-locale", "tauri", "tauri-build", + "tauri-plugin-barcode-scanner", "tauri-plugin-window", + "tauri-utils", ] [[package]] @@ -3547,6 +3552,7 @@ dependencies = [ "aes-gcm-siv", "argon2", "async-std", + "base64-url", "blake3", "chacha20poly1305", "crypto_box", @@ -4436,6 +4442,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" + [[package]] name = "quick-xml" version = "0.28.2" @@ -4608,6 +4620,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -5575,6 +5596,20 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin-barcode-scanner" +version = "2.0.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058922dd9cafc89a865593c99507177c4cdbdad9d22a911ac41872ed7dbf0348" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "thiserror", +] + [[package]] name = "tauri-plugin-window" version = "2.0.0-alpha.1" @@ -6043,15 +6078,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "treediff" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" -dependencies = [ - "serde_json", -] - [[package]] name = "try-lock" version = "0.2.4" @@ -6198,9 +6224,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" @@ -6325,6 +6351,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -6540,6 +6572,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/README.md b/README.md index a1a8a3a..e8d9f17 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ Read our [getting started guide](https://docs.nextgraph.org/en/getting-started/) ## For contributors -- [Install Rust](https://www.rust-lang.org/tools/install) minimum required MSRV 1.74.0 -- [Install Nodejs](https://nodejs.org/en/download/) -- [Install LLVM](https://rust-lang.github.io/rust-bindgen/requirements.html) +- [Install Rust](https://www.rust-lang.org/tools/install) minimum required MSRV 1.74.0 +- [Install Nodejs](https://nodejs.org/en/download/) +- [Install LLVM](https://rust-lang.github.io/rust-bindgen/requirements.html) On openbsd, for LLVM you need to choose llvm-17. @@ -57,6 +57,14 @@ cargo install wasm-pack --git https://github.com/rustwasm/wasm-pack.git --rev c2 then : +create a file called `nextgraph/src/local_broker_dev_env.rs` with the content : + +``` +pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA"; +``` + +once your ngd server will run in your dev env, replace the above string with the actual PEER ID of your ngd server. + ``` cargo install cargo-watch // optionally, if you want a Rust REPL: cargo install evcxr_repl @@ -70,20 +78,20 @@ cargo build The crates are organized as follow : -- [nextgraph](nextgraph/README.md) : Client library. Use this crate to embed NextGraph client in your Rust application -- [ngcli](ngcli/README.md) : CLI tool to manipulate the local documents and repos and administrate the server -- [ngd](ngd/README.md) : binary executable of the daemon (that can run a broker, verifier and/or Rust services) -- [ng-app](ng-app/README.md) : all the native apps, based on Tauri, and the official web app. -- [ng-sdk-js](ng-sdk-js/DEV.md) : contains the JS SDK, with example for: web app, react app, or node service. -- ng-repo : Repositories common library -- ng-net : Network common library -- ng-verifier : Verifier library, that exposes the document API to the app -- ng-wallet : keeps the secret keys of all identities of the user in a safe wallet -- ng-broker : Core and Server Broker library -- ng-client-ws : Websocket client library -- ng-storage-rocksdb : RocksDB backed stores. see also dependency [repo here](https://git.nextgraph.org/NextGraph/rust-rocksdb) -- ngone : server for nextgraph.one. helps user bootstrap into the right app. Not useful to you. Published here for transparency -- ngaccount : server for nextgraph's Broker Service Provider account manager. Not useful to you. Published here for transparency +- [nextgraph](nextgraph/README.md) : Client library. Use this crate to embed NextGraph client in your Rust application +- [ngcli](ngcli/README.md) : CLI tool to manipulate the local documents and repos and administrate the server +- [ngd](ngd/README.md) : binary executable of the daemon (that can run a broker, verifier and/or Rust services) +- [ng-app](ng-app/README.md) : all the native apps, based on Tauri, and the official web app. +- [ng-sdk-js](ng-sdk-js/DEV.md) : contains the JS SDK, with example for: web app, react app, or node service. +- ng-repo : Repositories common library +- ng-net : Network common library +- ng-verifier : Verifier library, that exposes the document API to the app +- ng-wallet : keeps the secret keys of all identities of the user in a safe wallet +- ng-broker : Core and Server Broker library +- ng-client-ws : Websocket client library +- ng-storage-rocksdb : RocksDB backed stores. see also dependency [repo here](https://git.nextgraph.org/NextGraph/rust-rocksdb) +- ngone : server for nextgraph.one. helps user bootstrap into the right app. Not useful to you. Published here for transparency +- ngaccount : server for nextgraph's Broker Service Provider account manager. Not useful to you. Published here for transparency ### Run @@ -224,9 +232,9 @@ additional terms or conditions. Licensed under either of -- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - at your option. +- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + at your option. `SPDX-License-Identifier: Apache-2.0 OR MIT` diff --git a/nextgraph/.gitignore b/nextgraph/.gitignore index 3598c30..01f9583 100644 --- a/nextgraph/.gitignore +++ b/nextgraph/.gitignore @@ -1 +1,2 @@ -tests \ No newline at end of file +tests +local_broker_dev_env_peer_id.rs \ No newline at end of file diff --git a/nextgraph/Cargo.toml b/nextgraph/Cargo.toml index a160823..ae4d0d8 100644 --- a/nextgraph/Cargo.toml +++ b/nextgraph/Cargo.toml @@ -18,6 +18,7 @@ maintenance = { status = "actively-developed" } [dependencies] serde_bare = "0.5.0" serde_json = "1.0" +serde_bytes = "0.11.7" base64-url = "2.0.0" once_cell = "1.17.1" zeroize = { version = "1.7.0", features = ["zeroize_derive"] } @@ -25,7 +26,10 @@ futures = "0.3.24" async-std = { version = "1.12.0", features = [ "attributes", "unstable" ] } async-trait = "0.1.64" async-once-cell = "0.5.3" +lazy_static = "1.4.0" web-time = "0.2.0" +whoami = "1.5.1" +qrcode = { version = "0.14.1", default-features = false, features = ["svg"] } ng-repo = { path = "../ng-repo", version = "0.1.0-preview.1" } ng-net = { path = "../ng-net", version = "0.1.0-preview.1" } ng-wallet = { path = "../ng-wallet", version = "0.1.0-preview.5" } diff --git a/nextgraph/src/lib.rs b/nextgraph/src/lib.rs index 9dbf3a0..1e31fe3 100644 --- a/nextgraph/src/lib.rs +++ b/nextgraph/src/lib.rs @@ -98,3 +98,37 @@ pub mod verifier { pub mod wallet { pub use ng_wallet::*; } + +pub fn get_device_name() -> String { + let mut list: Vec = Vec::with_capacity(3); + #[cfg(not(target_arch = "wasm32"))] + if let Ok(realname) = whoami::fallible::realname() { + list.push(realname); + } else { + #[cfg(not(target_arch = "wasm32"))] + if let Ok(username) = whoami::fallible::username() { + list.push(username); + } + } + if let Ok(devicename) = whoami::fallible::devicename() { + list.push(devicename); + } else { + #[cfg(not(target_arch = "wasm32"))] + if let Ok(hostname) = whoami::fallible::hostname() { + list.push(hostname); + } else { + if let Ok(distro) = whoami::fallible::distro() { + list.push(distro); + } + } + } + #[cfg(target_arch = "wasm32")] + if let Ok(distro) = whoami::fallible::distro() { + list.push(distro); + } + + list.join(" ") +} + +#[cfg(debug_assertions)] +mod local_broker_dev_env; diff --git a/nextgraph/src/local_broker.rs b/nextgraph/src/local_broker.rs index edf7e60..81ce09e 100644 --- a/nextgraph/src/local_broker.rs +++ b/nextgraph/src/local_broker.rs @@ -20,6 +20,8 @@ use once_cell::sync::Lazy; use serde_bare::to_vec; use serde_json::json; use zeroize::Zeroize; +use qrcode::{render::svg, QrCode}; +use lazy_static::lazy_static; use ng_repo::block_storage::BlockStorage; use ng_repo::block_storage::HashMapBlockStorage; @@ -27,7 +29,7 @@ use ng_repo::errors::{NgError, ProtocolError}; use ng_repo::log::*; use ng_repo::os_info::get_os_info; use ng_repo::types::*; -use ng_repo::utils::{derive_key, generate_keypair}; +use ng_repo::utils::{derive_key, generate_keypair, encrypt_in_place}; use ng_net::{actor::*,actors::admin::*}; use ng_net::broker::*; @@ -1492,6 +1494,235 @@ pub async fn wallet_add(lws: LocalWalletStorageV0) -> Result<(), NgError> { } Ok(()) } +#[cfg(debug_assertions)] +lazy_static! { + + static ref NEXTGRAPH_EU: BrokerServerV0 = BrokerServerV0 { + server_type: BrokerServerTypeV0::Localhost(14400), + can_verify: false, + can_forward: false, + peer_id: ng_repo::utils::decode_key({use crate::local_broker_dev_env::PEER_ID; PEER_ID}).unwrap(), + }; +} + +#[cfg(not(debug_assertions))] +lazy_static! { + static ref NEXTGRAPH_EU: BrokerServerV0 = BrokerServerV0 { + server_type: BrokerServerTypeV0::Domain("nextgraph.eu".to_string()), + can_verify: false, + can_forward: false, + peer_id: ng_repo::utils::decode_key("FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA").unwrap(), + }; + +} + +/// Obtains a Wallet object from a QRCode or a TextCode. +/// +/// The returned object can be used to import the wallet into a new Device +/// with the help of the function [wallet_open_with_pazzle_words] +/// followed by [wallet_import] +pub async fn wallet_import_from_code(code: String) -> Result { + + let qr = NgQRCode::from_code(code)?; + match qr { + NgQRCode::V0(NgQRCodeV0{broker, rendezvous, secret_key, is_rendezvous}) => { + let wallet: ExportedWallet = do_ext_call( + &broker, + ExtWalletGetExportV0 { + id:rendezvous, + is_rendezvous + }).await?; + + let mut buf = wallet.0.into_vec(); + encrypt_in_place(&mut buf,*secret_key.slice(), [0;12]); + let wallet: Wallet = serde_bare::from_slice(&buf)?; + + let broker = match LOCAL_BROKER.get() { + None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized), + Some(Ok(broker)) => broker.read().await, + }; + // check that the wallet is not already present in local_broker + let wallet_name = wallet.name(); + if broker.wallets.get(&wallet_name).is_none() { + Ok(wallet) + } else { + Err(NgError::WalletAlreadyAdded) + } + + } + } +} + +/// Starts a rendez-vous to obtain a wallet from other device. +/// +/// A rendezvous is used when the device that is importing, doesn't have a camera. +/// The QRCode is displayed on that device, and another device (with camera, and with the wallet) will scan it. +/// +/// Returns the QRcode in SVG format, and the code (a string) to be used with [wallet_import_from_code] +pub async fn wallet_import_rendezvous(size: u32) -> Result<(String,String), NgError> { + let code = NgQRCode::V0(NgQRCodeV0 { + broker: NEXTGRAPH_EU.clone(), + rendezvous: SymKey::random(), + secret_key: SymKey::random(), + is_rendezvous: true + }); + let code_string = code.to_code(); + + let code_svg = match QrCode::with_error_correction_level(&code_string, qrcode::EcLevel::M) { + Ok(qr) => { + let svg = qr + .render() + .max_dimensions(size, size) + .dark_color(svg::Color("#000000")) + .light_color(svg::Color("#ffffff")) + .build(); + svg + }, + Err(_e) => {return Err(NgError::BrokerError)} + }; + + Ok((code_svg,code_string)) +} + +/// Gets the TextCode to display in order to export the wallet of the current session ID +/// +/// The ExportedWallet is valid for 5 min. +/// +/// Returns the TextCode +pub async fn wallet_export_get_textcode(session_id: u64) -> Result { + + let broker = match LOCAL_BROKER.get() { + None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized), + Some(Ok(broker)) => broker.read().await, + }; + + match &broker.config { + LocalBrokerConfig::Headless(_) => { + return Err(NgError::LocalBrokerIsHeadless) + }, + _ => { + let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(session_id)?; + + if is_remote { + return Err(NgError::NotImplemented); + } else { + let session = broker.opened_sessions_list[real_session_id].as_ref() + .ok_or(NgError::SessionNotFound)?; + let wallet_name = session.config.wallet_name(); + + match broker.wallets.get(&wallet_name) { + None => Err(NgError::WalletNotFound), + Some(lws) => { + //let broker = lws.bootstrap.servers().first().unwrap(); + let wallet = &lws.wallet; + let secret_key = SymKey::random(); + let rendezvous = SymKey::random(); + let code = NgQRCode::V0(NgQRCodeV0 { + broker: NEXTGRAPH_EU.clone(), + rendezvous: rendezvous.clone(), + secret_key: secret_key.clone(), + is_rendezvous: false + }); + let code_string = code.to_code(); + let mut wallet_ser = serde_bare::to_vec(wallet)?; + encrypt_in_place(&mut wallet_ser,*secret_key.slice(), [0;12]); + let exported_wallet = ExportedWallet(serde_bytes::ByteBuf::from(wallet_ser)); + match session.verifier.client_request::(WalletPutExport::V0(WalletPutExportV0{wallet:exported_wallet, rendezvous_id:rendezvous, is_rendezvous:false})).await { + Err(e) => Err(e), + Ok(SoS::Stream(_)) => Err(NgError::InvalidResponse), + Ok(SoS::Single(_)) => Ok(code_string) + } + } + } + } + } + } +} + +/// Gets the QRcode to display in order to export a wallet of the current session ID +/// +/// The ExportedWallet is valid for 5 min. +/// +/// Returns the QRcode in SVG format +pub async fn wallet_export_get_qrcode(session_id: u64, size: u32) -> Result { + + let code_string = wallet_export_get_textcode(session_id).await?; + + let code_svg = match QrCode::with_error_correction_level(&code_string, qrcode::EcLevel::M) { + Ok(qr) => { + let svg = qr + .render() + .max_dimensions(size, size) + .dark_color(svg::Color("#000000")) + .light_color(svg::Color("#ffffff")) + .build(); + svg + }, + Err(_e) => {return Err(NgError::BrokerError)} + }; + + Ok(code_svg) + +} + +/// Puts the Wallet to the rendezvous ID that was scanned +/// +/// The rendezvous ID is valid for 5 min. +pub async fn wallet_export_rendezvous(session_id: u64, code: String) -> Result<(), NgError> { + + let qr = NgQRCode::from_code(code)?; + match qr { + NgQRCode::V0(NgQRCodeV0{broker: _, rendezvous, secret_key, is_rendezvous}) => { + + if !is_rendezvous { + return Err(NgError::NotARendezVous); + } + + let broker = match LOCAL_BROKER.get() { + None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized), + Some(Ok(broker)) => broker.read().await, + }; + + match &broker.config { + LocalBrokerConfig::Headless(_) => { + return Err(NgError::LocalBrokerIsHeadless) + }, + _ => { + let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(session_id)?; + + if is_remote { + return Err(NgError::NotImplemented); + } else { + let session = broker.opened_sessions_list[real_session_id].as_ref() + .ok_or(NgError::SessionNotFound)?; + let wallet_name = session.config.wallet_name(); + + match broker.wallets.get(&wallet_name) { + None => Err(NgError::WalletNotFound), + Some(lws) => { + //let broker = lws.bootstrap.servers().first().unwrap(); + let wallet = &lws.wallet; + + let mut wallet_ser = serde_bare::to_vec(wallet)?; + encrypt_in_place(&mut wallet_ser,*secret_key.slice(), [0;12]); + let exported_wallet = ExportedWallet(serde_bytes::ByteBuf::from(wallet_ser)); + + // TODO: send the WalletPutExport client request to the broker received from QRcode. for now it is cheer luck that all clients are connected to nextgraph.eu. + // if the user doesn't have an account with nextgraph.eu, their broker should relay the request (core protocol ?) + + match session.verifier.client_request::(WalletPutExport::V0(WalletPutExportV0{wallet:exported_wallet, rendezvous_id:rendezvous, is_rendezvous:true})).await { + Err(e) => Err(e), + Ok(SoS::Stream(_)) => Err(NgError::InvalidResponse), + Ok(SoS::Single(_)) => Ok(()) + } + } + } + } + } + } + } + } +} /// Reads a binary Wallet File and decodes it to a Wallet object. /// @@ -2126,7 +2357,7 @@ pub async fn app_request(request: AppRequest) -> Result { let session = broker.opened_sessions_list[real_session_id] .as_mut() .ok_or(NgError::SessionNotFound)?; - session.verifier.app_request(request).await + session.verifier.app_request(request).await } } } @@ -2214,6 +2445,29 @@ async fn do_admin_call< .await } +async fn do_ext_call< + A: Into + Into + std::fmt::Debug + Sync + Send + 'static, + B: TryFrom + + std::fmt::Debug + + Sync + + Send + + 'static, +>( + broker_server: &BrokerServerV0, + cmd: A, +) -> Result { + let (peer_privk, peer_pubk) = generate_keypair(); + Broker::ext( + Box::new(ConnectionWebSocket {}), + peer_privk, + peer_pubk, + broker_server.peer_id, + broker_server.get_ws_url(&None).await.unwrap().0, // for now we are only connecting to NextGraph SaaS cloud (nextgraph.eu) so it is safe. + cmd, + ) + .await +} + #[doc(hidden)] pub async fn admin_create_user(server_peer_id: DirectPeerId, admin_user_key: PrivKey, server_addr: BindAddress) -> Result { diff --git a/nextgraph/src/local_broker_dev_env.rs b/nextgraph/src/local_broker_dev_env.rs new file mode 100644 index 0000000..1ddaafe --- /dev/null +++ b/nextgraph/src/local_broker_dev_env.rs @@ -0,0 +1 @@ +pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA"; diff --git a/ng-app/package.json b/ng-app/package.json index 339a935..02f38c4 100644 --- a/ng-app/package.json +++ b/ng-app/package.json @@ -18,6 +18,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "@tauri-apps/api": "2.0.0-alpha.8", + "@tauri-apps/plugin-barcode-scanner": "2.0.0-alpha.0", "@tauri-apps/plugin-window": "2.0.0-alpha.1", "async-proxy": "^0.4.1", "classnames": "^2.3.2", diff --git a/ng-app/src-tauri/Cargo.toml b/ng-app/src-tauri/Cargo.toml index 0c9ba1a..89b0715 100644 --- a/ng-app/src-tauri/Cargo.toml +++ b/ng-app/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2.0.0-alpha.8", features = [] } # tauri-macros = { version = "=2.0.0-alpha.6" } # tauri-codegen = { version = "=2.0.0-alpha.6" } -# tauri-utils = { version = "=2.0.0-alpha.6" } +tauri-utils = { version = "=2.0.0-alpha.7" } [dependencies] serde = { version = "1.0", features = ["derive"] } @@ -32,6 +32,7 @@ sys-locale = { version = "0.3.1" } ng-async-tungstenite = { git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime", "async-native-tls"] } tauri = { version = "2.0.0-alpha.14", features = [] } tauri-plugin-window = "2.0.0-alpha.1" +tauri-plugin-barcode-scanner = "=2.0.0-alpha.0" # tauri-plugin-window = { git = "https://git.nextgraph.org/NextGraph/plugins-workspace.git", branch="window-alpha.1-nextgraph" } # tauri = { git = "https://git.nextgraph.org/NextGraph/tauri.git", branch="alpha.11-nextgraph", features = ["no-ipc-custom-protocol"] } # tauri = { git = "https://github.com/simonhyll/tauri.git", branch="fix/ipc-mixup", features = [] } diff --git a/ng-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/ng-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml index a057f34..493b196 100644 --- a/ng-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/ng-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + Result { - let wallet = nextgraph::local_broker::wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, pin) - .map_err(|e| e.to_string())?; + let wallet = + nextgraph::local_broker::wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, pin) + .map_err(|e| e.to_string())?; Ok(wallet) } @@ -205,6 +206,55 @@ async fn wallet_import( .map_err(|e: NgError| e.to_string()) } +#[tauri::command(rename_all = "snake_case")] +async fn wallet_export_rendezvous( + session_id: u64, + code: String, + _app: tauri::AppHandle, +) -> Result<(), String> { + nextgraph::local_broker::wallet_export_rendezvous(session_id, code) + .await + .map_err(|e: NgError| e.to_string()) +} + +#[tauri::command(rename_all = "snake_case")] +async fn wallet_export_get_qrcode( + session_id: u64, + size: u32, + _app: tauri::AppHandle, +) -> Result { + nextgraph::local_broker::wallet_export_get_qrcode(session_id, size) + .await + .map_err(|e: NgError| e.to_string()) +} + +#[tauri::command(rename_all = "snake_case")] +async fn wallet_export_get_textcode( + session_id: u64, + _app: tauri::AppHandle, +) -> Result { + nextgraph::local_broker::wallet_export_get_textcode(session_id) + .await + .map_err(|e: NgError| e.to_string()) +} + +#[tauri::command(rename_all = "snake_case")] +async fn wallet_import_rendezvous( + size: u32, + _app: tauri::AppHandle, +) -> Result<(String, String), String> { + nextgraph::local_broker::wallet_import_rendezvous(size) + .await + .map_err(|e: NgError| e.to_string()) +} + +#[tauri::command(rename_all = "snake_case")] +async fn wallet_import_from_code(code: String, _app: tauri::AppHandle) -> Result { + nextgraph::local_broker::wallet_import_from_code(code) + .await + .map_err(|e: NgError| e.to_string()) +} + #[tauri::command(rename_all = "snake_case")] async fn get_wallets( app: tauri::AppHandle, @@ -491,6 +541,11 @@ fn client_info_rust() -> Result { Ok(ng_repo::os_info::get_os_info()) } +#[tauri::command(rename_all = "snake_case")] +fn get_device_name() -> Result { + Ok(nextgraph::get_device_name()) +} + #[derive(Default)] pub struct AppBuilder { setup: Option, @@ -517,7 +572,7 @@ impl AppBuilder { pub fn run(self) { let setup = self.setup; - tauri::Builder::default() + let builder = tauri::Builder::default() .setup(move |app| { if let Some(setup) = setup { (setup)(app)?; @@ -533,7 +588,14 @@ impl AppBuilder { } Ok(()) }) - .plugin(tauri_plugin_window::init()) + .plugin(tauri_plugin_window::init()); + + #[cfg(mobile)] + { + let builder = builder.plugin(tauri_plugin_barcode_scanner::init()); + } + + builder .invoke_handler(tauri::generate_handler![ test, locales, @@ -547,6 +609,11 @@ impl AppBuilder { wallet_read_file, wallet_get_file, wallet_import, + wallet_export_rendezvous, + wallet_export_get_qrcode, + wallet_export_get_textcode, + wallet_import_rendezvous, + wallet_import_from_code, wallet_close, encode_create_account, session_start, @@ -565,6 +632,7 @@ impl AppBuilder { app_request_stream, app_request, upload_chunk, + get_device_name, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/ng-app/src/App.svelte b/ng-app/src/App.svelte index 948fe8d..98c15d7 100644 --- a/ng-app/src/App.svelte +++ b/ng-app/src/App.svelte @@ -35,6 +35,7 @@ import User from "./routes/User.svelte"; import UserRegistered from "./routes/UserRegistered.svelte"; import Install from "./routes/Install.svelte"; + import ScanQR from "./routes/ScanQR.svelte"; import ng from "./api"; import AccountInfo from "./routes/AccountInfo.svelte"; @@ -53,6 +54,7 @@ routes.set("/user/registered", UserRegistered); routes.set("/wallet", WalletInfo); routes.set("/user/accounts", AccountInfo); + routes.set("/wallet/scanqr", ScanQR); if (import.meta.env.NG_APP_WEB) routes.set("/install", Install); routes.set(/^\/did:ng(.*)/i, NURI); routes.set("*", NotFound); diff --git a/ng-app/src/api.ts b/ng-app/src/api.ts index 91732c0..7c0f423 100644 --- a/ng-app/src/api.ts +++ b/ng-app/src/api.ts @@ -22,6 +22,11 @@ const mapping = { "wallet_read_file": ["file"], "wallet_get_file": ["wallet_name"], "wallet_import": ["encrypted_wallet","opened_wallet","in_memory"], + "wallet_export_rendezvous": ["session_id", "code"], + "wallet_export_get_qrcode": ["session_id", "size"], + "wallet_export_get_textcode": ["session_id"], + "wallet_import_rendezvous": ["size"], + "wallet_import_from_code": ["code"], "wallet_close": ["wallet_name"], "encode_create_account": ["payload"], "session_start": ["wallet_name","user"], @@ -34,6 +39,7 @@ const mapping = { "user_disconnect": ["user_id"], "app_request": ["request"], "test": [ ], + "get_device_name": [], "doc_fetch_private_subscribe": [], "doc_fetch_repo_subscribe": ["repo_id"], } diff --git a/ng-app/src/lib/Login.svelte b/ng-app/src/lib/Login.svelte index 2379488..b9c086a 100644 --- a/ng-app/src/lib/Login.svelte +++ b/ng-app/src/lib/Login.svelte @@ -27,12 +27,12 @@ Backspace, ArrowPath, LockOpen, - Key, CheckCircle, ArrowLeft, } from "svelte-heros-v2"; import PasswordInput from "./components/PasswordInput.svelte"; import Spinner from "./components/Spinner.svelte"; + import { display_error } from "../store"; //import Worker from "../worker.js?worker&inline"; export let wallet; export let for_import = false; @@ -48,9 +48,13 @@ onMount(async () => { loaded = false; + if (for_import) { + device_name = await ng.get_device_name(); + } load_svg(); //console.log(wallet); await init(); + }); async function init() { @@ -122,6 +126,8 @@ let unlockWith: "pazzle" | "mnemonic" | undefined; + let device_name; + function order() { step = "order"; ordered = []; @@ -168,6 +174,7 @@ // open the wallet try { if (tauri_platform) { + // TODO @niko: Add device_name as param to open_with_* APIs let opened_wallet = unlockWith === "pazzle" ? await ng.wallet_open_with_pazzle(wallet, pazzle, pin_code) @@ -191,6 +198,7 @@ wallet: opened_wallet, id: opened_wallet.V0.wallet_id, trusted, + device_name, }); } else { let worker_import = await import("../worker.js?worker&inline"); @@ -211,9 +219,14 @@ //console.log("Message received from worker", msg.data); if (msg.data.loaded) { if (unlockWith === "pazzle") { - myWorker.postMessage({ wallet, pazzle, pin_code }); + myWorker.postMessage({ wallet, pazzle, pin_code, device_name }); } else { - myWorker.postMessage({ wallet, mnemonic_words, pin_code }); + myWorker.postMessage({ + wallet, + mnemonic_words, + pin_code, + device_name, + }); } //console.log("postMessage"); } else if (msg.data.success) { @@ -233,6 +246,7 @@ wallet: msg.data.success, id: msg.data.success.V0.wallet_id, trusted, + device_name, }); } else { console.error(msg.data.error); @@ -387,13 +401,26 @@ {$t("pages.login.trust_device_yes")} - {/if} -
+
+ + {#if for_import} + + + {/if} + {#if !loaded} {$t("pages.login.loading_pazzle")}... @@ -422,7 +449,7 @@ on:keypress={start_with_mnemonic} role="link" tabindex="0" - class="mt-1 text-lg px-5 py-2.5 text-center inline-flex items-center mb-2 underline cursor-pointer" + class="mt-1 text-lg px-5 py-2.5 text-center inline-flex items-center mb-10 underline cursor-pointer" > {$t("pages.login.open_with_mnemonic")} @@ -649,7 +676,7 @@ /> - {$t("errors." + error)} + {display_error(error)}
diff --git a/ng-app/src/locales/en.json b/ng-app/src/locales/en.json index 5fd53c4..b691f33 100644 --- a/ng-app/src/locales/en.json +++ b/ng-app/src/locales/en.json @@ -100,6 +100,8 @@ }, "trust_device_allow_cookies": "By selecting this option, you agree to saving some cookies on your browser.", "trust_device_yes": "Yes, save my wallet on this device", + "device_name_label": "Name of this device. You can edit it now", + "device_name_placeholder": "Enter name of this device", "loading_pazzle": "Loading Pazzle", "open_with_pazzle": "Open With Pazzle", "login_cancel": "Cancel Login", @@ -207,6 +209,7 @@ "trust": "Do you trust this device?", "trust_description": "If you do, if this device is yours, or it is used by a 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.", "trust_toggle": "Save my wallet on this device?", + "device_name_description": "To see which devices you are connected with, every device should have a name (e.g. Bob's laptop). Please enter it here.", "cloud": "Keep a copy in the cloud?", "cloud_description": "Are you afraid that you will loose the file containing your wallet? If this would happen, your wallet would be lost forever, together with all your documents. We can keep an encrypted copy of your wallet in our cloud. Only you will be able to download it with a special link. You would have to keep this link safely though. By selecting this option, you agree to the", "cloud_toggle": "Save my wallet in the cloud?", @@ -331,18 +334,19 @@ "VerifierError": "Error during verification.", "SiteNotFoundOnBroker": "The site cannot be found on the broker", "BrokerConfigErrorStr": "{error}", - "BrokerConfigError": "Error in the broker configuration", + "BrokerConfigError": "Error in the broker configuration.", "MalformedEvent": "The event has an invalid format.", "InvalidPayload": "The payload is invalid.", "WrongUploadId": "The upload ID is incorrect.", "FileError": "Error with file.", "InternalError": "Internal Error", "OxiGraphError": "Error in OxiGraph database.", - "ConfigError": "Error in configuration", + "ConfigError": "Error in configuration.", "LocalBrokerIsHeadless": "The local broker is headless.", "LocalBrokerIsNotHeadless": "The local broker is not headless.", - "InvalidNuri": "Invalid NextGraph URI", - "InvalidTarget": "Cannot resolve target" + "InvalidNuri": "Invalid NextGraph URI.", + "InvalidTarget": "Cannot resolve target.", + "ExportWalletTimeOut": "Export of wallet has expired." }, "connectivity": { "stopped": "Stopped", diff --git a/ng-app/src/routes/AccountInfo.svelte b/ng-app/src/routes/AccountInfo.svelte index 2423ab8..37061a0 100644 --- a/ng-app/src/routes/AccountInfo.svelte +++ b/ng-app/src/routes/AccountInfo.svelte @@ -22,7 +22,7 @@ import { onMount, tick } from "svelte"; import { Sidebar, SidebarGroup, SidebarWrapper } from "flowbite-svelte"; import { t } from "svelte-i18n"; - import { active_session, active_wallet, connections } from "../store"; + import { active_session, active_wallet, connections, display_error } from "../store"; import { default as ng } from "../api"; import DeviceIcon from "../lib/components/DeviceIcon.svelte"; @@ -343,7 +343,7 @@ {:else}

{@html $t("errors.error_occurred", { - values: { message: $t("errors." + error) }, + values: { message: display_error(error) }, })}

diff --git a/ng-app/src/routes/ScanQR.svelte b/ng-app/src/routes/ScanQR.svelte new file mode 100644 index 0000000..7d8f393 --- /dev/null +++ b/ng-app/src/routes/ScanQR.svelte @@ -0,0 +1,51 @@ + + + + + + +
+ + Scanning the QRcode +
\ No newline at end of file diff --git a/ng-app/src/routes/User.svelte b/ng-app/src/routes/User.svelte index f10e73e..678ff1c 100644 --- a/ng-app/src/routes/User.svelte +++ b/ng-app/src/routes/User.svelte @@ -365,7 +365,7 @@ {:else}

{@html $t("errors.error_occurred", { - values: { message: $t("errors." + error) }, + values: { message: display_error(error) }, })}

diff --git a/ng-app/src/routes/UserRegistered.svelte b/ng-app/src/routes/UserRegistered.svelte index 522a94b..235f6db 100644 --- a/ng-app/src/routes/UserRegistered.svelte +++ b/ng-app/src/routes/UserRegistered.svelte @@ -19,6 +19,7 @@ import { onMount, tick } from "svelte"; import { default as ng } from "../api"; + import { display_error } from "../store"; const param = new URLSearchParams($querystring); @@ -79,7 +80,7 @@ {:else}

{@html $t("errors.error_occurred", { - values: { message: $t("errors." + error) }, + values: { message: display_error(error) }, })}

diff --git a/ng-app/src/routes/WalletCreate.svelte b/ng-app/src/routes/WalletCreate.svelte index 0384494..7b0a28f 100644 --- a/ng-app/src/routes/WalletCreate.svelte +++ b/ng-app/src/routes/WalletCreate.svelte @@ -44,7 +44,7 @@ } from "../wallet_emojis"; import { onMount, onDestroy, tick } from "svelte"; - import { wallets, set_active_session, has_wallets } from "../store"; + import { wallets, set_active_session, has_wallets, display_error } from "../store"; import Spinner from "../lib/components/Spinner.svelte"; const param = new URLSearchParams($querystring); @@ -139,6 +139,8 @@ /** The emojis for the newly created pazzle. */ let pazzle_emojis = []; let confirm_modal_open = false; + let device_name; + function scrollToTop() { top.scrollIntoView(); } @@ -221,6 +223,8 @@ } async function save_security() { + + device_name = await ng.get_device_name(); options = { trusted: true, cloud: false, @@ -254,6 +258,7 @@ core_bootstrap: invitation.V0.bootstrap, core_registration, additional_bootstrap, + //TODO: device_name, }; //console.log("do wallet with params", params); try { @@ -316,7 +321,7 @@ unsub_register_accepted = undefined; }; - onDestroy(() => { + onDestroy(async () => { unsub_register(); }); @@ -570,7 +575,7 @@ {:else}

{@html $t("errors.error_occurred", { - values: { message: $t("errors." + registration_error) }, + values: { message: display_error(registration_error) }, })}

@@ -1424,8 +1429,25 @@ "pages.wallet_create.save_wallet_options.trust_toggle" )} -

+ + {#if options.trusted} +
+

+ {@html $t( + "pages.wallet_create.save_wallet_options.device_name_description" + )} +

+ + {/if}

{@html $t( @@ -1707,7 +1729,7 @@ /> - {$t("errors." + error)} + {display_error(error)}

{:else} - pages.wallet_info.gen_qr_alt + /--> {/if}
{/if} @@ -607,7 +620,7 @@ {:else}

{@html $t("errors.error_occurred", { - values: { message: $t("errors." + error) }, + values: { message: display_error(error) }, })}

diff --git a/ng-app/src/routes/WalletLogin.svelte b/ng-app/src/routes/WalletLogin.svelte index 5af265d..329135e 100644 --- a/ng-app/src/routes/WalletLogin.svelte +++ b/ng-app/src/routes/WalletLogin.svelte @@ -32,6 +32,8 @@ active_session, set_active_session, has_wallets, + wallet_import_qrcode, + display_error, } from "../store"; import { QrCode } from "svelte-heros-v2"; @@ -57,6 +59,7 @@ } onMount(async () => { + step = "open"; wallets_unsub = wallets.subscribe((value) => { wallet = selected && $wallets[selected]?.wallet; @@ -93,6 +96,49 @@ } } }); + if ($wallet_import_qrcode) { + + let code = $wallet_import_qrcode; + wallet_import_qrcode.set(""); + try { + let temp_wallet = await ng.wallet_import_from_code(code); + // TODO: in 2 steps. first display: wallet was retrieved successfully. + // then when user clicks on "continue to login", do: + wallet = temp_wallet; + importing = true; + } catch(e) { + error = e; + } + } + + // example of getting wallet from TextCode + // async () => { + // try { + // let temp_wallet = await ng.wallet_import_from_code("AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA"); + // // TODO: in 2 steps. first display: wallet was retrieved successfully. + // // then when user clicks on "continue to login", do: + // wallet = temp_wallet; + // importing = true; + // } catch (e) { + // error = e; + // } + // } + + // example of rendezvous for desktop and web without cam (please remove it) + // qrcode = await ng.wallet_import_rendezvous(300); + // try { + // let temp_wallet = await ng.wallet_import_from_code(qrcode[1]); + // // TODO: in 2 steps. first display: wallet was retrieved successfully. + // // then when user clicks on "continue to login", do: + // wallet = temp_wallet; + // importing = true; + // } catch (e) { + // error = e; + // } + // TODO: display with QRcode with : + // {#if qrcode} + // {@html qrcode[0]} + // {/if} }); function loggedin() { step = "loggedin"; @@ -215,7 +261,7 @@

{@html $t("errors.error_occurred", { - values: { message: $t("errors." + error) }, + values: { message: display_error(error) }, })}

+ - +