-
Notifications
You must be signed in to change notification settings - Fork 103
/
scrypt.rs
271 lines (240 loc) · 9.24 KB
/
scrypt.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
//! The "scrypt" passphrase-based recipient type, native to age.
use std::collections::HashSet;
use std::iter;
use std::time::Duration;
use age_core::{
format::{FileKey, Stanza, FILE_KEY_BYTES},
primitives::{aead_decrypt, aead_encrypt},
secrecy::{ExposeSecret, SecretString},
};
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use rand::{
distributions::{Alphanumeric, DistString},
rngs::OsRng,
RngCore,
};
use zeroize::Zeroize;
use crate::{
error::{DecryptError, EncryptError},
primitives::scrypt,
util::read::{base64_arg, decimal_digit_arg},
};
pub(super) const SCRYPT_RECIPIENT_TAG: &str = "scrypt";
const SCRYPT_SALT_LABEL: &[u8] = b"age-encryption.org/v1/scrypt";
const ONE_SECOND: Duration = Duration::from_secs(1);
const SALT_LEN: usize = 16;
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 {
let measure_duration = |log_n| {
// Platforms that have a functional SystemTime::now():
#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))]
{
use std::time::SystemTime;
let start = SystemTime::now();
scrypt(&[], log_n, "").expect("log_n < 64");
SystemTime::now().duration_since(start).ok()
}
// Platforms that can use Performance timer
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "web-sys"))]
{
web_sys::window().and_then(|window| {
{ window.performance() }.map(|performance| {
let start = performance.now();
scrypt(&[], log_n, "").expect("log_n < 64");
Duration::from_secs_f64((performance.now() - start) / 1_000e0)
})
})
}
// Platforms where SystemTime::now() panics:
#[cfg(all(
target_arch = "wasm32",
not(target_os = "wasi"),
not(feature = "web-sys")
))]
{
None
}
};
// Time a work factor that should always be fast.
let mut log_n = 10;
let mut duration: Option<Duration> = measure_duration(log_n);
while duration.map(|d| d.is_zero()).unwrap_or(false) {
// On some newer platforms, the work factor may be so fast that it is cannot be
// measured. Increase the work factor until we can measure something.
log_n += 1;
duration = measure_duration(log_n);
}
duration
.map(|mut d| {
// Use duration as a proxy for CPU usage, which scales linearly with N.
while d < ONE_SECOND && log_n < 63 {
log_n += 1;
d *= 2;
}
log_n
})
.unwrap_or({
// Couldn't measure, so guess. This is roughly 1 second on a modern machine.
18
})
}
/// A passphrase-based recipient. Anyone with the passphrase can decrypt the file.
///
/// If an `scrypt::Recipient` is used, it must be the only recipient for the file: it
/// can't be mixed with other recipient types and can't be used multiple times for the
/// same 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`].
///
/// [`x25519::Identity`]: crate::x25519::Identity
pub struct Recipient {
passphrase: SecretString,
log_n: u8,
}
impl Recipient {
/// Constructs a new `Recipient` with the given passphrase.
///
/// The scrypt work factor is picked to target about 1 second for encryption or
/// decryption on this device. Override it with [`Self::set_work_factor`].
pub fn new(passphrase: SecretString) -> Self {
Self {
passphrase,
log_n: target_scrypt_work_factor(),
}
}
/// Sets the scrypt work factor to `N = 2^log_n`.
///
/// This method must be called before [`Self::wrap_file_key`] to have an effect.
///
/// [`Self::wrap_file_key`]: crate::Recipient::wrap_file_key
///
/// # Panics
///
/// Panics if `log_n == 0` or `log_n >= 64`.
pub fn set_work_factor(&mut self, log_n: u8) {
assert!(0 < log_n && log_n < 64);
self.log_n = log_n;
}
}
impl crate::Recipient for Recipient {
fn wrap_file_key(
&self,
file_key: &FileKey,
) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
let mut rng = OsRng;
let mut salt = [0; SALT_LEN];
rng.fill_bytes(&mut salt);
let mut inner_salt = [0; SCRYPT_SALT_LABEL.len() + SALT_LEN];
inner_salt[..SCRYPT_SALT_LABEL.len()].copy_from_slice(SCRYPT_SALT_LABEL);
inner_salt[SCRYPT_SALT_LABEL.len()..].copy_from_slice(&salt);
let enc_key =
scrypt(&inner_salt, self.log_n, self.passphrase.expose_secret()).expect("log_n < 64");
let encrypted_file_key = aead_encrypt(&enc_key, file_key.expose_secret());
let encoded_salt = BASE64_STANDARD_NO_PAD.encode(salt);
let label = Alphanumeric.sample_string(&mut rng, 32);
Ok((
vec![Stanza {
tag: SCRYPT_RECIPIENT_TAG.to_owned(),
args: vec![encoded_salt, format!("{}", self.log_n)],
body: encrypted_file_key,
}],
iter::once(label).collect(),
))
}
}
/// A passphrase-based identity. Anyone with the passphrase can decrypt the file.
///
/// The identity caps the amount of work that the [`Decryptor`] might have to do to
/// process received files. A fairly high default is used (targeting roughly 16 seconds of
/// work per stanza on the current machine), which might not be suitable for systems
/// processing untrusted files.
///
/// [`Decryptor`]: crate::Decryptor
pub struct Identity {
passphrase: SecretString,
target_work_factor: u8,
max_work_factor: u8,
}
impl Identity {
/// Constructs a new `Identity` with the given passphrase.
pub fn new(passphrase: SecretString) -> Self {
let target_work_factor = target_scrypt_work_factor();
// Place bounds on the work factor we will accept (roughly 16 seconds).
let max_work_factor = target_work_factor + 4;
Self {
passphrase,
target_work_factor,
max_work_factor,
}
}
/// Sets the maximum accepted scrypt work factor to `N = 2^max_log_n`.
///
/// This method must be called before [`Self::unwrap_stanza`] to have an effect.
///
/// [`Self::unwrap_stanza`]: crate::Identity::unwrap_stanza
pub fn set_max_work_factor(&mut self, max_log_n: u8) {
self.max_work_factor = max_log_n;
}
}
impl crate::Identity for Identity {
fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
if stanza.tag != SCRYPT_RECIPIENT_TAG {
return None;
}
// Enforce valid and canonical stanza format.
// https://c2sp.org/age#scrypt-recipient-stanza
let (salt, log_n) = match &stanza.args[..] {
[salt, log_n] => match (
base64_arg::<_, SALT_LEN, 18>(salt),
decimal_digit_arg(log_n),
) {
(Some(salt), Some(log_n)) => (salt, log_n),
_ => return Some(Err(DecryptError::InvalidHeader)),
},
_ => return Some(Err(DecryptError::InvalidHeader)),
};
if stanza.body.len() != ENCRYPTED_FILE_KEY_BYTES {
return Some(Err(DecryptError::InvalidHeader));
}
if log_n > self.max_work_factor {
return Some(Err(DecryptError::ExcessiveWork {
required: log_n,
target: self.target_work_factor,
}));
}
let mut inner_salt = [0; SCRYPT_SALT_LABEL.len() + SALT_LEN];
inner_salt[..SCRYPT_SALT_LABEL.len()].copy_from_slice(SCRYPT_SALT_LABEL);
inner_salt[SCRYPT_SALT_LABEL.len()..].copy_from_slice(&salt);
let enc_key = match scrypt(&inner_salt, log_n, self.passphrase.expose_secret()) {
Ok(k) => k,
Err(_) => {
return Some(Err(DecryptError::ExcessiveWork {
required: log_n,
target: self.target_work_factor,
}));
}
};
// This AEAD is not robust, so an attacker could craft a message that decrypts
// under two different keys (meaning two different passphrases) and then use an
// error side-channel in an online decryption oracle to learn if either key is
// correct. This is deemed acceptable because the use case (an online decryption
// oracle) is not recommended, and the security loss is only one bit. This also
// does not bypass any scrypt work, but that work can be precomputed in an online
// oracle scenario.
Some(
aead_decrypt(&enc_key, FILE_KEY_BYTES, &stanza.body)
.map(|mut pt| {
// It's ours!
FileKey::init_with_mut(|file_key| {
file_key.copy_from_slice(&pt);
pt.zeroize();
})
})
.map_err(DecryptError::from),
)
}
}