diff --git a/api/src/owner.rs b/api/src/owner.rs index 2556c02c..26e785e7 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -1565,6 +1565,130 @@ where let lc = w_lock.lc_provider()?; lc.close_wallet(name) } + + /// Return the BIP39 mnemonic for the given wallet. This function will decrypt + /// the wallet's seed file with the given password, and thus does not need the + /// wallet to be open. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `password`: The password used to encrypt the seed file. + /// + /// # Returns + /// * Ok(BIP-39 mneminc) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone()); + /// + /// let pw = ZeroingString::from("my_password"); + /// let res = api_owner.get_mnemonic(None, pw); + /// + /// if let Ok(mne) = res { + /// // ... + /// } + /// ``` + pub fn get_mnemonic( + &self, + name: Option<&str>, + password: ZeroingString, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.get_mnemonic(name, password) + } + + /// Changes a wallet's password, meaning the old seed file is decrypted with the old password, + /// and a new seed file is created with the same mnemonic and encrypted with the new password. + /// + /// This function temporarily backs up the old seed file until a test-decryption of the new + /// file is confirmed to contain the same seed as the original seed file, at which point the + /// backup is deleted. If this operation fails for an unknown reason, the backup file will still + /// exist in the wallet's data directory encrypted with the old password. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `old`: The password used to encrypt the existing seed file (i.e. old password) + /// * `new`: The password to be used to encrypt the new seed file + /// + /// # Returns + /// * Ok(()) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone()); + /// + /// let old = ZeroingString::from("my_password"); + /// let new = ZeroingString::from("new_password"); + /// let res = api_owner.change_password(None, old, new); + /// + /// if let Ok(mne) = res { + /// // ... + /// } + /// ``` + pub fn change_password( + &self, + name: Option<&str>, + old: ZeroingString, + new: ZeroingString, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.change_password(name, old, new) + } + + /// Deletes a wallet, removing the config file, seed file and all data files. + /// Obviously, use with extreme caution and plenty of user warning + /// + /// Highly recommended that the wallet be explicitly closed first via the `close_wallet` + /// function. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone()); + /// + /// let res = api_owner.delete_wallet(None); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// ``` + + pub fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.delete_wallet(name) + } } #[doc(hidden)] diff --git a/api/src/owner_rpc_s.rs b/api/src/owner_rpc_s.rs index a62b3fa5..627f1fe6 100644 --- a/api/src/owner_rpc_s.rs +++ b/api/src/owner_rpc_s.rs @@ -1587,6 +1587,102 @@ pub trait OwnerRpcS { */ fn close_wallet(&self, name: Option) -> Result<(), ErrorKind>; + + /** + Networked version of [Owner::get_mnemonic](struct.Owner.html#method.get_mnemonic). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_mnemonic", + "params": { + "name": null, + "password": "" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "fat twenty mean degree forget shell check candy immense awful flame next during february bulb bike sun wink theory day kiwi embrace peace lunch" + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + + fn get_mnemonic(&self, name: Option, password: String) -> Result; + + /** + Networked version of [Owner::change_password](struct.Owner.html#method.change_password). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "change_password", + "params": { + "name": null, + "old": "", + "new": "new_password" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + fn change_password( + &self, + name: Option, + old: String, + new: String, + ) -> Result<(), ErrorKind>; + + /** + Networked version of [Owner::delete_wallet](struct.Owner.html#method.delete_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "delete_wallet", + "params": { + "name": null + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + fn delete_wallet(&self, name: Option) -> Result<(), ErrorKind>; } impl<'a, L, C, K> OwnerRpcS for Owner<'a, L, C, K> @@ -1843,4 +1939,27 @@ where let n = name.as_ref().map(|s| s.as_str()); Owner::close_wallet(self, n).map_err(|e| e.kind()) } + + fn get_mnemonic(&self, name: Option, password: String) -> Result { + let n = name.as_ref().map(|s| s.as_str()); + let res = + Owner::get_mnemonic(self, n, ZeroingString::from(password)).map_err(|e| e.kind())?; + Ok(format!("{}", &*res)) + } + + fn change_password( + &self, + name: Option, + old: String, + new: String, + ) -> Result<(), ErrorKind> { + let n = name.as_ref().map(|s| s.as_str()); + Owner::change_password(self, n, ZeroingString::from(old), ZeroingString::from(new)) + .map_err(|e| e.kind()) + } + + fn delete_wallet(&self, name: Option) -> Result<(), ErrorKind> { + let n = name.as_ref().map(|s| s.as_str()); + Owner::delete_wallet(self, n).map_err(|e| e.kind()) + } } diff --git a/impls/src/lifecycle/default.rs b/impls/src/lifecycle/default.rs index bf0a11b8..8e1072e9 100644 --- a/impls/src/lifecycle/default.rs +++ b/impls/src/lifecycle/default.rs @@ -202,8 +202,9 @@ where } Ok(d) => d, }; - let wallet_seed = WalletSeed::from_file(&data_dir_name, password) - .context(ErrorKind::Lifecycle("Error opening wallet".into()))?; + let wallet_seed = WalletSeed::from_file(&data_dir_name, password).context( + ErrorKind::Lifecycle("Error opening wallet (is password correct?)".into()), + )?; let keychain = wallet_seed .derive_keychain(global::is_floonet()) .context(ErrorKind::Lifecycle("Error deriving keychain".into()))?; @@ -270,12 +271,69 @@ where Ok(()) } - fn change_password(&self, _old: String, _new: String) -> Result<(), Error> { - unimplemented!() + fn change_password( + &self, + _name: Option<&str>, + old: ZeroingString, + new: ZeroingString, + ) -> Result<(), Error> { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = data_dir_name.to_str().unwrap(); + // get seed for later check + + let orig_wallet_seed = WalletSeed::from_file(&data_dir_name, old).context( + ErrorKind::Lifecycle("Error opening wallet seed file".into()), + )?; + let orig_mnemonic = orig_wallet_seed + .to_mnemonic() + .context(ErrorKind::Lifecycle("Error recovering mnemonic".into()))?; + + // Back up existing seed, and keep track of filename as we're deleting it + // once the password change is confirmed + let backup_name = WalletSeed::backup_seed(data_dir_name).context(ErrorKind::Lifecycle( + "Error temporarily backing up existing seed".into(), + ))?; + + // Delete seed file + WalletSeed::delete_seed_file(data_dir_name).context(ErrorKind::Lifecycle( + "Unable to delete seed file for password change".into(), + ))?; + + // Init a new file + let _ = WalletSeed::init_file( + data_dir_name, + 0, + Some(ZeroingString::from(orig_mnemonic)), + new.clone(), + ); + info!("Wallet seed file created"); + + let new_wallet_seed = WalletSeed::from_file(&data_dir_name, new).context( + ErrorKind::Lifecycle("Error opening wallet seed file".into()), + )?; + + if orig_wallet_seed != new_wallet_seed { + let msg = format!( + "New and Old wallet seeds are not equal on password change, not removing backups." + ); + return Err(ErrorKind::Lifecycle(msg).into()); + } + // Removin + info!("Password change confirmed, removing old seed file."); + fs::remove_file(backup_name).context(ErrorKind::IO)?; + + Ok(()) } - fn delete_wallet(&self, _name: Option, _password: String) -> Result<(), Error> { - unimplemented!() + fn delete_wallet(&self, _name: Option<&str>) -> Result<(), Error> { + let data_dir_name = PathBuf::from(self.data_dir.clone()); + warn!( + "Removing all wallet data from: {}", + data_dir_name.to_str().unwrap() + ); + fs::remove_dir_all(data_dir_name).context(ErrorKind::IO)?; + Ok(()) } fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error> { diff --git a/impls/src/lifecycle/seed.rs b/impls/src/lifecycle/seed.rs index a88f27ae..8c0cb765 100644 --- a/impls/src/lifecycle/seed.rs +++ b/impls/src/lifecycle/seed.rs @@ -94,7 +94,7 @@ impl WalletSeed { } } - pub fn backup_seed(data_file_dir: &str) -> Result<(), Error> { + pub fn backup_seed(data_file_dir: &str) -> Result { let seed_file_name = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,); let mut path = Path::new(seed_file_name).to_path_buf(); @@ -114,7 +114,7 @@ impl WalletSeed { ))?; } warn!("{} backed up as {}", seed_file_name, backup_seed_file_name); - Ok(()) + Ok(backup_seed_file_name) } pub fn recover_from_phrase( @@ -180,7 +180,6 @@ impl WalletSeed { data_file_dir: &str, password: util::ZeroingString, ) -> Result { - // TODO: Is this desirable any more? // create directory if it doesn't exist fs::create_dir_all(data_file_dir).context(ErrorKind::IO)?; @@ -205,6 +204,15 @@ impl WalletSeed { Err(ErrorKind::WalletSeedDoesntExist)? } } + + pub fn delete_seed_file(data_file_dir: &str) -> Result<(), Error> { + let seed_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,); + if Path::new(seed_file_path).exists() { + debug!("Deleting wallet seed file at: {}", seed_file_path); + fs::remove_file(seed_file_path).context(ErrorKind::IO)?; + } + Ok(()) + } } /// Encrypted wallet seed, for storing on disk and decrypting diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 65729db2..73af8444 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -112,10 +112,15 @@ where ) -> Result<(), Error>; /// changes password - fn change_password(&self, old: String, new: String) -> Result<(), Error>; + fn change_password( + &self, + name: Option<&str>, + old: ZeroingString, + new: ZeroingString, + ) -> Result<(), Error>; /// deletes wallet - fn delete_wallet(&self, name: Option, password: String) -> Result<(), Error>; + fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error>; /// return wallet instance fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error>; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b584ea04..729fc0c6 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -405,8 +405,8 @@ where let value: OUT = v; Ok(Ok(value)) } - Err(e) => { - println!("Error deserializing: {:?}", e); + Err(_) => { + //println!("Error deserializing: {:?}", e); let value: OUT = serde_json::from_value(json!("Null")).unwrap(); Ok(Ok(value)) } diff --git a/tests/data/v3_reqs/change_password.req.json b/tests/data/v3_reqs/change_password.req.json new file mode 100644 index 00000000..14de45b3 --- /dev/null +++ b/tests/data/v3_reqs/change_password.req.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "change_password", + "params": { + "name": null, + "old": "passwoid", + "new": "password" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_wallet.req.json b/tests/data/v3_reqs/create_wallet.req.json index e97e5d8c..3b5f081a 100644 --- a/tests/data/v3_reqs/create_wallet.req.json +++ b/tests/data/v3_reqs/create_wallet.req.json @@ -4,7 +4,7 @@ "params": { "name": null, "mnemonic": null, - "mnemonic_length": 0, + "mnemonic_length": 32, "password": "passwoid" }, "id": 1 diff --git a/tests/data/v3_reqs/delete_wallet.req.json b/tests/data/v3_reqs/delete_wallet.req.json new file mode 100644 index 00000000..ef1aa97c --- /dev/null +++ b/tests/data/v3_reqs/delete_wallet.req.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "delete_wallet", + "params": { + "name": null + }, + "id": 1 +} diff --git a/tests/owner_v3_lifecycle.rs b/tests/owner_v3_lifecycle.rs index 0d5164eb..ad5c627b 100644 --- a/tests/owner_v3_lifecycle.rs +++ b/tests/owner_v3_lifecycle.rs @@ -367,7 +367,91 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { println!("RES 16: {:?}", res); assert!(res.is_ok()); + //17) Change the password + let req = include_str!("data/v3_reqs/close_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 17: {:?}", res); + assert!(res.is_ok()); + + let req = include_str!("data/v3_reqs/change_password.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 17a: {:?}", res); + assert!(res.is_ok()); + + // 18) trying to open with old password should fail + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 18: {:?}", res); + assert!(res.is_err()); + + // 19) Open with new password + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "open_wallet", + "params": { + "name": null, + "password": "password" + } + }); + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 19: {:?}", res); + assert!(res.is_ok()); + let token = res.unwrap(); + + // 20) Send a request with new token with changed password, ensure balances are still there and + // therefore seed is the same + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 20: {:?}", res); + thread::sleep(Duration::from_millis(200)); + assert_eq!(res.unwrap().1.amount_awaiting_finalization, 6000000000); + + // 21) Delete the wallet (close first) + let req = include_str!("data/v3_reqs/close_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + assert!(res.is_ok()); + + let req = include_str!("data/v3_reqs/delete_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 21: {:?}", res); + assert!(res.is_ok()); + + // 22) Wallet should be gone + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 22: {:?}", res); + assert!(res.is_err()); + clean_output_dir(test_dir); Ok(())