Merge branch 'develop' into fix/wallet-store-ids-in-state
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
### Added
|
||||
|
||||
- wallet: require password to switch accounts
|
||||
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
|
||||
- wallet: added support for multiple accounts ([#1265])
|
||||
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
|
||||
- mixnet-contract: Replace all naked `-` with `saturating_sub`.
|
||||
|
||||
Generated
+94
-6
@@ -75,9 +75,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.55"
|
||||
version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
|
||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
@@ -87,7 +87,18 @@ checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash",
|
||||
"password-hash 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -619,6 +630,45 @@ dependencies = [
|
||||
"generic-array 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"strsim 0.10.0",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
@@ -2950,7 +3000,7 @@ name = "nym_wallet"
|
||||
version = "1.0.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
"argon2 0.3.4",
|
||||
"base64",
|
||||
"bip39",
|
||||
"cfg-if",
|
||||
@@ -3108,6 +3158,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.20.0"
|
||||
@@ -3184,6 +3240,17 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.6"
|
||||
@@ -4262,9 +4329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.79"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
@@ -5051,6 +5118,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
@@ -5500,6 +5573,21 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wallet-recovery-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"argon2 0.4.0",
|
||||
"base64",
|
||||
"bip39",
|
||||
"clap",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
[workspace]
|
||||
members = ["src-tauri"]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"src-tauri",
|
||||
"wallet-recovery-cli",
|
||||
]
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "wallet-recovery-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aes-gcm = "0.9"
|
||||
anyhow = "1.0"
|
||||
argon2 = "0.4"
|
||||
base64 = "0.13"
|
||||
bip39 = "1.0"
|
||||
clap = { version = "3.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
serde_json = "1.0.0"
|
||||
@@ -0,0 +1,186 @@
|
||||
//! Wallet CLI recovery tool
|
||||
//!
|
||||
//! Can decrypted wallet files saved by nym-wallet.
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use aes_gcm::{aead::Aead, Aes256Gcm, Key, NewAead, Nonce};
|
||||
use anyhow::{anyhow, Result};
|
||||
use argon2::{Algorithm, Argon2, Params, Version};
|
||||
use clap::Parser;
|
||||
use serde_json::Value;
|
||||
|
||||
// Mostly defaults
|
||||
const MEMORY_COST: u32 = 16 * 1024; // 4096 is default
|
||||
const ITERATIONS: u32 = 3; // This appears to be the default
|
||||
const PARALLELISM: u32 = 1; // 1 thread. Default
|
||||
const OUTPUT_LENGTH: usize = 32; // Default
|
||||
|
||||
/// Simple utility to decrypt wallet file used by `nym-wallet` to store encrypted mnemonics.
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, about)]
|
||||
struct Args {
|
||||
/// Password used to attempt to decrypt the logins found in the file. The option can be
|
||||
/// provided multiple times for files that require multiple passwords.
|
||||
#[clap(short, long, min_values(1), required = true)]
|
||||
password: Vec<String>,
|
||||
|
||||
/// Path to the wallet file that will be decrypted.
|
||||
#[clap(short, long)]
|
||||
file: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
setup_logging();
|
||||
let args = Args::parse();
|
||||
let file = File::open(args.file)?;
|
||||
decrypt_file(file, &args.password)
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder.init();
|
||||
}
|
||||
|
||||
fn decrypt_file(file: File, passwords: &[String]) -> Result<()> {
|
||||
let json_file: Value = serde_json::from_reader(file)?;
|
||||
|
||||
// The logins are stored under the more generic name "accounts"
|
||||
let logins = json_file["accounts"]
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!("No accounts found in file!"))?;
|
||||
|
||||
println!("The file contains the logins:");
|
||||
for login in logins {
|
||||
let id = &login["id"];
|
||||
println!(" - id: {id}");
|
||||
}
|
||||
|
||||
println!("We have {} password(s) to try", passwords.len());
|
||||
let mut successes = 0;
|
||||
for login in logins {
|
||||
match decrypt_login(login, passwords) {
|
||||
Ok(is_success) if is_success => successes += 1,
|
||||
Ok(_) => println!("None of the provided passwords succeeded"),
|
||||
Err(err) => println!("Failed: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nManaged to decrypt {} out of {} found logins, using the {} provided password(s)",
|
||||
successes,
|
||||
logins.len(),
|
||||
passwords.len(),
|
||||
);
|
||||
if successes != logins.len() {
|
||||
return Err(anyhow!("Failed to decrypt all logins"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_login(login: &Value, passwords: &[String]) -> Result<bool> {
|
||||
let id = &login["id"];
|
||||
println!("\nAttempting to parse login entry: {id}");
|
||||
|
||||
let (ciphertext, iv, salt) = get_login_entry(login)?;
|
||||
let (ciphertext, iv, salt) = base64_decode(ciphertext, iv, salt)?;
|
||||
|
||||
for (i, password) in passwords.iter().enumerate() {
|
||||
print!("Trying to decrypt with password {i}:");
|
||||
if let Ok((mnemonic, hd_path)) = decrypt_password(password, &ciphertext, &iv, &salt) {
|
||||
println!(" success!");
|
||||
println!(" mnemonic: {mnemonic}");
|
||||
println!(" hd_path: {hd_path}");
|
||||
return Ok(true);
|
||||
}
|
||||
println!(" failed")
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn get_login_entry(login: &Value) -> Result<(&str, &str, &str)> {
|
||||
let account = &login["account"]
|
||||
.as_object()
|
||||
.ok_or_else(|| anyhow!("No account entry in json"))?;
|
||||
let ciphertext = account["ciphertext"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("No ciphertext entry"))?;
|
||||
let iv = account["iv"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("No IV entry"))?;
|
||||
let salt = account["salt"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("No salt entry"))?;
|
||||
Ok((ciphertext, iv, salt))
|
||||
}
|
||||
|
||||
fn base64_decode(ciphertext: &str, iv: &str, salt: &str) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
|
||||
let ciphertext = base64::decode(&ciphertext)
|
||||
.map_err(|err| anyhow!("Unable to base64 decode ciphertext: {}", err))?;
|
||||
let iv = base64::decode(iv).map_err(|err| anyhow!("Unable to base64 decode iv: {}", err))?;
|
||||
let salt =
|
||||
base64::decode(salt).map_err(|err| anyhow!("Unable to base64 decode salt: {}", err))?;
|
||||
Ok((ciphertext, iv, salt))
|
||||
}
|
||||
|
||||
fn decrypt_password(
|
||||
password: &str,
|
||||
ciphertext: &[u8],
|
||||
iv: &[u8],
|
||||
salt: &[u8],
|
||||
) -> Result<(String, String)> {
|
||||
let params = Params::new(MEMORY_COST, ITERATIONS, PARALLELISM, Some(OUTPUT_LENGTH)).unwrap();
|
||||
|
||||
// Argon2id is default, V0x13 is default
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
let mut key = Key::default();
|
||||
argon2
|
||||
.hash_password_into(password.as_bytes(), salt, &mut key)
|
||||
.map_err(|err| anyhow!("Unable to hash password: {}", err))?;
|
||||
|
||||
// Create the Cipher
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
// Decrypt using the nonce, which we get from the IV
|
||||
let nonce = Nonce::from_slice(iv);
|
||||
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext.as_ref())
|
||||
.map_err(|_| anyhow!("Unable to decrypt"))?;
|
||||
|
||||
let plaintext = String::from_utf8(plaintext)?;
|
||||
|
||||
let json_data: Value = serde_json::from_str(&plaintext)?;
|
||||
|
||||
let mnemonic = json_data["mnemonic"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("No mnemonic entry after decrypting"))?;
|
||||
let hd_path = json_data["hd_path"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("No hd_path entry after decrypting"))?;
|
||||
Ok((mnemonic.to_string(), hd_path.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn decrypt_saved_file() {
|
||||
const SAVED_WALLET: &str = "../src-tauri/src/wallet_storage/test-data/saved-wallet.json";
|
||||
let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET);
|
||||
let file = File::open(wallet_file).unwrap();
|
||||
let passwords = vec!["password".to_string()];
|
||||
assert!(decrypt_file(file, &passwords).is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user