Skip to content
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

Add recovery phrase for password recovery #260

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions examples/change_password_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::io;
use std::io::Write;
use std::{env::args, str::FromStr};

use bip39::{Language, Mnemonic, MnemonicType};
use rpassword::read_password;
use shush_rs::{ExposeSecret, SecretString};
use tracing::{error, info};
Expand Down Expand Up @@ -46,4 +47,38 @@ async fn main() {
Err(FsError::InvalidDataDirStructure) => error!("Invalid structure of data directory"),
Err(err) => error!("Error: {err}"),
}

// Generate a 24-word recovery phrase
let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English);
let phrase = mnemonic.phrase();
println!("Your recovery phrase is: {}", phrase);

// Use the recovery phrase to change the password
print!("Enter recovery phrase: ");
io::stdout().flush().unwrap();
let recovery_phrase = read_password().unwrap();
let mnemonic = Mnemonic::from_phrase(&recovery_phrase, Language::English).unwrap();
let seed = mnemonic.to_seed("");
let new_password = SecretString::from_str(&hex::encode(seed)).unwrap();
print!("Confirm new password: ");
io::stdout().flush().unwrap();
let new_password2 = SecretString::from_str(&read_password().unwrap()).unwrap();
if new_password.expose_secret() != new_password2.expose_secret() {
error!("Passwords do not match");
return;
}
println!("Changing password using recovery phrase...");
match EncryptedFs::passwd(
Path::new(&data_dir),
SecretString::from_str(&recovery_phrase).unwrap(),
new_password,
Cipher::ChaCha20Poly1305,
)
.await
{
Ok(()) => info!("Password changed successfully using recovery phrase"),
Err(FsError::InvalidPassword) => error!("Invalid recovery phrase"),
Err(FsError::InvalidDataDirStructure) => error!("Invalid structure of data directory"),
Err(err) => error!("Error: {err}"),
}
}
30 changes: 30 additions & 0 deletions src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use base64::alphabet::STANDARD;
use base64::engine::general_purpose::NO_PAD;
use base64::engine::GeneralPurpose;
use base64::{DecodeError, Engine};
use bip39::{Language, Mnemonic, MnemonicType, Seed};
use hex::FromHexError;
use num_format::{Locale, ToFormattedString};
use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng};
Expand Down Expand Up @@ -380,6 +381,20 @@ where
Ok(())
}

pub fn generate_recovery_phrase(language: Language) -> Mnemonic {
Mnemonic::new(MnemonicType::Words24, language)
}

pub fn derive_key_from_recovery_phrase(
recovery_phrase: &str,
language: Language,
) -> Result<SecretVec<u8>> {
let mnemonic = Mnemonic::from_phrase(recovery_phrase, language)
.map_err(|err| Error::GenericString(err.to_string()))?;
let seed = mnemonic.to_seed("");
Ok(SecretVec::new(Box::new(seed.as_bytes().to_vec())))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -650,4 +665,19 @@ mod tests {

assert!(result.is_err());
}

#[test]
fn test_generate_recovery_phrase() {
let mnemonic = generate_recovery_phrase(Language::English);
let phrase = mnemonic.phrase();
assert_eq!(phrase.split_whitespace().count(), 24);
}

#[test]
fn test_derive_key_from_recovery_phrase() {
let mnemonic = generate_recovery_phrase(Language::English);
let phrase = mnemonic.phrase();
let derived_key = derive_key_from_recovery_phrase(phrase, Language::English).unwrap();
assert_eq!(derived_key.expose_secret().len(), 64);
}
}
60 changes: 60 additions & 0 deletions src/encryptedfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) const CONTENTS_DIR: &str = "contents";
pub(crate) const SECURITY_DIR: &str = "security";
pub(crate) const KEY_ENC_FILENAME: &str = "key.enc";
pub(crate) const KEY_SALT_FILENAME: &str = "key.salt";
pub(crate) const RECOVERY_PHRASE_KEY_ENC_FILENAME: &str = "recovery_phrase_key.enc";

pub(crate) const LS_DIR: &str = "ls";
pub(crate) const HASH_DIR: &str = "hash";
Expand Down Expand Up @@ -2158,6 +2159,65 @@ impl EncryptedFs {
Ok(())
}

/// Change the password of the filesystem using the recovery phrase.
pub async fn passwd_with_recovery_phrase(
data_dir: &Path,
recovery_phrase: &str,
new_password: SecretString,
cipher: Cipher,
) -> FsResult<()> {
check_structure(data_dir, false).await?;
// decrypt key using recovery phrase
let salt: Vec<u8> = bincode::deserialize_from(File::open(
data_dir.join(SECURITY_DIR).join(KEY_SALT_FILENAME),
)?)?;
let initial_key = crypto::derive_key_from_recovery_phrase(recovery_phrase, cipher, &salt)?;
let enc_file = data_dir.join(SECURITY_DIR).join(RECOVERY_PHRASE_KEY_ENC_FILENAME);
let reader = crypto::create_read(File::open(enc_file)?, cipher, &initial_key);
let key: Vec<u8> =
bincode::deserialize_from(reader).map_err(|_| FsError::InvalidPassword)?;
let key = SecretBox::new(Box::new(key));
// encrypt it with a new key derived from new password
let new_key = crypto::derive_key(&new_password, cipher, &salt)?;
crypto::atomic_serialize_encrypt_into(
&data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME),
&*key.expose_secret(),
cipher,
&new_key,
)?;
Ok(())
}

/// Regenerate the recovery phrase for the filesystem.
pub async fn regenerate_recovery_phrase(
data_dir: &Path,
password: SecretString,
old_recovery_phrase: &str,
new_recovery_phrase: &str,
cipher: Cipher,
) -> FsResult<()> {
check_structure(data_dir, false).await?;
// decrypt key using old recovery phrase
let salt: Vec<u8> = bincode::deserialize_from(File::open(
data_dir.join(SECURITY_DIR).join(KEY_SALT_FILENAME),
)?)?;
let initial_key = crypto::derive_key_from_recovery_phrase(old_recovery_phrase, cipher, &salt)?;
let enc_file = data_dir.join(SECURITY_DIR).join(RECOVERY_PHRASE_KEY_ENC_FILENAME);
let reader = crypto::create_read(File::open(enc_file)?, cipher, &initial_key);
let key: Vec<u8> =
bincode::deserialize_from(reader).map_err(|_| FsError::InvalidPassword)?;
let key = SecretBox::new(Box::new(key));
// encrypt it with a new key derived from new recovery phrase
let new_key = crypto::derive_key_from_recovery_phrase(new_recovery_phrase, cipher, &salt)?;
crypto::atomic_serialize_encrypt_into(
&data_dir.join(SECURITY_DIR).join(RECOVERY_PHRASE_KEY_ENC_FILENAME),
&*key.expose_secret(),
cipher,
&new_key,
)?;
Ok(())
}

fn next_handle(&self) -> u64 {
self.current_handle
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
Expand Down
90 changes: 90 additions & 0 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ fn get_cli_args() -> ArgMatches {
.value_name("DATA_DIR")
.help("Where to store the encrypted data"),
)
.arg(
Arg::new("recovery-phrase")
.long("recovery-phrase")
.short('r')
.value_name("RECOVERY_PHRASE")
.help("Use the recovery phrase to change the password"),
)
.arg(
Arg::new("refresh-recovery-phrase")
.long("refresh-recovery-phrase")
.short('f')
.action(ArgAction::SetTrue)
.help("Regenerate the recovery phrase"),
)
)
.get_matches()
}
Expand Down Expand Up @@ -224,6 +238,82 @@ async fn async_main() -> Result<()> {
async fn run_change_password(cipher: Cipher, matches: &ArgMatches) -> Result<()> {
let data_dir: String = matches.get_one::<String>("data-dir").unwrap().to_string();

if matches.get_flag("refresh-recovery-phrase") {
// read password from stdin
print!("Enter password: ");
io::stdout().flush().unwrap();
let password = SecretString::from_str(&read_password().unwrap()).unwrap();
print!("Enter old recovery phrase: ");
io::stdout().flush().unwrap();
let old_recovery_phrase = read_password().unwrap();
print!("Enter new recovery phrase: ");
io::stdout().flush().unwrap();
let new_recovery_phrase = read_password().unwrap();
println!("Regenerating recovery phrase...");
EncryptedFs::regenerate_recovery_phrase(
Path::new(&data_dir),
password,
&old_recovery_phrase,
&new_recovery_phrase,
cipher,
)
.await
.map_err(|err| {
match err {
FsError::InvalidPassword => {
println!("Invalid password or recovery phrase");
}
FsError::InvalidDataDirStructure => {
println!("Invalid structure of data directory");
}
_ => {
error!(err = %err);
}
}
ExitStatusError::Failure(1)
})?;
println!("Recovery phrase regenerated successfully");
return Ok(());
}

if let Some(recovery_phrase) = matches.get_one::<String>("recovery-phrase") {
// read new password from stdin
print!("Enter new password: ");
io::stdout().flush().unwrap();
let new_password = SecretString::from_str(&read_password().unwrap()).unwrap();
print!("Confirm new password: ");
io::stdout().flush().unwrap();
let new_password2 = SecretString::from_str(&read_password().unwrap()).unwrap();
if new_password.expose_secret() != new_password2.expose_secret() {
println!("Passwords do not match");
return Err(ExitStatusError::Failure(1).into());
}
println!("Changing password using recovery phrase...");
EncryptedFs::passwd_with_recovery_phrase(
Path::new(&data_dir),
recovery_phrase,
new_password,
cipher,
)
.await
.map_err(|err| {
match err {
FsError::InvalidPassword => {
println!("Invalid recovery phrase");
}
FsError::InvalidDataDirStructure => {
println!("Invalid structure of data directory");
}
_ => {
error!(err = %err);
}
}
ExitStatusError::Failure(1)
})?;
println!("Password changed successfully using recovery phrase");
return Ok(());
}

// read password from stdin
print!("Enter old password: ");
io::stdout().flush().unwrap();
Expand Down
Loading