-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(wallet): wallet secret key file encryption #1949
feat(wallet): wallet secret key file encryption #1949
Conversation
Yeah include them in! It makes sense in your case! |
aa9b547 makes sure that the CLI prompts the user for a password when an encrypted wallet requires it for authentication. |
ca50432
to
92246d6
Compare
PR is ready for review. |
// Delete the unencrypted secret key file | ||
delete_main_secret_key(&wallet_dir)?; | ||
|
||
// Save the secret key as an encrypted file | ||
store_main_secret_key(&wallet_dir, &wallet_key, Some(password.to_owned()))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make sure the key is stored successfully before deleting the old one. Else what happens if the store_main_secret_key
errors out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah good call, that would be quite a catastrophic failure. I'll swap the order of actions around, so that store_main_secret_key
is done first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commit 71267b5 correctly handles this scenario now.
// Save the secret key as an encrypted file
store_main_secret_key(&wallet_dir, &wallet_key, Some(password.to_owned()))?;
// Delete the unencrypted secret key file
// Cleanup if it fails
if let Err(err) = delete_unencrypted_main_secret_key(&wallet_dir) {
let _ = delete_encrypted_main_secret_key(&wallet_dir);
return Err(err);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure, Cleanup if it fails
with delete_encrypted_main_secret_key
doesn't delete the encrypted one does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it does, are we 100% sure that an error above means the unencrypted_main_secret_key
was NOT deleted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just trying to make sure we don't lose keys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It comes down to https://doc.rust-lang.org/std/fs/fn.remove_file.html. I think that we can reasonably expect that it only fails when it couldn't delete the file (unencrypted main_secret_key
in this case).
But to be 100% on the safe side, we could not do a cleanup in case of failure of delete_unencrypted_main_secret_key
. Worst case scenario then, we would have both a main_secret_key
file and a main_secret_key.encoded
file in the same wallet directory.
/// to directory root_dir/wallet_ADDRESS | ||
/// to directory root_dir/wallet_DATETIME | ||
pub fn stash(root_dir: &Path) -> Result<PathBuf> { | ||
let wallet = HotWallet::load_from(root_dir)?; | ||
let wallet_dir = root_dir.join(WALLET_DIR_NAME); | ||
let addr_hex = &format!("{:?}", wallet.address()); | ||
let new_name = format!("{WALLET_DIR_NAME}_{addr_hex}"); | ||
let datetime_str = Local::now().format("%Y-%m-%d_%H-%M-%S").to_string(); | ||
let new_name = format!("{WALLET_DIR_NAME}_{datetime_str}"); | ||
let moved_dir = root_dir.join(new_name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why switch to date/time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes it a lot easier to stash encrypted wallets without needing to decrypt them to get the public address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without needing to decrypt them to get the public address.
Not sure I understand, how can you get the public address if it's not in the name anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public address can be derived from the secret key. The problem with encrypted wallets is that the secret key is encrypted and needs a password to reveal it.
If we look at the old code:
let wallet = HotWallet::load_from(root_dir)?; // Would fail if the wallet is encrypted
let addr_hex = &format!("{:?}", wallet.address()); // Needs wallet
To fix that, we would have to something like this:
let wallet = HotWallet::load_encrypted_from_path(root_dir, PASSWORD)?;
let addr_hex = &format!("{:?}", wallet.address());
But then the user needs to know and provide the correct password before an encrypted wallet can be stashed. Since this command is only used when creating a new wallet and replacing the old one, I think it makes more sense from a user perspective to not require the password of an encrypted wallet to stash it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh I see! Although sorting by key is better, it's a hack so we don't need to unlock the wallet so do that?
But then on the other hand, filtering by date here is weird, and we will need to cleanup old ones every time...
We will also need to filter by public key when we want to support multi-key in the future so keeping pks is cleaner.
How about keeping the public key in a non-encrypted file along with the wallet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh I see! Although sorting by key is better, it's a hack so we don't need to unlock the wallet so do that?
Correct!
How about keeping the public key in a non-encrypted file along with the wallet?
I thought about using the existing main_pubkey
file. But the drawback for me was that we can not verify whether that pubkey is actually valid for a locked encrypted wallet/secret key. We'd still have to unlock the wallet to verify if the pubkey actually belongs to that wallet.
We will also need to filter by public key when we want to support multi-key in the future so keeping pks is cleaner.
When the project starts supporting multiple keys/wallets, I agree that wallets should be saved in folder names that represent their pubkey. I'm not sure what folder structure will be used then (perhaps #1944 ?). In that case, I see the current stash
more as a function to backup an old wallet that you may or may not know the password of.
If we do decide on stashing old wallets under a folder name that contains the public address. I'm just a bit stuck on figuring out how we would best deal with stashing encrypted wallets where the user doesn't remember the password of?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub struct AuthenticationManager { | ||
/// Password to decrypt the wallet. | ||
/// Wrapped in Secret<> so that it doesn't accidentally get exposed | ||
password: Option<Secret<String>>, | ||
/// Expiry time of the password. | ||
/// Has to be provided by the user again after a certain amount of time | ||
password_expires_at: Option<DateTime<Utc>>, | ||
/// Path to the root directory of the wallet | ||
wallet_dir: PathBuf, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the wallet ends up in a file dumped on disk and this struct is a field of wallet.
So if the password
is Some
, it ends up dumped to the disk in clear too.
Even after expiry, an attacker could read the password by cat
'ing the wallet file.
Or did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the wallet file only a watch-only-wallet and not a hotwallet? Also, the Secret<>
wrapper from the secrecy crate, prevents just that scenario by not offering a serialize impl. And the debug impl will automatically redact the inner value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the wallet file only a watch-only-wallet and not a hotwallet?
That I'm not sure.
Also, the Secret<> wrapper from the secrecy crate, prevents just that scenario
Okay this is nice! We're safe then!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work! Very clean code, love this! 🥇
Left some comments!
71267b5
to
d61495f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Excellent work!
421deb1
to
966c5b2
Compare
Hi @grumbach , The test for stashing/unstashing fails only on Windows because of a permission error. I do not have a Windows PC to test myself. Is this a known problem specific to a Windows Docker image? EDIT: Managed to fix it. |
4075b1e
to
e6a3385
Compare
Description
Adds an option to save the wallet private key as encrypted with a password to a file called
main_secret_key.encrypted
, inside the wallet directory.The encrypted secret key file will look like this:
Relevant encryption logic:
CLI changes
wallet create
command now prompts the user if they want to encrypt their new wallet with a password. Also, the following flags have been added:Added
wallet encrypt
command to encrypt an existing unencrypted wallet with a password.Other changes
CLI commands that use an encrypted wallet, now request a user to provide the password to unlock the wallet.
The hotwallet stash function now uses a different folder name for saving old wallets. Instead of using the wallet public address:
wallet_PUBLIC_ADDRESS
, it now uses:wallet_CURRENT_DATETIME
. This change is to support stashing encrypted wallets, without having to decrypt them to get the public address.TODO List
Related Issue
Fixes #1943. Solution 1.
Type of Change
Please mark the types of changes made in this pull request.
Checklist
Please ensure all of the following tasks have been completed: