Skip to content

Commit

Permalink
Add recovery phrase for password recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
radumarias committed Dec 13, 2024
1 parent de64d45 commit 340e960
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
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

0 comments on commit 340e960

Please sign in to comment.