-
Notifications
You must be signed in to change notification settings - Fork 90
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
[gh-755] encrypt w/ ChaCha20Poly1305 and password algorithm Argon2id #856
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 1 Ignored Deployment
|
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.
When to unlock the private key?
crates/rooch-key/src/key_derive.rs
Outdated
) -> Result<(Addr, KeyPair, KeyPairType, String), anyhow::Error> | ||
where | ||
KeyPairType: CoinOperations<Addr, KeyPair>, | ||
{ | ||
let mnemonic = Mnemonic::new(parse_word_length(word_length)?, Language::English); | ||
let seed = Seed::new(&mnemonic, ""); | ||
let password_str = password.as_deref().unwrap_or(""); | ||
let seed = Seed::new(&mnemonic, password_str); |
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 using the password_str
as part of the seed is not a good idea for CLI.
- The user needs to save the mnemonic and password and use the mnemonic and password to recover the account. If use a wrong password, will recover a new account.
- The user can not change the password.
Some wallet provides a salt
to mnemonic, like this, and the user needs to know it is salt, not the password.
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 user needs to save the mnemonic and password and use the mnemonic and password to recover the account. If use a wrong password, will recover a new account.
This is a common practice for hardware and software authentication schemes e.g. MetaMask with mnemonic and password, or BitBox02 with password option. Here, password is merely an option for the user and the user can opt in to choose whether to remember it.
The user can not change the password.
If changing password would generate a new account, it is the common practice for the bitcoin software bc we are using bip39 mnemonic word Rust lib. In other words, changing password would mean collapsing the existing seed infrastructure at the first time generated. There's an article introducing the passphrase trade-offs: https://help.blockstream.com/hc/en-us/articles/8712301763737-What-is-a-BIP39-passphrase-#h_01G8K3SK5VBHBYZSX5XS4309YG.
I think it's only a choice made by a user. If the user doesn't opt in with a password, we will still encrypt the key pair from the seed without password.
Some wallet provides a salt to mnemonic, like this, and the user needs to know it is salt, not the password.
The accurate answer is passphrase: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#user-content-From_mnemonic_to_seed. Salt is from a password with mnemonic string according to the original code:
let salt = format!("mnemonic{}", password);
We can add documents about how the seed phrase is constructed.
When decrypting private key, the user would need to provide:
To decrypt the private key and sign the transaction, by design. |
Discussion:
|
2ea6dfd
to
eb86ae9
Compare
crates/rooch/src/commands/init.rs
Outdated
@@ -23,6 +24,9 @@ pub struct Init { | |||
/// Command line input of custom server URL | |||
#[clap(short = 's', long = "server-url")] | |||
pub server_url: Option<String>, | |||
/// Whether a password should be provided. | |||
#[clap(short = 'p', long = "password")] | |||
password_required: bool, |
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.
Make password_required
default value to true
? And we can set it to false
in the test case.
So, if we want to let password_required
accept value, it should be Option<bool>
.
#[clap(short = 'p', long = "password")]
password_required: Option<bool>,
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've added it.
TODO: do some refactor to key store and retrieve decrypted private keys from key store params in signing functions. It may take some days and if it isn't included in Q3 release, I will handle this issue at a stable pace. |
This design may need further refactor:
|
crates/rooch-key/src/key_derive.rs
Outdated
let tag = { | ||
let start = ciphertext_with_tag.len() - 16; | ||
let end = ciphertext_with_tag.len(); | ||
let mut tag = Vec::with_capacity(16); | ||
tag.extend_from_slice(&ciphertext_with_tag[start..end]); | ||
tag | ||
}; |
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 split the tag? or include the tag in the ciphertext? I can not find the use case of the tag.
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 split the tag? or include the tag in the ciphertext? I can not find the use case of the tag.
The tag is useless here, and why did I split it was cause the private key should be 32 bytes. The ciphertext is composed of 32 bytes encrypted text and 16 bytes tag thus 48 bytes. In order to achieve key derivations, i.e. parsing the encrypted cipertext to private keys and vice versa, it needs to be split.
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.
2.5. The Poly1305 Algorithm
Poly1305 is a one-time authenticator designed by D. J. Bernstein.
Poly1305 takes a 32-byte one-time key and a message and produces a
16-byte tag. This tag is used to authenticate the message.
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.
Add a comment to the tag.
Merge this PR first or wait to implement the password option to decrypt private key for commands, like |
You can merge this PR first and then I will refactor key store, yaml, etc. and test the signature verification feature with encrypted key pair. The password option has already been added to those commands to decrypt the private key. |
// Prompt for a password if required | ||
rpassword::prompt_password("Enter a password to encrypt the keys in the rooch keystore. Press return to have an empty value: ").unwrap() | ||
}; | ||
println!("Your password is {}", password); |
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.
Do not need to print the password when decrypting the keypair.
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.
Resolved.
|
||
/// Whether a password should be provided | ||
#[clap(short = 'p', long = "password")] | ||
password_required: Option<bool>, |
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.
Is it possible to auto-detect that if the private key is encrypted, then prompt the password input?
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 private key is always encrypted. When the user does not enter the password, the password defaults to "".
Some tests need to be fixed. |
This command will fail: rooch move run --function 0x3::empty::empty --sender-account 0xac00d67a15ae97af88aff69cd80befdfe430330a08a6092163de84753cb220c4 --session-key 975a3d052b5ee4bdcc7c8c8a16ae878ef427b4fe6ecb1aa9088b43645111ae20 --password false The session key
The new structure of the key store: keys:
0xac00d67a15ae97af88aff69cd80befdfe430330a08a6092163de84753cb220c4:
RoochKeyPairType:
hashed_password: $argon2id$v=19$m=19456,t=2,p=1$7vLoelDnzdybYdaHLYZXYQrqYSyOix7z5jC6Imf175A$jvcGd8dyjCrG4tAhYTqyq9J1aI54Ugvr0bUYdB9ygSo
... I don't know the session key's design since the structure of the key store has been changed to eliminate the key pair, but is it required to save the session key as key pair in key store or in |
Then cmd: "session-key create --sender-account {default} --scope 0x3::empty::empty" | ||
Then cmd: "move run --function 0x3::empty::empty --sender-account {default} --session-key {{$.session-key[-1].authentication_key}}" | ||
Then cmd: "session-key create --sender-account {default} --scope 0x3::empty::empty --password false" | ||
Then cmd: "move run --function 0x3::empty::empty --sender-account {default} --session-key {{$.session-key[-1].authentication_key}} --password false" |
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 seems like the session-key
command output is broken, so this command can not get the --session-key
argument.
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.
This issue has been fixed.
crates/rooch-key/src/key_derive.rs
Outdated
fn derive_key_pair_from_path( | ||
type EncryptionKeyResult = Result<(Vec<u8>, Vec<u8>, Vec<u8>), RoochError>; | ||
|
||
pub trait CoinOperations<Addr, KeyPair, PrivKey> { |
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 CoinOperations
name is not suitable yet.
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.
Renamed.
crates/rooch-key/src/key_derive.rs
Outdated
// 96-bits; unique per message | ||
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); | ||
|
||
// Derive the key material using nonce and password | ||
let mut output_key_material = [0u8; 32]; // Can be any desired size | ||
Argon2::default() | ||
.hash_password_into( | ||
password.unwrap_or("".to_owned()).as_bytes(), | ||
&nonce, | ||
&mut output_key_material, | ||
) | ||
.map_err(|e| RoochError::KeyConversionError(e.to_string()))?; | ||
|
||
// Create a ChaCha20Poly1305 cipher with the key material from password | ||
let cipher = ChaCha20Poly1305::new_from_slice(&output_key_material) | ||
.map_err(|e| RoochError::KeyConversionError(e.to_string()))?; | ||
|
||
// Encrypt the private key data to a ciphertext with a tag | ||
let ciphertext_with_tag = match cipher.encrypt(&nonce, private_key.as_bytes()) { | ||
Ok(ciphertext) => ciphertext, | ||
Err(_) => { | ||
return Err(RoochError::KeyConversionError( | ||
"Encryption failed".to_owned(), | ||
)) | ||
} | ||
}; | ||
|
||
// Extract the ciphertext without the tag | ||
let ciphertext = ciphertext_with_tag[..ciphertext_with_tag.len() - 16].to_vec(); | ||
|
||
// Extract the tag (last 16 bytes) | ||
// The tag is useless here for deriving the key pair in function derive_key_pair_from_ciphertext | ||
// Because from_bytes needs exactly 32 bytes input to convert to the private keys | ||
// Poly1305 is a one-time authenticator designed by D. J. Bernstein. | ||
// Poly1305 takes a 32-byte one-time key and a message and produces a | ||
// 16-byte tag. This tag is used to authenticate the message. | ||
// https://www.rfc-editor.org/rfc/rfc7539 | ||
let tag = { | ||
let start = ciphertext_with_tag.len() - 16; | ||
let end = ciphertext_with_tag.len(); | ||
let mut tag = Vec::with_capacity(16); | ||
tag.extend_from_slice(&ciphertext_with_tag[start..end]); | ||
tag | ||
}; | ||
|
||
Ok((nonce.to_vec(), ciphertext, tag)) |
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.
Most lines repeat with CoinOperations<RoochKeyPair>
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.
Simplified.
crates/rooch-key/src/keystore.rs
Outdated
@@ -47,30 +59,39 @@ pub enum Keystore<K: Ord, V> { | |||
} | |||
|
|||
#[enum_dispatch] | |||
pub trait AccountKeystore<Addr: Copy, PubKey, KeyPair, Sig, TransactionData>: Send + Sync { | |||
pub trait AccountKeystore<Addr: Copy, PubKey, KeyPair, Sig, TransactionData, PrivKey>: |
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.
This should be part of #788. As discussed in the PR, Maybe we do not need to distinguish the Keystore via the KeyPair type and TransactionData.
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.
Will refactor it in #788 then.
crates/rooch/src/commands/init.rs
Outdated
active_address: Some(new_address), | ||
password: Some(result.result.encryption.hashed_password), | ||
nonce: Some(result.result.encryption.nonce.encode_hex()), | ||
ciphertext: Some(result.result.encryption.ciphertext.encode_hex()), |
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.
keystore
file should be an independent file, not part of client_config.
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.
Removed.
// Prompt for a password if required | ||
rpassword::prompt_password("Enter a password to encrypt the keys in the rooch keystore. Press return to have an empty value: ").unwrap() | ||
}; | ||
println!("Your password is {}", password); |
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.
Do not print the password when decrypting.
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 leaved it with an empty string for a new password.
We can merge this PR first and discuss some refactors later:
|
|
I didn't mean to remove the password prompt. But it is OK. I will merge this PR first, and we discuss how to refactor it. |
Summary
ChaCha20Poly1305
to encrypt the key pair.Argon2id
to encrypt password in plaintext and use input password compared with cipher password.nonce
,ciphertext
,tag
,password
from key store to compare with input password and if successful, retrieve the private key and form a key pair to sign transactions.ChaCha20Poly1305 is a standardized encryption algorithm (AEAD) widely used by network softwares:
Argon2id is a password encryption algorithm recommended by OWASP in their recent document:
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html