Merge branch 'develop' into fix/wallet-store-ids-in-state

This commit is contained in:
Jon Häggblad
2022-05-17 07:22:30 +02:00
committed by GitHub
6 changed files with 305 additions and 8 deletions
+1
View File
@@ -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`.
+94 -6
View File
@@ -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"
+6 -1
View File
@@ -1,2 +1,7 @@
[workspace]
members = ["src-tauri"]
resolver = "2"
members = [
"src-tauri",
"wallet-recovery-cli",
]
@@ -18,4 +18,4 @@
}
}
]
}
}
+17
View File
@@ -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"
+186
View File
@@ -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());
}
}