Skip to content

Commit

Permalink
store scrypt params in backup
Browse files Browse the repository at this point in the history
  • Loading branch information
Stefano committed Oct 9, 2023
1 parent 20eedf9 commit 7ba33ef
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 57 deletions.
172 changes: 117 additions & 55 deletions src/wallet/backup.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use chacha20poly1305::aead::{generic_array::GenericArray, stream};
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305};
use rand::{distributions::Alphanumeric, Rng};
use scrypt::password_hash::{PasswordHasher, Salt};
use scrypt::Scrypt;
use scrypt::password_hash::{PasswordHasher, Salt, SaltString};
use scrypt::{Params, Scrypt};
use serde::{Deserialize, Serialize};
use slog::Logger;
use tempfile::TempDir;
use typenum::consts::U32;
Expand All @@ -25,16 +26,65 @@ const BACKUP_VERSION: u8 = 1;

struct BackupPaths {
encrypted: PathBuf,
nonce: PathBuf,
salt: PathBuf,
backup_pub_data: PathBuf,
tempdir: TempDir,
version: PathBuf,
zip: PathBuf,
}

struct CypherSecrets {
key: GenericArray<u8, U32>,
nonce: [u8; BACKUP_NONCE_LENGTH],
#[derive(Clone, Deserialize, Serialize)]
pub(crate) struct ScryptParams {
log_n: u8,
r: u32,
p: u32,
len: usize,
version: Option<u32>,
algorithm: Option<String>,
}

impl ScryptParams {
pub(crate) fn new(log_n: Option<u8>, r: Option<u32>, p: Option<u32>) -> ScryptParams {
ScryptParams {
log_n: log_n.unwrap_or(Params::RECOMMENDED_LOG_N),
r: r.unwrap_or(Params::RECOMMENDED_R),
p: p.unwrap_or(Params::RECOMMENDED_P),
len: BACKUP_KEY_LENGTH,
version: None,
algorithm: None,
}
}
}

impl Default for ScryptParams {
fn default() -> ScryptParams {
ScryptParams::new(None, None, None)
}
}

impl TryInto<Params> for ScryptParams {
type Error = Error;

fn try_into(self: ScryptParams) -> Result<Params, Error> {
Params::new(self.log_n, self.r, self.p, self.len).map_err(|e| Error::Internal {
details: format!("invalid params {}", e),
})
}
}

#[derive(Deserialize, Serialize)]
struct BackupPubData {
scrypt_params: ScryptParams,
salt: String,
nonce: String,
version: u8,
}

impl BackupPubData {
fn nonce(&self) -> Result<[u8; BACKUP_NONCE_LENGTH], InternalError> {
let nonce_bytes = self.nonce.as_bytes();
nonce_bytes[0..BACKUP_NONCE_LENGTH]
.try_into()
.map_err(|_| InternalError::Unexpected)
}
}

impl Wallet {
Expand All @@ -45,6 +95,19 @@ impl Wallet {
/// hashing and a random nonce for encrypting are randomly generated and included in the final
/// backup file, along with the backup version
pub fn backup(&self, backup_path: &str, password: &str) -> Result<(), Error> {
self.backup_customize(backup_path, password, None)
}

/// For now this method is only used for testing, a few details should be refined before
/// exposing it to the public:
/// - Which parameters should we allow users to change? Should we set sensible minimums?
/// - Can we guarantee old backups can always be recovered in the future?
pub(crate) fn backup_customize(
&self,
backup_path: &str,
password: &str,
scrypt_params: Option<ScryptParams>,
) -> Result<(), Error> {
// setup
info!(self.logger, "starting backup...");
let backup_file = PathBuf::from(&backup_path);
Expand All @@ -55,18 +118,23 @@ impl Wallet {
}
let tmp_base_path = _get_parent_path(&backup_file)?;
let files = _get_backup_paths(&tmp_base_path)?;
let salt: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(BACKUP_KEY_LENGTH)
.map(char::from)
.collect();
debug!(self.logger, "using generated salt: {}", &salt);
let scrypt_params = scrypt_params.unwrap_or_default();
let mut rng = rand::thread_rng();
let salt = SaltString::generate(&mut rng);
let str_params = serde_json::to_string(&scrypt_params).map_err(InternalError::from)?;
debug!(self.logger, "using generated scrypt params: {}", str_params);
let nonce: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(BACKUP_NONCE_LENGTH)
.map(char::from)
.collect();
debug!(self.logger, "using generated nonce: {}", &nonce);
let backup_pub_data = BackupPubData {
scrypt_params,
salt: salt.to_string(),
nonce,
version: BACKUP_VERSION,
};

// create zip archive of wallet data
debug!(
Expand All @@ -80,12 +148,13 @@ impl Wallet {
self.logger,
"\nencrypting {:?} to {:?}", &files.zip, &files.encrypted
);
_encrypt_file(&files.zip, &files.encrypted, password, &salt, &nonce)?;
_encrypt_file(&files.zip, &files.encrypted, password, &backup_pub_data)?;

// add backup nonce + salt + version to final zip file
write(files.nonce, nonce)?;
write(files.salt, salt)?;
write(files.version, BACKUP_VERSION.to_string())?;
write(
files.backup_pub_data,
serde_json::to_string(&backup_pub_data).unwrap(),
)?;
debug!(
self.logger,
"\nzipping {:?} to {:?}", &files.tempdir, &backup_file
Expand Down Expand Up @@ -118,13 +187,11 @@ pub fn restore_backup(backup_path: &str, password: &str, target_dir: &str) -> Re
// unpack given zip file and retrieve backup data
info!(logger, "unzipping {:?}", backup_file);
_unzip(&backup_file, &PathBuf::from(files.tempdir.path()), &logger)?;
let nonce = read_to_string(files.nonce)?;
debug!(logger, "using retrieved nonce: {}", &nonce);
let salt = read_to_string(files.salt)?;
debug!(logger, "using retrieved salt: {}", &salt);
let version = read_to_string(files.version)?
.parse::<u8>()
.map_err(|_| InternalError::Unexpected)?;
let json_pub_data = read_to_string(files.backup_pub_data)?;
debug!(logger, "using retrieved backup_pub_data: {}", json_pub_data);
let backup_pub_data: BackupPubData =
serde_json::from_str(json_pub_data.as_str()).map_err(InternalError::from)?;
let version = backup_pub_data.version;
debug!(logger, "retrieved version: {}", &version);
if version != BACKUP_VERSION {
return Err(Error::UnsupportedBackupVersion {
Expand All @@ -137,7 +204,7 @@ pub fn restore_backup(backup_path: &str, password: &str, target_dir: &str) -> Re
logger.clone(),
"decrypting {:?} to {:?}", files.encrypted, files.zip
);
_decrypt_file(&files.encrypted, &files.zip, password, &salt, &nonce)?;
_decrypt_file(&files.encrypted, &files.zip, password, &backup_pub_data)?;
info!(
logger.clone(),
"unzipping {:?} to {:?}", &files.zip, &target_dir_path
Expand All @@ -152,16 +219,12 @@ fn _get_backup_paths(tmp_base_path: &Path) -> Result<BackupPaths, Error> {
create_dir_all(tmp_base_path)?;
let tempdir = tempfile::tempdir_in(tmp_base_path)?;
let encrypted = tempdir.path().join("backup.enc");
let nonce = tempdir.path().join("backup.nonce");
let salt = tempdir.path().join("backup.salt");
let version = tempdir.path().join("backup.version");
let backup_pub_data = tempdir.path().join("backup.pub_data");
let zip = tempdir.path().join("backup.zip");
Ok(BackupPaths {
encrypted,
nonce,
salt,
backup_pub_data,
tempdir,
version,
zip,
})
}
Expand Down Expand Up @@ -274,47 +337,46 @@ fn _unzip(zip_path: &PathBuf, path_out: &Path, logger: &Logger) -> Result<(), Er

fn _get_cypher_secrets(
password: &str,
salt_str: &str,
nonce_str: &str,
) -> Result<CypherSecrets, Error> {
backup_pub_data: &BackupPubData,
) -> Result<GenericArray<u8, U32>, Error> {
// hash password using scrypt with the provided salt
let password_bytes = password.as_bytes();
let salt = Salt::from_b64(salt_str).map_err(InternalError::from)?;
let salt = Salt::from_b64(&backup_pub_data.salt).map_err(InternalError::from)?;
let password_hash = Scrypt
.hash_password(password_bytes, salt)
.hash_password_customized(
password_bytes,
None,
None,
backup_pub_data.scrypt_params.clone().try_into()?,
salt,
)
.map_err(InternalError::from)?;
let hash_output = password_hash
.hash
.ok_or_else(|| InternalError::NoPasswordHashError)?;
let hash = hash_output.as_bytes();

// get key from password hash
let key = Key::clone_from_slice(&hash[..BACKUP_KEY_LENGTH]);

// get nonce from provided str
let nonce_bytes = nonce_str.as_bytes();
let nonce: [u8; BACKUP_NONCE_LENGTH] = nonce_bytes[0..BACKUP_NONCE_LENGTH]
.try_into()
.map_err(|_| InternalError::Unexpected)?;
let key = Key::clone_from_slice(hash);

Ok(CypherSecrets { key, nonce })
Ok(key)
}

fn _encrypt_file(
path_cleartext: &PathBuf,
path_encrypted: &PathBuf,
password: &str,
salt_str: &str,
nonce_str: &str,
backup_pub_data: &BackupPubData,
) -> Result<(), Error> {
let cypher_secrets = _get_cypher_secrets(password, salt_str, nonce_str)?;
let key = _get_cypher_secrets(password, backup_pub_data)?;

// - XChacha20Poly1305 is fast, requires no special hardware and supports stream operation
// - stream mode required as files to encrypt may be big, so avoiding a memory buffer

// setup
let aead = XChaCha20Poly1305::new(&cypher_secrets.key);
let nonce = GenericArray::from_slice(&cypher_secrets.nonce);
let aead = XChaCha20Poly1305::new(&key);
let nonce = backup_pub_data.nonce()?;
let nonce = GenericArray::from_slice(&nonce);
let mut stream_encryptor = stream::EncryptorBE32::from_aead(aead, nonce);
let mut buffer = [0u8; BACKUP_BUFFER_LEN_ENCRYPT];
let mut source_file = File::open(path_cleartext)?;
Expand Down Expand Up @@ -347,14 +409,14 @@ fn _decrypt_file(
path_encrypted: &PathBuf,
path_cleartext: &PathBuf,
password: &str,
salt_str: &str,
nonce_str: &str,
backup_pub_data: &BackupPubData,
) -> Result<(), Error> {
let cypher_secrets = _get_cypher_secrets(password, salt_str, nonce_str)?;
let key = _get_cypher_secrets(password, backup_pub_data)?;

// setup
let aead = XChaCha20Poly1305::new(&cypher_secrets.key);
let nonce = GenericArray::from_slice(&cypher_secrets.nonce);
let aead = XChaCha20Poly1305::new(&key);
let nonce = backup_pub_data.nonce()?;
let nonce = GenericArray::from_slice(&nonce);
let mut stream_decryptor = stream::DecryptorBE32::from_aead(aead, nonce);
let mut buffer = [0u8; BACKUP_BUFFER_LEN_DECRYPT];
let mut source_file = File::open(path_encrypted)?;
Expand Down
12 changes: 10 additions & 2 deletions src/wallet/test/backup.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::*;
use crate::wallet::backup::restore_backup;
use crate::wallet::backup::{restore_backup, ScryptParams};
use scrypt::Params;
use serial_test::parallel;

#[test]
Expand Down Expand Up @@ -285,7 +286,14 @@ fn double_restore() {
// backup
println!("\nbacking up...");
wallet_1.backup(&backup_file_1, password_1).unwrap();
wallet_2.backup(&backup_file_2, password_2).unwrap();
let custom_params = ScryptParams::new(
Some(Params::RECOMMENDED_LOG_N + 1),
Some(Params::RECOMMENDED_R + 1),
Some(Params::RECOMMENDED_P + 1),
);
wallet_2
.backup_customize(&backup_file_2, password_2, Some(custom_params))
.unwrap();

// drop wallets
drop(online_1);
Expand Down

0 comments on commit 7ba33ef

Please sign in to comment.