diff --git a/Cargo.lock b/Cargo.lock index 04b14657aa..8984ef80c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -687,6 +687,7 @@ dependencies = [ "humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", "tar 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", @@ -954,6 +955,7 @@ dependencies = [ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "prettytable-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1973,6 +1975,16 @@ dependencies = [ "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rpassword" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.9" @@ -3009,6 +3021,7 @@ dependencies = [ "checksum reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ab52e462d1e15891441aeefadff68bdea005174328ce3da0a314f2ad313ec837" "checksum ring 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed733c36010c3d4d4718588f16a6c06a670b01c0047029ae81c3ca0acd81ff5" "checksum ripemd160 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "482aa56cc68aaeccdaaff1cc5a72c247da8bbad3beb174ca5741f274c22883fb" +"checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451" "checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" diff --git a/Cargo.toml b/Cargo.toml index 52f32867a7..43f18fd674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ serde = "1" serde_json = "1" log = "0.4" term = "0.5" +rpassword = "2.0.0" grin_api = { path = "./api", version = "0.4.1" } grin_config = { path = "./config", version = "0.4.1" } diff --git a/doc/wallet/usage.md b/doc/wallet/usage.md index 37ae6d5395..796f01807c 100644 --- a/doc/wallet/usage.md +++ b/doc/wallet/usage.md @@ -18,8 +18,9 @@ When creating a new wallet, the file structure should be: * `grin-wallet.toml` contains configuration information for the wallet. You can modify values within to change ports, the address of your grin node, or logging values. -* `wallet_data/wallet.seed` is your master seed file. You must back this file up somewhere in order to - be able to recover or restore your wallet (along with its password, if given). +* `wallet_data/wallet.seed` is your master seed file. Its contents are encrypted with your password (required). + You should back this file up somewhere in order to be able to recover or restore your wallet. Your seed file + can also be recovered using a seed phrase if you lose this file or forget your password. ### Data Directory @@ -59,12 +60,12 @@ and the results verified against the latest chain information. ##### Password -All keys generated by your wallet are combinations of the master seed + a password. If no password is provided, it's assumed this -password is blank. If you do provide a password, all operations will use the seed+password and you will need the password to view or -spend any generated outputs. The password is specified with `-p` +Your wallet.seed file, which contains your wallet's unique master seed, is encrypted with your password. Your password is specified +at wallet creation time, and must be provided for any wallet operation. You will be prompted for your password when required, but +you can also specify it on the command line by providing the `-p`argument. ```sh -[host]$ grin wallet -p mypassword info +[host]$ grin wallet -p mypass info ``` ## Basic Wallet Commands @@ -74,22 +75,62 @@ spend any generated outputs. The password is specified with `-p` ### init -Before using a wallet a new `grin-wallet.toml` configuration file, seed file `wallet.seed` and storage database need +Before using a wallet a new `grin-wallet.toml` configuration file, master seed contained in `wallet.seed` and storage database need to be generated via the init command as follows: -By default this will place your wallet files into `~/.grin`. It is VERY IMPORTANT that you back up the `~/.grin/wallet_data/wallet.seed` -file somewhere safe and private, and ensure you somehow remember the password used to generate the wallet. +```sh +[host]$ grin wallet init +``` -Alternatively, if you'd like to run a wallet in a directory other than the default, you can run: +You will be prompted to enter a password for the new wallet. By default, your wallet files will be placed into `~/.grin`. Alternatively, +if you'd like to run a wallet in a directory other than the default, you can run: ```sh -[host]$ grin wallet [-p password] init -h +[host]$ grin wallet -p mypass init -h ``` This will create a `grin-wallet.toml` file in the current directory configured to use the data files in the current directory, as well as all needed data files. When running any `grin wallet` command, grin will check the current directory to see if a `grin-wallet.toml` file exists. If not it will use the default in `~/.grin` +The init command will also print a 24 (or 12) word recovery phrase, which you should write down and store in a non-digital format. This +phrase can be used to re-create your master seed file if it gets lost or corrupted, or you forget the wallet password. If you'd prefer +to use a 12-word recovery phrase, you can also pass in the `--short_wordlist` or `-s` parameter. + +It is also highly recommended that you back up the `~/.grin/wallet_data/wallet.seed` file somewhere safe and private, +and ensure you somehow remember the password used to encrypt the wallet seed file. + +### recover + +The `recover` command is used to regenerate your wallet seed file from your recovery phrase. Note that this operation only +restores your seed file, not the outputs stored in your wallet. If, for instance, you forget your wallet password, you can +delete the `wallet_data/wallet.seed` file from your wallet data directory, run `grin wallet recover`, and (provided you used +the correct recovery phrase,) your wallet contents should again be usable. + +To recover your wallet seed, delete (or backup) the wallet's `wallet_data/wallet.seed` file, then run: + +```sh +[host]$ grin wallet recover -p "[12 or 24 word passphrase separated by spaces" + +e.g: + +[host]$ grin wallet recover -p "shiver alarm excuse turtle absorb surface lunch virtual want remind hard slow vacuum park silver asthma engage library battle jelly buffalo female inquiry wire" +``` + +If you're restoring a wallet from scratch, you'll then need to use the `grin wallet restore` command to scan the chain +for your outputs and restore them. See the `grin wallet restore` command below for details of the entire process. + +You can also view your recovery phrase with your password by running the recover command without any arguments, e.g: + + +```sh +[host]$ grin wallet recover +Password: +Your recovery phrase is: +shiver alarm excuse turtle absorb surface lunch virtual want remind hard slow vacuum park silver asthma engage library battle jelly buffalo female inquiry wire +Please back-up these words in a non-digital format. +``` + ### account To create a new account, use the 'grin wallet account' command with the argument '-c', e.g.: @@ -296,7 +337,7 @@ Wallet Outputs - Block Height: 49 #### cancel -Everything before Step 6 in the send phase above happens completely locally in the wallets' data storage and separately from the chain. +Everything before Step 6 in the send phase above happens completely locally in the wallets' data storage and separately from the chain. Since it's very easy for a sender, (through error or malice,) to fail to post a transaction to the chain, it's very possible for the contents of a wallet to become locked, with all outputs unable to be selected because the wallet is waiting for a transaction that will never hit the chain to complete. For example, in the output from `grin wallet txs -i 6` above, the transaction is showing as `confirmed == false` @@ -342,7 +383,7 @@ This will create a file called tx_3.json containing your raw transaction data. N ##### restore -If for some reason the wallet cancel commands above don't work, or you need to restore from a backed up `wallet.seed` file and password, you can perform a full wallet restore. +If for some reason the wallet cancel commands above don't work, you need to restore from a backed up `wallet.seed` file and password, or have recovered the wallet seed from a recovery phrase, you can perform a full wallet restore. To do this, generate an empty wallet somewhere with: @@ -357,18 +398,17 @@ Delete the newly generated wallet data directory and seed file: [host@new_wallet_dir]# rm wallet_data/wallet.seed ``` -Then copy your backed up `wallet.seed` file into the new `wallet_data` directory, ensuring it's called `wallet.seed` - -```sh -[host@new_wallet_dir]# cp OLD_WALLET.seed wallet_data/wallet.seed -``` +If you need to recover your wallet seed from a recovery phrase, use the `grin wallet recover -p "[recovery phrase]" command +as outlined above. Otherwise, if you're restoring from a backed-up seed file, simply copy your backed up `wallet.seed` file +into the new `wallet_data` directory, ensuring it's called `wallet.seed` -Then ensure that you're running a grin node, and makes sure nothing is attempting to mine into your wallet. Then, in the -wallet directory: +Ensure the Grin node with which your wallet is talking is running, and make sure nothing is attempting to mine into your wallet. +Then, in the wallet directory: ```sh grin wallet restore ``` Note this operation can potentially take a long time. Once it's done, your wallet outputs should be restored, and you can -transact with your restored wallet as before the backup. +transact with your restored wallet as before the backup. Your transaction log history is not restored, and will simply +contain incoming transactions for each output found. diff --git a/keychain/src/keychain.rs b/keychain/src/keychain.rs index 33f961d01b..908f86817e 100644 --- a/keychain/src/keychain.rs +++ b/keychain/src/keychain.rs @@ -43,6 +43,16 @@ impl Keychain for ExtKeychain { Ok(keychain) } + fn from_mnemonic(word_list: &str, extension_word: &str) -> Result { + let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit); + let master = ExtendedPrivKey::from_mnemonic(&secp, word_list, extension_word)?; + let keychain = ExtKeychain { + secp: secp, + master: master, + }; + Ok(keychain) + } + /// For testing - probably not a good idea to use outside of tests. fn from_random_seed() -> Result { let seed: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect(); @@ -85,8 +95,7 @@ impl Keychain for ExtKeychain { } else { None } - }) - .collect(); + }).collect(); let mut neg_keys: Vec = blind_sum .negative_key_ids @@ -98,8 +107,7 @@ impl Keychain for ExtKeychain { } else { None } - }) - .collect(); + }).collect(); pos_keys.extend( &blind_sum @@ -220,8 +228,7 @@ mod test { &BlindSum::new() .add_blinding_factor(BlindingFactor::from_secret_key(skey1)) .add_blinding_factor(BlindingFactor::from_secret_key(skey2)) - ) - .unwrap(), + ).unwrap(), BlindingFactor::from_secret_key(skey3), ); } diff --git a/keychain/src/types.rs b/keychain/src/types.rs index 4fbfa72bd7..afeefdc4cf 100644 --- a/keychain/src/types.rs +++ b/keychain/src/types.rs @@ -432,6 +432,7 @@ impl ExtKeychainPath { pub trait Keychain: Sync + Send + Clone { fn from_seed(seed: &[u8]) -> Result; + fn from_mnemonic(word_list: &str, extension_word: &str) -> Result; fn from_random_seed() -> Result; fn root_key_id() -> Identifier; fn derive_key_id(depth: u8, d1: u32, d2: u32, d3: u32, d4: u32) -> Identifier; diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index 470f0097d3..b01e6eb8de 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -263,7 +263,7 @@ impl LocalServerContainer { self.wallet_config.data_file_dir = self.working_dir.clone(); let _ = fs::create_dir_all(self.wallet_config.clone().data_file_dir); - let r = wallet::WalletSeed::init_file(&self.wallet_config); + let r = wallet::WalletSeed::init_file(&self.wallet_config, 32, ""); let client_n = HTTPNodeClient::new(&self.wallet_config.check_node_api_http_addr, None); @@ -295,9 +295,9 @@ impl LocalServerContainer { pub fn get_wallet_seed(config: &WalletConfig) -> wallet::WalletSeed { let _ = fs::create_dir_all(config.clone().data_file_dir); - wallet::WalletSeed::init_file(config).unwrap(); + wallet::WalletSeed::init_file(config, 32, "").unwrap(); let wallet_seed = - wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file."); + wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file."); wallet_seed } @@ -306,7 +306,7 @@ impl LocalServerContainer { wallet_seed: &wallet::WalletSeed, ) -> wallet::WalletInfo { let keychain: keychain::ExtKeychain = wallet_seed - .derive_keychain("") + .derive_keychain() .expect("Failed to derive keychain from seed file and passphrase."); let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None); let mut wallet = LMDBBackend::new(config.clone(), "", client_n) @@ -329,10 +329,10 @@ impl LocalServerContainer { .expect("Could not parse amount as a number with optional decimal point."); let wallet_seed = - wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file."); + wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file."); let keychain: keychain::ExtKeychain = wallet_seed - .derive_keychain("") + .derive_keychain() .expect("Failed to derive keychain from seed file and passphrase."); let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None); diff --git a/servers/tests/simulnet.rs b/servers/tests/simulnet.rs index 785a16a939..849875c050 100644 --- a/servers/tests/simulnet.rs +++ b/servers/tests/simulnet.rs @@ -883,7 +883,7 @@ pub fn create_wallet( ) -> Arc>> { let mut wallet_config = WalletConfig::default(); wallet_config.data_file_dir = String::from(dir); - let _ = wallet::WalletSeed::init_file(&wallet_config); + let _ = wallet::WalletSeed::init_file(&wallet_config, 32, ""); let mut wallet: LMDBBackend = LMDBBackend::new(wallet_config.clone(), "", client_n).unwrap_or_else(|e| { panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config) diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 9696ecee04..b076a6008c 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -13,6 +13,7 @@ // limitations under the License. use clap::ArgMatches; +use rpassword; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::thread; @@ -31,9 +32,10 @@ use keychain; use servers::start_webwallet_server; use util::file::get_first_line; -pub fn _init_wallet_seed(wallet_config: WalletConfig) { - if let Err(_) = WalletSeed::from_file(&wallet_config) { - WalletSeed::init_file(&wallet_config).expect("Failed to create wallet seed file."); +pub fn _init_wallet_seed(wallet_config: WalletConfig, password: &str) { + if let Err(_) = WalletSeed::from_file(&wallet_config, password) { + WalletSeed::init_file(&wallet_config, 32, password) + .expect("Failed to create wallet seed file."); }; } @@ -48,6 +50,33 @@ pub fn seed_exists(wallet_config: WalletConfig) -> bool { } } +pub fn prompt_password(args: &ArgMatches) -> String { + match args.value_of("pass") { + None => { + println!("Temporary note:"); + println!( + "If this is your first time running your wallet since BIP32 (word lists) \ + were implemented, your seed will be converted to \ + the new format. Please ensure the provided password is correct." + ); + println!("If this goes wrong, your old 'wallet.seed' file has been saved as 'wallet.seed.bak' \ + Rename this file to back to `wallet.seed` and try again"); + rpassword::prompt_password_stdout("Password: ").unwrap() + } + Some(p) => p.to_owned(), + } +} + +pub fn prompt_password_confirm() -> String { + let first = rpassword::prompt_password_stdout("Password: ").unwrap(); + let second = rpassword::prompt_password_stdout("Confirm Password: ").unwrap(); + if first != second { + println!("Passwords do not match"); + std::process::exit(0); + } + first +} + pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i32 { // just get defaults from the global config let mut wallet_config = config.members.unwrap().wallet; @@ -74,15 +103,22 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i } let node_api_secret = get_first_line(wallet_config.node_api_secret_path.clone()); - // Derive the keychain based on seed from seed file and specified passphrase. + // Decrypt the seed from the seed file and derive the keychain. // Generate the initial wallet seed if we are running "wallet init". - if let ("init", Some(_)) = wallet_args.subcommand() { - WalletSeed::init_file(&wallet_config).expect("Failed to init wallet seed file."); + if let ("init", Some(r)) = wallet_args.subcommand() { + let list_length = match r.is_present("short_wordlist") { + false => 32, + true => 16, + }; + println!("Please enter a password for your new wallet"); + let passphrase = prompt_password_confirm(); + WalletSeed::init_file(&wallet_config, list_length, &passphrase) + .expect("Failed to init wallet seed file."); info!("Wallet seed file created"); let client_n = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, node_api_secret); let _: LMDBBackend = - LMDBBackend::new(wallet_config.clone(), "", client_n).unwrap_or_else(|e| { + LMDBBackend::new(wallet_config.clone(), &passphrase, client_n).unwrap_or_else(|e| { panic!( "Error creating DB for wallet: {} Config: {:?}", e, wallet_config @@ -95,13 +131,45 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i return 0; } - let passphrase = match wallet_args.value_of("pass") { - None => { - error!("Failed to read passphrase."); - return 1; + // Recover a seed from a recovery phrase + if let ("recover", Some(r)) = wallet_args.subcommand() { + if !r.is_present("recovery_phrase") { + // only needed to display phrase + let passphrase = prompt_password(wallet_args); + let seed = match WalletSeed::from_file(&wallet_config, &passphrase) { + Ok(s) => s, + Err(e) => { + println!("Can't open wallet seed file (check password): {}", e); + std::process::exit(0); + } + }; + let _ = seed.show_recovery_phrase(); + std::process::exit(0); } - Some(p) => p, - }; + let word_list = match r.value_of("recovery_phrase") { + Some(w) => w, + None => { + println!("Recovery word phrase must be provided (in quotes)"); + std::process::exit(0); + } + }; + // check word list is okay before asking for password + if WalletSeed::from_mnemonic(word_list).is_err() { + println!("Recovery word phrase is invalid"); + std::process::exit(0); + } + println!("Please provide a new password for the recovered wallet"); + let passphrase = prompt_password_confirm(); + let res = WalletSeed::recover_from_phrase(&wallet_config, word_list, &passphrase); + if let Err(e) = res { + thread::sleep(Duration::from_millis(200)); + error!("Error recovering seed with list '{}' - {}", word_list, e); + return 0; + } + + thread::sleep(Duration::from_millis(200)); + return 0; + } let account = match wallet_args.value_of("account") { None => { @@ -111,6 +179,9 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i Some(p) => p, }; + // all further commands always need a password + let passphrase = prompt_password(wallet_args); + // Handle listener startup commands { let api_secret = get_first_line(wallet_config.api_secret_path.clone()); @@ -146,10 +217,14 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i .listen( params, wallet_config.clone(), - passphrase, + &passphrase, account, node_api_secret.clone(), ).unwrap_or_else(|e| { + if e.kind() == ErrorKind::WalletSeedDecryption { + println!("Error decrypting wallet seed (check provided password)"); + std::process::exit(0); + } panic!( "Error creating wallet listener: {:?} Config: {:?}", e, wallet_config @@ -159,10 +234,19 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i ("owner_api", Some(_api_args)) => { let wallet = instantiate_wallet( wallet_config.clone(), - passphrase, + &passphrase, account, node_api_secret.clone(), - ); + ).unwrap_or_else(|e| { + if e.kind() == grin_wallet::ErrorKind::Encryption { + println!("Error decrypting wallet seed (check provided password)"); + std::process::exit(0); + } + panic!( + "Error creating wallet listener: {:?} Config: {:?}", + e, wallet_config + ); + }); // TLS is disabled because we bind to localhost controller::owner_listener(wallet.clone(), "127.0.0.1:13420", api_secret, None) .unwrap_or_else(|e| { @@ -175,10 +259,19 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i ("web", Some(_api_args)) => { let wallet = instantiate_wallet( wallet_config.clone(), - passphrase, + &passphrase, account, node_api_secret.clone(), - ); + ).unwrap_or_else(|e| { + if e.kind() == grin_wallet::ErrorKind::Encryption { + println!("Error decrypting wallet seed (check provided password)"); + std::process::exit(0); + } + panic!( + "Error creating wallet listener: {:?} Config: {:?}", + e, wallet_config + ); + }); // start owner listener and run static file server start_webwallet_server(); controller::owner_listener(wallet.clone(), "127.0.0.1:13420", api_secret, tls_conf) @@ -195,10 +288,19 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i let wallet = instantiate_wallet( wallet_config.clone(), - passphrase, + &passphrase, account, node_api_secret.clone(), - ); + ).unwrap_or_else(|e| { + if e.kind() == grin_wallet::ErrorKind::Encryption { + println!("Error decrypting wallet seed (check provided password)"); + std::process::exit(0); + } + panic!( + "Error instantiating wallet: {:?} Config: {:?}", + e, wallet_config + ); + }); let res = controller::owner_single_use(wallet.clone(), |api| { match wallet_args.subcommand() { diff --git a/src/bin/grin.rs b/src/bin/grin.rs index cbc39a0829..e1272a8b34 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -25,6 +25,7 @@ extern crate serde; extern crate serde_json; #[macro_use] extern crate log; +extern crate rpassword; extern crate term; extern crate grin_api as api; @@ -153,9 +154,8 @@ fn real_main() -> i32 { .arg(Arg::with_name("pass") .short("p") .long("pass") - .help("Wallet passphrase used to generate the private key seed") - .takes_value(true) - .default_value("")) + .help("Wallet passphrase used to encrypt wallet seed") + .takes_value(true)) .arg(Arg::with_name("account") .short("a") .long("account") @@ -343,10 +343,23 @@ fn real_main() -> i32 { .short("h") .long("here") .help("Create wallet files in the current directory instead of the default ~/.grin directory") + .takes_value(false)) + .arg(Arg::with_name("short_wordlist") + .help("Generate a 12 word recovery phrase/seed instead of default 24.") + .short("s") + .long("short_wordlist") .takes_value(false))) + .subcommand(SubCommand::with_name("recover") + .about("recover (create a new wallet.seed file) from a recovery phrase") + .arg(Arg::with_name("recovery_phrase") + .help("12 or 24 word recovery phrase (encased in quotes).") + .short("p") + .long("recovery_phrase") + .takes_value(true))) + .subcommand(SubCommand::with_name("restore") - .about("Attempt to restore wallet contents from the chain using seed and password. \ + .about("Attempt to restore wallet contents from the chain using seed. \ NOTE: Backup wallet.* and run `wallet listen` before running restore."))) .get_matches(); @@ -381,9 +394,9 @@ fn real_main() -> i32 { panic!("Error loading wallet configuration: {}", e); }); if !cmd::seed_exists(w.members.as_ref().unwrap().wallet.clone()) { - if let ("init", Some(_)) = wallet_args.subcommand() { + if "init" == wallet_args.subcommand().0 || "recover" == wallet_args.subcommand().0 { } else { - println!("Wallet seed file doesn't exist. Run `grin wallet -p [password] init` first"); + println!("Wallet seed file doesn't exist. Run `grin wallet init` first"); exit(1); } } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index cd0d6f1c1c..8571fe0877 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -26,6 +26,7 @@ term = "0.5" tokio = "0.1.7" tokio-core = "0.1" tokio-retry = "0.1" +ring = "0.13" uuid = { version = "0.6", features = ["serde", "v4"] } url = "1.7.0" chrono = { version = "0.4.4", features = ["serde"] } diff --git a/wallet/src/adapters/http.rs b/wallet/src/adapters/http.rs index 08cdb4e6a8..b8efc9574c 100644 --- a/wallet/src/adapters/http.rs +++ b/wallet/src/adapters/http.rs @@ -71,7 +71,8 @@ impl WalletCommAdapter for HTTPWalletCommAdapter { node_api_secret: Option, ) -> Result<(), Error> { let wallet = - instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone()); + instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone()) + .context(ErrorKind::WalletSeedDecryption)?; let listen_addr = params.get("api_listen_addr").unwrap(); let tls_conf = match params.get("certificate") { Some(s) => Some(api::TLSConfig::new( diff --git a/wallet/src/error.rs b/wallet/src/error.rs index b8eb8aeef7..1db13a9c6e 100644 --- a/wallet/src/error.rs +++ b/wallet/src/error.rs @@ -87,6 +87,14 @@ pub enum ErrorKind { #[fail(display = "Wallet seed doesn't exist error")] WalletSeedDoesntExist, + /// Enc/Decryption Error + #[fail(display = "Enc/Decryption error")] + Encryption, + + /// BIP 39 word list + #[fail(display = "BIP39 Mnemonic (word list) Error")] + Mnemonic, + /// Other #[fail(display = "Generic error: {}", _0)] GenericError(String), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index bf2698502b..0cf1d09f30 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -35,6 +35,7 @@ extern crate failure; extern crate failure_derive; extern crate futures; extern crate hyper; +extern crate ring; extern crate tokio; extern crate tokio_core; extern crate tokio_retry; @@ -62,7 +63,7 @@ pub use error::{Error, ErrorKind}; pub use libwallet::types::{BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst}; pub use lmdb_wallet::{wallet_db_exists, LMDBBackend}; pub use node_clients::{create_coinbase, HTTPNodeClient}; -pub use types::{WalletConfig, WalletSeed, SEED_FILE}; +pub use types::{EncryptedWalletSeed, WalletConfig, WalletSeed, SEED_FILE}; use std::sync::Arc; use util::Mutex; @@ -73,20 +74,12 @@ pub fn instantiate_wallet( passphrase: &str, account: &str, node_api_secret: Option, -) -> Arc>> { +) -> Result>>, Error> { + // First test decryption, so we can abort early if we have the wrong password + let _ = WalletSeed::from_file(&wallet_config, passphrase)?; let client_n = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, node_api_secret); - let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, client_n) - .unwrap_or_else(|e| { - panic!( - "Error creating DB wallet: {} Config: {:?}", - e, wallet_config - ); - }); - db_wallet - .set_parent_key_id_by_name(account) - .unwrap_or_else(|e| { - panic!("Error starting wallet: {}", e); - }); + let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, client_n)?; + db_wallet.set_parent_key_id_by_name(account)?; info!("Using LMDB Backend for wallet"); - Arc::new(Mutex::new(db_wallet)) + Ok(Arc::new(Mutex::new(db_wallet))) } diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libwallet/error.rs index 7a84a2a556..1c5456b0b9 100644 --- a/wallet/src/libwallet/error.rs +++ b/wallet/src/libwallet/error.rs @@ -148,6 +148,10 @@ pub enum ErrorKind { #[fail(display = "Wallet seed doesn't exist error")] WalletSeedDoesntExist, + /// Wallet seed doesn't exist + #[fail(display = "Wallet seed decryption error")] + WalletSeedDecryption, + /// Transaction doesn't exist #[fail(display = "Transaction {} doesn't exist", _0)] TransactionDoesntExist(String), diff --git a/wallet/src/lmdb_wallet.rs b/wallet/src/lmdb_wallet.rs index 7d300b817c..1a04daf9a8 100644 --- a/wallet/src/lmdb_wallet.rs +++ b/wallet/src/lmdb_wallet.rs @@ -121,12 +121,10 @@ where { /// Initialise with whatever stored credentials we have fn open_with_credentials(&mut self) -> Result<(), Error> { - let wallet_seed = WalletSeed::from_file(&self.config) + let wallet_seed = WalletSeed::from_file(&self.config, &self.passphrase) .context(ErrorKind::CallbackImpl("Error opening wallet"))?; - let keychain = wallet_seed.derive_keychain(&self.passphrase); + let keychain = wallet_seed.derive_keychain(); self.keychain = Some(keychain.context(ErrorKind::CallbackImpl("Error deriving keychain"))?); - // Just blow up password for now after it's been used - self.passphrase = String::from(""); Ok(()) } diff --git a/wallet/src/types.rs b/wallet/src/types.rs index cb14433844..278ad8c4b1 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::cmp::min; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::Path; @@ -20,11 +19,15 @@ use std::path::MAIN_SEPARATOR; use blake2; use rand::{thread_rng, Rng}; +use serde_json; + +use ring::aead; +use ring::{digest, pbkdf2}; use core::global::ChainTypes; use error::{Error, ErrorKind}; use failure::ResultExt; -use keychain::Keychain; +use keychain::{mnemonic, Keychain}; use util; pub const SEED_FILE: &'static str = "wallet.seed"; @@ -79,16 +82,20 @@ impl WalletConfig { } } -#[derive(Clone, PartialEq)] -pub struct WalletSeed([u8; 32]); +#[derive(Clone, Debug, PartialEq)] +pub struct WalletSeed(Vec); impl WalletSeed { pub fn from_bytes(bytes: &[u8]) -> WalletSeed { - let mut seed = [0; 32]; - for i in 0..min(32, bytes.len()) { - seed[i] = bytes[i]; + WalletSeed(bytes.to_vec()) + } + + pub fn from_mnemonic(word_list: &str) -> Result { + let res = mnemonic::to_entropy(word_list); + match res { + Ok(s) => Ok(WalletSeed::from_bytes(&s)), + Err(_) => Err(ErrorKind::Mnemonic.into()), } - WalletSeed(seed) } pub fn from_hex(hex: &str) -> Result { @@ -101,18 +108,72 @@ impl WalletSeed { util::to_hex(self.0.to_vec()) } - pub fn derive_keychain(&self, password: &str) -> Result { - let seed = blake2::blake2b::blake2b(64, &password.as_bytes(), &self.0); - let result = K::from_seed(seed.as_bytes())?; + pub fn to_mnemonic(&self) -> Result { + let result = mnemonic::from_entropy(&self.0); + match result { + Ok(r) => Ok(r), + Err(_) => Err(ErrorKind::Mnemonic.into()), + } + } + + pub fn derive_keychain_old(old_wallet_seed: [u8; 32], password: &str) -> Vec { + let seed = blake2::blake2b::blake2b(64, password.as_bytes(), &old_wallet_seed); + seed.as_bytes().to_vec() + } + + pub fn derive_keychain(&self) -> Result { + let result = K::from_seed(&self.0)?; Ok(result) } - pub fn init_new() -> WalletSeed { - let seed: [u8; 32] = thread_rng().gen(); + pub fn init_new(seed_length: usize) -> WalletSeed { + let mut seed: Vec = vec![]; + let mut rng = thread_rng(); + for _ in 0..seed_length { + seed.push(rng.gen()); + } WalletSeed(seed) } - pub fn init_file(wallet_config: &WalletConfig) -> Result { + pub fn recover_from_phrase( + wallet_config: &WalletConfig, + word_list: &str, + password: &str, + ) -> Result<(), Error> { + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + if Path::new(seed_file_path).exists() { + error!( + "wallet seed file {} exists. \ + Please backup and delete this file before attempting recovery.", + seed_file_path + ); + return Err(ErrorKind::WalletSeedExists)?; + } + let seed = WalletSeed::from_mnemonic(word_list)?; + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?; + let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; + file.write_all(&enc_seed_json.as_bytes()) + .context(ErrorKind::IO)?; + warn!("Seed created from word list"); + Ok(()) + } + + pub fn show_recovery_phrase(&self) -> Result<(), Error> { + println!("Your recovery phrase is:"); + println!("{}", self.to_mnemonic()?); + println!("Please back-up these words in a non-digital format."); + Ok(()) + } + + pub fn init_file( + wallet_config: &WalletConfig, + seed_length: usize, + password: &str, + ) -> Result { // create directory if it doesn't exist fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; @@ -121,20 +182,24 @@ impl WalletSeed { wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, ); - debug!("Generating wallet seed file at: {}", seed_file_path); + warn!("Generating wallet seed file at: {}", seed_file_path); if Path::new(seed_file_path).exists() { Err(ErrorKind::WalletSeedExists)? } else { - let seed = WalletSeed::init_new(); + let seed = WalletSeed::init_new(seed_length); + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = + serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?; let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; - file.write_all(&seed.to_hex().as_bytes()) + file.write_all(&enc_seed_json.as_bytes()) .context(ErrorKind::IO)?; + seed.show_recovery_phrase()?; Ok(seed) } } - pub fn from_file(wallet_config: &WalletConfig) -> Result { + pub fn from_file(wallet_config: &WalletConfig, password: &str) -> Result { // create directory if it doesn't exist fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; @@ -143,13 +208,44 @@ impl WalletSeed { wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, ); - debug!("Using wallet seed file at: {}", seed_file_path,); + debug!("Using wallet seed file at: {}", seed_file_path); if Path::new(seed_file_path).exists() { let mut file = File::open(seed_file_path).context(ErrorKind::IO)?; let mut buffer = String::new(); file.read_to_string(&mut buffer).context(ErrorKind::IO)?; - let wallet_seed = WalletSeed::from_hex(&buffer.trim())?; + let enc_seed: EncryptedWalletSeed = + match serde_json::from_str(&buffer).context(ErrorKind::Format) { + Ok(s) => s, + Err(_) => { + println!("Attempting to convert old wallet seed file to new format"); + // TODO: remove for mainnet + // try to convert from old format + let mut bak_file = File::create(format!("{}.bak", seed_file_path)) + .context(ErrorKind::IO)?; + let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; + let old_wallet_seed = WalletSeed::from_hex(&buffer.trim())?; + bak_file + .write_all(&old_wallet_seed.to_hex().as_bytes()) + .context(ErrorKind::IO)?; + let mut c_wallet_seed = [0u8; 32]; + c_wallet_seed.copy_from_slice(&old_wallet_seed.0[0..32]); + let converted_wallet_seed = + WalletSeed::derive_keychain_old(c_wallet_seed, password); + let enc_seed = EncryptedWalletSeed::from_seed( + &WalletSeed::from_bytes(&converted_wallet_seed), + password, + )?; + let enc_seed_json = + serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?; + file.write_all(&enc_seed_json.as_bytes()) + .context(ErrorKind::IO)?; + println!("Seed file conversion done"); + println!("Consider moving funds to a newly-created wallet to support recovery phrases"); + enc_seed + } + }; + let wallet_seed = enc_seed.decrypt(password)?; Ok(wallet_seed) } else { error!( @@ -161,3 +257,68 @@ impl WalletSeed { } } } + +/// Encrypted wallet seed, for storing on disk and decrypting +/// with provided password + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct EncryptedWalletSeed { + encrypted_seed: String, + /// Salt, not so useful in single case but include anyhow for situations + /// where someone wants to store many of these + pub salt: String, + /// Nonce + pub nonce: String, +} + +impl EncryptedWalletSeed { + /// Create a new encrypted seed from the given seed + password + pub fn from_seed(seed: &WalletSeed, password: &str) -> Result { + let salt: [u8; 8] = thread_rng().gen(); + let nonce: [u8; 12] = thread_rng().gen(); + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key); + let content = seed.0.to_vec(); + let mut enc_bytes = content.clone(); + let suffix_len = aead::CHACHA20_POLY1305.tag_len(); + for _ in 0..suffix_len { + enc_bytes.push(0); + } + let sealing_key = + aead::SealingKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?; + aead::seal_in_place(&sealing_key, &nonce, &[], &mut enc_bytes, suffix_len) + .context(ErrorKind::Encryption)?; + Ok(EncryptedWalletSeed { + encrypted_seed: util::to_hex(enc_bytes.to_vec()), + salt: util::to_hex(salt.to_vec()), + nonce: util::to_hex(nonce.to_vec()), + }) + } + + /// Decrypt seed + pub fn decrypt(&self, password: &str) -> Result { + let mut encrypted_seed = match util::from_hex(self.encrypted_seed.clone()) { + Ok(s) => s, + Err(_) => return Err(ErrorKind::Encryption)?, + }; + let salt = match util::from_hex(self.salt.clone()) { + Ok(s) => s, + Err(_) => return Err(ErrorKind::Encryption)?, + }; + let nonce = match util::from_hex(self.nonce.clone()) { + Ok(s) => s, + Err(_) => return Err(ErrorKind::Encryption)?, + }; + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key); + + let opening_key = + aead::OpeningKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?; + let decrypted_data = aead::open_in_place(&opening_key, &nonce, &[], 0, &mut encrypted_seed) + .context(ErrorKind::Encryption)?; + + Ok(WalletSeed::from_bytes(&decrypted_data)) + } +} diff --git a/wallet/tests/common/mod.rs b/wallet/tests/common/mod.rs index f82ee79236..271f31482a 100644 --- a/wallet/tests/common/mod.rs +++ b/wallet/tests/common/mod.rs @@ -163,7 +163,7 @@ where { let mut wallet_config = WalletConfig::default(); wallet_config.data_file_dir = String::from(dir); - let _ = wallet::WalletSeed::init_file(&wallet_config); + let _ = wallet::WalletSeed::init_file(&wallet_config, 32, ""); let mut wallet = LMDBBackend::new(wallet_config.clone(), "", n_client) .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)); wallet.open_with_credentials().unwrap_or_else(|e| { diff --git a/wallet/tests/file.rs b/wallet/tests/file.rs index 7d16d7bc84..5b65aa77ac 100644 --- a/wallet/tests/file.rs +++ b/wallet/tests/file.rs @@ -114,7 +114,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { //"mining", //"listener", )?; - /// output tx file + // output tx file let file_adapter = FileWalletCommAdapter::new(); file_adapter.send_tx_async(&send_file, &mut slate)?; api.tx_lock_outputs(&slate, lock_fn)?; diff --git a/wallet/tests/libwallet.rs b/wallet/tests/libwallet.rs index c636c7d9cf..fbea595e1e 100644 --- a/wallet/tests/libwallet.rs +++ b/wallet/tests/libwallet.rs @@ -26,6 +26,7 @@ use util::secp; use util::secp::key::{PublicKey, SecretKey}; use wallet::libtx::{aggsig, proof}; use wallet::libwallet::types::Context; +use wallet::{EncryptedWalletSeed, WalletSeed}; use rand::thread_rng; @@ -481,3 +482,22 @@ fn test_rewind_range_proof() { assert_eq!(proof_info.success, false); assert_eq!(proof_info.value, 0); } + +#[test] +fn wallet_seed_encrypt() { + let password = "passwoid"; + let wallet_seed = WalletSeed::init_new(32); + let mut enc_wallet_seed = EncryptedWalletSeed::from_seed(&wallet_seed, password).unwrap(); + println!("EWS: {:?}", enc_wallet_seed); + let decrypted_wallet_seed = enc_wallet_seed.decrypt(password).unwrap(); + assert_eq!(wallet_seed, decrypted_wallet_seed); + + // Wrong password + let decrypted_wallet_seed = enc_wallet_seed.decrypt(""); + assert!(decrypted_wallet_seed.is_err()); + + // Wrong nonce + enc_wallet_seed.nonce = "wrongnonce".to_owned(); + let decrypted_wallet_seed = enc_wallet_seed.decrypt(password); + assert!(decrypted_wallet_seed.is_err()); +}