Skip to content
Closed
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
2 changes: 1 addition & 1 deletion age/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ mod util;
pub use error::{DecryptError, EncryptError};
pub use identity::{IdentityFile, IdentityFileEntry};
pub use primitives::stream;
pub use protocol::{decryptor, Decryptor, Encryptor};
pub use protocol::{decryptor, Decryptor, Encryptor, WorkFactor};

#[cfg(feature = "armor")]
pub use primitives::armor;
Expand Down
22 changes: 18 additions & 4 deletions age/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use crate::{
scrypt, Recipient,
};

pub use crate::scrypt::WorkFactor;

#[cfg(feature = "async")]
use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

Expand Down Expand Up @@ -52,7 +54,7 @@ enum EncryptorType {
/// Encryption to a list of recipients identified by keys.
Keys(Vec<Box<dyn Recipient>>),
/// Encryption to a passphrase.
Passphrase(SecretString),
Passphrase(SecretString, WorkFactor),
}

/// Encryptor for creating an age file.
Expand All @@ -74,7 +76,19 @@ impl Encryptor {
///
/// [`x25519::Identity`]: crate::x25519::Identity
pub fn with_user_passphrase(passphrase: SecretString) -> Self {
Encryptor(EncryptorType::Passphrase(passphrase))
Encryptor(EncryptorType::Passphrase(passphrase, WorkFactor::default()))
}

/// Returns an `Encryptor` that will create an age file encrypted with a passphrase and a specify work factor.
/// Anyone with the passphrase can decrypt the file.
///
/// This API should only be used with a passphrase that was provided by (or generated
/// for) a human. For programmatic use cases, instead generate an [`x25519::Identity`]
/// and then use [`Encryptor::with_recipients`].
///
/// [`x25519::Identity`]: crate::x25519::Identity
pub fn with_user_passphrase_and_work_factor(passphrase: SecretString, work_factor: WorkFactor) -> Self {
Encryptor(EncryptorType::Passphrase(passphrase, work_factor))
}

/// Creates the header for this age file.
Expand All @@ -91,8 +105,8 @@ impl Encryptor {
stanzas.push(grease_the_joint());
stanzas
}
EncryptorType::Passphrase(passphrase) => {
scrypt::Recipient { passphrase }.wrap_file_key(&file_key)?
EncryptorType::Passphrase(passphrase, work_factor) => {
scrypt::Recipient { passphrase, work_factor }.wrap_file_key(&file_key)?
}
};

Expand Down
28 changes: 24 additions & 4 deletions age/src/scrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const ENCRYPTED_FILE_KEY_BYTES: usize = FILE_KEY_BYTES + 16;
/// Pick an scrypt work factor that will take around 1 second on this device.
///
/// Guaranteed to return a valid work factor (less than 64).
fn target_scrypt_work_factor() -> u8 {
fn target_scrypt_work_factor(time_target: Duration) -> u8 {
// Time a work factor that should always be fast.
let mut log_n = 10;

Expand Down Expand Up @@ -63,7 +63,7 @@ fn target_scrypt_work_factor() -> u8 {
duration
.map(|mut d| {
// Use duration as a proxy for CPU usage, which scales linearly with N.
while d < ONE_SECOND && log_n < 63 {
while d < time_target && log_n < 63 {
log_n += 1;
d *= 2;
}
Expand All @@ -75,8 +75,25 @@ fn target_scrypt_work_factor() -> u8 {
})
}

/// The work factor used for scrypt
/// The default is 1s
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum WorkFactor{
/// A hardcoded value
Fixed(u8),
/// A time-based value
TimeBased(Duration)
}

impl Default for WorkFactor {
fn default() -> Self {
WorkFactor::TimeBased(ONE_SECOND)
}
}

pub(crate) struct Recipient {
pub(crate) passphrase: SecretString,
pub(crate) work_factor: WorkFactor
}

impl crate::Recipient for Recipient {
Expand All @@ -88,7 +105,10 @@ impl crate::Recipient for Recipient {
inner_salt.extend_from_slice(SCRYPT_SALT_LABEL);
inner_salt.extend_from_slice(&salt);

let log_n = target_scrypt_work_factor();
let log_n = match self.work_factor {
WorkFactor::Fixed(f) => f,
WorkFactor::TimeBased(target) => target_scrypt_work_factor(target)
};

let enc_key =
scrypt(&inner_salt, log_n, self.passphrase.expose_secret()).expect("log_n < 64");
Expand Down Expand Up @@ -129,7 +149,7 @@ impl<'a> crate::Identity for Identity<'a> {
}

// Place bounds on the work factor we will accept (roughly 16 seconds).
let target = target_scrypt_work_factor();
let target = target_scrypt_work_factor(ONE_SECOND);
if log_n > self.max_work_factor.unwrap_or_else(|| target + 4) {
return Some(Err(DecryptError::ExcessiveWork {
required: log_n,
Expand Down