crypto/keys/
age.rs

1// Copyright 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4// https://age-encryption.org/v1
5
6use core::convert::TryFrom;
7
8use aead::KeyInit;
9use base64::{engine::general_purpose::STANDARD_NO_PAD as BASE64, Engine as _};
10use chacha20poly1305::{aead::AeadInPlace, ChaCha20Poly1305};
11use hkdf::Hkdf;
12use hmac::{Hmac, Mac};
13use scrypt::{scrypt, Params as ScryptParams};
14use sha2::Sha256;
15use zeroize::Zeroize;
16
17/// Age decode/decrypt errors.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum DecError {
20    /// Format is not `age-encryption.org`
21    UnknownFormat,
22    /// Version is not `v1`
23    UnsupportedAgeVersion,
24    /// Recipient is not `scrypt`
25    UnsupportedAgeRecipient,
26    /// Failed to parse parts of the header
27    BadAgeFormat,
28    /// Failed to decrypt file key: incorrect password or corrupt header
29    BadFileKey,
30    /// Header MAC is invalid: incorrect password or corrupt header
31    BadHeaderMac,
32    /// Failed to decrypt and verify payload chunk
33    BadChunk,
34    /// Output buffer too small
35    BufferTooSmall { expected: usize, provided: usize },
36    /// Input buffer incorrect (unexpected, too small) length
37    BufferBadLength,
38    /// Work factor during decryption exceeds maximum threshold value
39    WorkFactorTooBig { required: u8, allowed: u8 },
40}
41
42impl From<DecError> for crate::Error {
43    fn from(inner: DecError) -> Self {
44        crate::Error::AgeFormatError(inner)
45    }
46}
47
48/// Age encode/encrypt errors.
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum EncError {
51    /// Output buffer too small
52    BufferTooSmall { expected: usize, provided: usize },
53    /// Randomness generation failed
54    RngFailed,
55}
56
57impl From<EncError> for crate::Error {
58    fn from(err: EncError) -> Self {
59        match err {
60            EncError::BufferTooSmall { expected, provided } => Self::BufferSize {
61                name: "age",
62                needs: expected,
63                has: provided,
64            },
65            EncError::RngFailed => Self::SystemError {
66                call: "getrandom::getrandom",
67                raw_os_error: None,
68            },
69        }
70    }
71}
72
73/// Work factor representation is incorrect (>=64)
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct IncorrectWorkFactor;
76
77impl From<IncorrectWorkFactor> for crate::Error {
78    fn from(_: IncorrectWorkFactor) -> Self {
79        Self::ConvertError {
80            from: "u8",
81            to: "WorkFactor",
82        }
83    }
84}
85
86const WORK_FACTOR_MAX_VALUE: usize = core::mem::size_of::<usize>() * 8;
87
88// header with 1-digit work factor
89const SCRYPT_MIN_HEADER_LEN: usize = 149;
90
91// header with 2-digit work factor
92const SCRYPT_MAX_HEADER_LEN: usize = 150;
93
94const SALT_BASE64_LEN: usize = 22;
95
96const ENCRYPTED_FILE_KEY_BASE64_LEN: usize = 43;
97
98const MAC_BASE64_LEN: usize = 43;
99
100/// Wrap key is derived from password & salt via scrypt KDF.
101/// Wrap key is used to protect file key.
102fn derive_wrap_key(password: &[u8], salt: &[u8; 16], work_factor: u8, wrap_key: &mut [u8; 32]) {
103    // wrap key = scrypt(N = work factor, r = 8, p = 1, dkLen = 32,
104    //     S = "age-encryption.org/v1/scrypt" || salt, P = passphrase)
105    let params = ScryptParams::new(work_factor, 8, 1, 32).unwrap();
106    const SALT_LABEL: &[u8; 28] = b"age-encryption.org/v1/scrypt";
107    let mut scrypt_salt = [0_u8; SALT_LABEL.len() + 16];
108    scrypt_salt[..SALT_LABEL.len()].copy_from_slice(SALT_LABEL);
109    scrypt_salt[SALT_LABEL.len()..].copy_from_slice(&salt[..]);
110    scrypt(password, &scrypt_salt[..], &params, &mut wrap_key[..]).expect("wrap_key is the correct length");
111    scrypt_salt.zeroize();
112}
113
114/// File key is encrypted with wrap key via ChaCha20-Poly1305 with zero nonce and empty aad.
115/// Body contains encrypted file key and authentication tag.
116fn enc_file_key(password: &[u8], salt: &[u8; 16], work_factor: u8, file_key: &[u8; 16], body: &mut [u8; 16 + 16]) {
117    let mut wrap_key = [0_u8; 32];
118    derive_wrap_key(password, salt, work_factor, &mut wrap_key);
119    // body+tag = ChaCha20-Poly1305(key = wrap key, plaintext = file key)
120    let c = ChaCha20Poly1305::new(&wrap_key.into());
121    wrap_key.zeroize();
122    body[..16].copy_from_slice(&file_key[..]);
123    let tag = c
124        .encrypt_in_place_detached(&[0; 12].into(), &[], &mut body[..16])
125        .expect("the ChaCha20 block counter doesn't overflow");
126    body[16..].copy_from_slice(&tag);
127}
128
129/// Body contains encrypted file key and authentication tag.
130/// Encrypted file key is decrypted with wrap key via ChaCha20-Poly1305 with zero nonce and empty aad.
131fn dec_file_key(
132    password: &[u8],
133    salt: &[u8; 16],
134    work_factor: u8,
135    body: &[u8],
136    file_key: &mut [u8; 16],
137) -> Result<(), DecError> {
138    let mut wrap_key = [0_u8; 32];
139    derive_wrap_key(password, salt, work_factor, &mut wrap_key);
140    // body+tag = ChaCha20-Poly1305(key = wrap key, plaintext = file key)
141    let c = ChaCha20Poly1305::new(&wrap_key.into());
142    wrap_key.zeroize();
143    file_key.copy_from_slice(&body[..16]);
144    let mut tag = [0_u8; 16];
145    tag.copy_from_slice(&body[16..32]);
146    let r = c
147        .decrypt_in_place_detached(&[0; 12].into(), &[], file_key, &tag.into())
148        .map_err(|_| DecError::BadFileKey);
149    if r.is_err() {
150        file_key.zeroize();
151    }
152    tag.zeroize();
153    r
154}
155
156/// Mac key is derived from file key without salt via HKDF with SHA256.
157/// Mac key is used to authenticate header.
158fn derive_hmac_key(file_key: &[u8; 16], hmac_key: &mut [u8; 32]) {
159    // mac key = HKDF-SHA-256(ikm = file key, salt = none, info = "header")
160    Hkdf::<Sha256>::new(None, &file_key[..])
161        .expand(b"header", &mut hmac_key[..])
162        .expect("file_key and hmac_key are the correct length");
163}
164
165/// Payload key is derived from file key with nonce as salt via HKDF with SHA256.
166/// Payload key is used to encrypt payload chunks.
167fn derive_payload_key(file_key: &[u8; 16], nonce: &[u8; 16], payload_key: &mut [u8; 32]) {
168    // payload key = HKDF-SHA-256(ikm = file key, salt = nonce, info = "payload")
169    Hkdf::<Sha256>::new(Some(&nonce[..]), &file_key[..])
170        .expand(b"payload", &mut payload_key[..])
171        .expect("file_key and payload_key are the correct length");
172}
173
174/// Compute header MAC with mac key derived from file key.
175fn mac_header(file_key: &[u8; 16], header: &[u8], mac: &mut [u8; 32]) {
176    let mut hmac_key = [0_u8; 32];
177    derive_hmac_key(file_key, &mut hmac_key);
178    let mut hmac = <Hmac<Sha256> as Mac>::new_from_slice(&hmac_key[..]).unwrap();
179    hmac_key.zeroize();
180    // exclude the last ' ' after '---'
181    hmac.update(header);
182    mac.copy_from_slice(&hmac.finalize().into_bytes());
183}
184
185/// Verify header MAC with mac key derived from file key.
186fn verify_mac_header(file_key: &[u8; 16], header: &[u8], mac: &[u8]) -> Result<(), DecError> {
187    let mut hmac_key = [0_u8; 32];
188    derive_hmac_key(file_key, &mut hmac_key);
189    let mut hmac = <Hmac<Sha256> as Mac>::new_from_slice(&hmac_key[..]).unwrap();
190    hmac_key.zeroize();
191    // exclude the last ' ' after '---'
192    hmac.update(header);
193    hmac.verify_slice(&mac[..32]).map_err(|_| DecError::BadHeaderMac)
194}
195
196/// Length of header in bytes depends only on work factor.
197/// The rest of the header is fixed-length.
198const fn header_len(work_factor: u8) -> usize {
199    // with 10 <= work_factor < 64 the header is fixed-length -- 150 bytes
200    debug_assert!(work_factor < 64);
201    SCRYPT_MIN_HEADER_LEN + if work_factor < 10 { 0 } else { 1 }
202}
203
204/// Encode header given password, salt, file key and work factor.
205///
206/// Arguments:
207/// * `password` -- secret string
208/// * `file_key` -- random 128-bit key used to derive other keys and compute MACs
209/// * `salt` -- 16-byte random salt
210/// * `work_factor` -- base-2 logarithm of scrypt work factor in decimal, `10 <= work_factor < 64`; work factor is
211///   2-digit so that the header is fixed-length
212///
213/// Return:
214/// * Length of the encoded header.
215fn enc_header(password: &[u8], salt: &[u8; 16], file_key: &[u8; 16], work_factor: u8, header: &mut [u8]) -> usize {
216    let mut i = 0_usize;
217    debug_assert!(header_len(work_factor) <= header.len());
218
219    // 1. AGE prefix
220    // 2. version
221    // 3. scrypt recipient stanza
222    let b = b"age-encryption.org/v1\n-> scrypt ";
223    header[i..i + b.len()].copy_from_slice(b);
224    i += b.len();
225
226    // 4. scrypt base64-encoded salt
227    let b = BASE64.encode_slice(salt, &mut header[i..]).unwrap();
228    debug_assert_eq!(SALT_BASE64_LEN, b);
229    i += b;
230
231    // 5. 1 or 2 decimal digit work factor
232    header[i] = b' ';
233    i += 1;
234    if 10 <= work_factor {
235        header[i] = b'0' + work_factor / 10;
236        i += 1;
237    }
238    header[i] = b'0' + work_factor % 10;
239    i += 1;
240    header[i] = b'\n';
241    i += 1;
242
243    // encrypt file key
244    let mut body = [0_u8; 16 + 16];
245    enc_file_key(password, salt, work_factor, file_key, &mut body);
246
247    // 6. base64-encoded encrypted file key
248    let b = BASE64.encode_slice(body, &mut header[i..]).unwrap();
249    debug_assert_eq!(ENCRYPTED_FILE_KEY_BASE64_LEN, b);
250    i += b;
251
252    // 7. final delimiter before MAC
253    let b = b"\n--- ";
254    header[i..i + b.len()].copy_from_slice(b);
255    i += b.len();
256
257    // MAC computed over the entire header up to and including '---'
258    let mut mac = [0_u8; 32];
259    // exclude the last ' ' after '---'
260    mac_header(file_key, &header[..i - 1], &mut mac);
261
262    // 8. base64-encoded MAC
263    let b = BASE64.encode_slice(mac, &mut header[i..]).unwrap();
264    debug_assert_eq!(MAC_BASE64_LEN, b);
265    i += b;
266
267    // 9. final new-line
268    header[i] = b'\n';
269    i += 1;
270
271    // 10. binary encrypted payload
272
273    i
274}
275
276/// Helper condition checker for decoding.
277#[inline]
278fn guard<E>(expr: bool, err: E) -> Result<(), E> {
279    if expr {
280        Ok(())
281    } else {
282        Err(err)
283    }
284}
285
286/// Decode header given password and decrypt file key.
287/// Length of decoded header is returned, or error.
288fn dec_header(password: &[u8], max_work_factor: u8, header: &[u8], file_key: &mut [u8; 16]) -> Result<usize, DecError> {
289    let mut i = 0_usize;
290    guard(header.len() >= SCRYPT_MIN_HEADER_LEN, DecError::BufferBadLength)?;
291
292    // 1. AGE prefix
293    let b = b"age-encryption.org/";
294    guard(header[i..i + b.len()] == b[..], DecError::UnknownFormat)?;
295    i += b.len();
296
297    // 2. version
298    let b = b"v1\n";
299    guard(header[i..i + b.len()] == b[..], DecError::UnsupportedAgeVersion)?;
300    i += b.len();
301
302    // 3. scrypt recipient stanza
303    let b = b"-> scrypt ";
304    guard(header[i..i + b.len()] == b[..], DecError::UnsupportedAgeRecipient)?;
305    i += b.len();
306
307    // 4. scrypt base64-encoded salt
308    // extra 2 bytes for base64 decoding
309    let mut salt2 = [0_u8; 16 + 2];
310    let b = BASE64
311        .decode_slice(&header[i..i + SALT_BASE64_LEN], &mut salt2)
312        .map_err(|_| DecError::BadAgeFormat)?;
313    guard(16 == b, DecError::BadAgeFormat)?;
314    i += SALT_BASE64_LEN;
315    let mut salt = [0_u8; 16];
316    salt.copy_from_slice(&salt2[..16]);
317
318    // 5. 1 or 2 decimal digit work factor
319    let mut work_factor;
320    guard(header[i] == b' ', DecError::BadAgeFormat)?;
321    i += 1;
322    guard(char::from(header[i]).is_ascii_digit(), DecError::BadAgeFormat)?;
323    work_factor = header[i] - b'0';
324    i += 1;
325    if char::from(header[i]).is_ascii_digit() {
326        guard(header.len() >= SCRYPT_MAX_HEADER_LEN, DecError::BufferBadLength)?;
327
328        work_factor *= 10;
329        work_factor += header[i] - b'0';
330        i += 1;
331    }
332    guard(header[i] == b'\n', DecError::BadAgeFormat)?;
333    i += 1;
334    guard(
335        work_factor <= max_work_factor,
336        DecError::WorkFactorTooBig {
337            required: work_factor,
338            allowed: max_work_factor,
339        },
340    )?;
341
342    // 6. base64-encoded encrypted file key
343    // extra 2 bytes for base64 decoding
344    let mut body2 = [0_u8; 16 + 16 + 2];
345    let b = BASE64
346        .decode_slice(&header[i..i + ENCRYPTED_FILE_KEY_BASE64_LEN], &mut body2[..])
347        .map_err(|_| DecError::BadAgeFormat)?;
348    guard(16 + 16 == b, DecError::BadAgeFormat)?;
349    i += ENCRYPTED_FILE_KEY_BASE64_LEN;
350
351    // decrypt file key
352    dec_file_key(password, &salt, work_factor, &body2, file_key)?;
353
354    // 7. final delimiter before MAC
355    let b = b"\n--- ";
356    guard(header[i..i + b.len()] == b[..], DecError::BadAgeFormat)?;
357    i += b.len();
358
359    // 8. base64-encoded MAC
360    // extra 2 bytes for base64 decoding
361    let mut mac2 = [0_u8; 32 + 2];
362    let b = BASE64
363        .decode_slice(&header[i..i + MAC_BASE64_LEN], &mut mac2)
364        .map_err(|_| DecError::BadAgeFormat)?;
365    guard(32 == b, DecError::BadAgeFormat)?;
366
367    // MAC computed over the entire header up to and including '---'
368    // exclude the last ' ' after '---'
369    verify_mac_header(file_key, &header[..i - 1], &mac2)?;
370    i += MAC_BASE64_LEN;
371
372    // 9. final new-line
373    guard(header[i] == b'\n', DecError::BadAgeFormat)?;
374    i += 1;
375
376    // 10. binary encrypted payload
377
378    Ok(i)
379}
380
381/// Nonce increment. Will never overflow in practice.
382fn inc_nonce(nonce: &mut [u8; 12]) {
383    for n in nonce[..11].iter_mut().rev() {
384        *n = n.wrapping_add(1);
385        if *n != 0 {
386            break;
387        }
388    }
389}
390
391/// Encrypt payload chunk with payload key & nonce via ChaCha20-Poly1305.
392fn enc_chunk(c: &ChaCha20Poly1305, nonce: &[u8; 12], plain_chunk: &[u8], cipher_chunk: &mut [u8]) {
393    debug_assert_eq!(plain_chunk.len() + 16, cipher_chunk.len());
394    debug_assert!(plain_chunk.len() <= 64 * 1024);
395
396    // cipher chunk = ChaCha20-Poly1305(key = payload key, plaintext = plain chunk)
397    cipher_chunk[..plain_chunk.len()].copy_from_slice(plain_chunk);
398    let tag = c
399        .encrypt_in_place_detached(nonce.into(), &[], &mut cipher_chunk[..plain_chunk.len()])
400        .expect("the ChaCha20 block counter doesn't overflow");
401    cipher_chunk[plain_chunk.len()..].copy_from_slice(&tag);
402}
403
404/// Decrypt and verify payload chunk with payload key & nonce via ChaCha20-Poly1305.
405fn dec_chunk(
406    c: &ChaCha20Poly1305,
407    nonce: &[u8; 12],
408    cipher_chunk: &[u8],
409    plain_chunk: &mut [u8],
410) -> Result<(), DecError> {
411    debug_assert!(plain_chunk.len() <= 64 * 1024);
412    debug_assert_eq!(plain_chunk.len() + 16, cipher_chunk.len());
413
414    // cipher chunk = ChaCha20-Poly1305(key = payload key, plaintext = plain chunk)
415    plain_chunk.copy_from_slice(&cipher_chunk[..plain_chunk.len()]);
416    let mut tag = [0_u8; 16];
417    tag.copy_from_slice(&cipher_chunk[plain_chunk.len()..]);
418    let r = c
419        .decrypt_in_place_detached(nonce.into(), &[], plain_chunk, &tag.into())
420        .map_err(|_| DecError::BadChunk);
421    if r.is_err() {
422        plain_chunk.zeroize();
423    }
424    tag.zeroize();
425    r
426}
427
428/// Total length of ciphertext with nonce and authentication tags depending on plaintext length.
429const fn enc_payload_len(plaintext_len: usize) -> usize {
430    let num_chunks = if plaintext_len == 0 {
431        1
432    } else {
433        (plaintext_len - 1) / (64 * 1024) + 1
434    };
435    16 + num_chunks * 16 + plaintext_len
436}
437
438/// The length of plaintext depending on ciphertext length.
439/// Note, not all ciphertext lengths are valid.
440pub const fn dec_payload_len(ciphertext_len: usize) -> Option<usize> {
441    if ciphertext_len < 16 {
442        // no 16-byte nonce
443        None
444    } else {
445        let r = (ciphertext_len - 16) % (64 * 1024 + 16);
446        let q = (ciphertext_len - 16) / (64 * 1024 + 16);
447        if ((0 == q || 0 < r) && r < 16) || (0 < q && r == 16) {
448            // no 16-byte tag in the last chunk, or
449            // no empty last block allowed except for the first one
450            None
451        } else {
452            let num_chunks = q + if 0 < r { 1 } else { 0 };
453            Some(ciphertext_len - 16 - num_chunks * 16)
454        }
455    }
456}
457
458/// Encrypt the whole payload with file key and nonce.
459/// Nonce is used to derive payload key and is prepended to ciphertext.
460/// Note, nonce used to encrypt payload chunks is 12-byte counter.
461fn enc_payload(file_key: &[u8; 16], nonce: &[u8; 16], mut plaintext: &[u8], ciphertext: &mut [u8]) {
462    let mut i = 0_usize;
463
464    debug_assert_eq!(enc_payload_len(plaintext.len()), ciphertext.len());
465
466    ciphertext[i..i + 16].copy_from_slice(nonce);
467    i += 16;
468
469    let mut payload_key = [0_u8; 32];
470    derive_payload_key(file_key, nonce, &mut payload_key);
471    let c = ChaCha20Poly1305::new(&payload_key.into());
472    payload_key.zeroize();
473
474    // chunk encryption nonce; don't mix up with payload key derivation nonce
475    let mut nonce = [0_u8; 12];
476
477    loop {
478        let s = core::cmp::min(64 * 1024, plaintext.len());
479        if plaintext.len() == s {
480            nonce[11] = 0x01;
481        }
482        enc_chunk(&c, &nonce, &plaintext[..s], &mut ciphertext[i..i + s + 16]);
483        plaintext = &plaintext[s..];
484        i += s + 16;
485        if plaintext.is_empty() {
486            break;
487        }
488        inc_nonce(&mut nonce);
489    }
490}
491
492/// Decrypt the whole payload with file key and nonce.
493/// Nonce is taken from the ciphertext.
494/// Note, nonce used to encrypt payload chunks is 12-byte counter.
495fn dec_payload(file_key: &[u8; 16], mut ciphertext: &[u8], plaintext: &mut [u8]) -> Result<usize, DecError> {
496    let mut i = 0_usize;
497    // payload key derivation nonce
498    let mut nonce = [0_u8; 16];
499
500    if let Some(plaintext_len) = dec_payload_len(ciphertext.len()) {
501        guard(
502            plaintext_len <= plaintext.len(),
503            DecError::BufferTooSmall {
504                expected: plaintext_len,
505                provided: plaintext.len(),
506            },
507        )?;
508        debug_assert_eq!(enc_payload_len(plaintext_len), ciphertext.len());
509    } else {
510        guard(false, DecError::BufferBadLength)?;
511    }
512
513    nonce.copy_from_slice(&ciphertext[..16]);
514    ciphertext = &ciphertext[16..];
515
516    let mut payload_key = [0_u8; 32];
517    derive_payload_key(file_key, &nonce, &mut payload_key);
518    let c = ChaCha20Poly1305::new(&payload_key.into());
519    payload_key.zeroize();
520
521    // chunk encryption nonce
522    let mut nonce = [0_u8; 12];
523
524    let r = loop {
525        let s = core::cmp::min(64 * 1024 + 16, ciphertext.len());
526        if ciphertext.len() == s {
527            nonce[11] = 0x01;
528        }
529        let r = dec_chunk(&c, &nonce, &ciphertext[..s], &mut plaintext[i..i + s - 16]);
530        if r.is_err() {
531            break r;
532        }
533        ciphertext = &ciphertext[s..];
534        i += s - 16;
535        if ciphertext.is_empty() {
536            break Ok(());
537        }
538
539        inc_nonce(&mut nonce);
540    };
541
542    if r.is_err() {
543        plaintext.zeroize();
544    }
545    r.map(|_| i)
546}
547
548/// Safety wrapper for work factor representation.
549#[derive(Clone, Copy, Debug, PartialEq, Eq)]
550pub struct WorkFactor(u8);
551
552impl WorkFactor {
553    /// Unchecked constructor.
554    pub const fn new(work_factor: u8) -> Self {
555        assert!(
556            (work_factor as usize) < WORK_FACTOR_MAX_VALUE,
557            "incorrect age work factor"
558        );
559        Self(work_factor)
560    }
561}
562
563impl TryFrom<u8> for WorkFactor {
564    type Error = IncorrectWorkFactor;
565    fn try_from(work_factor: u8) -> Result<Self, Self::Error> {
566        if (work_factor as usize) < WORK_FACTOR_MAX_VALUE {
567            Ok(Self(work_factor))
568        } else {
569            Err(IncorrectWorkFactor)
570        }
571    }
572}
573
574impl From<WorkFactor> for u8 {
575    fn from(work_factor: WorkFactor) -> u8 {
576        work_factor.0
577    }
578}
579
580/// The total age length including header and body depending on work factor and plaintext length.
581pub const fn enc_len(work_factor: WorkFactor, plaintext_len: usize) -> usize {
582    header_len(work_factor.0) + enc_payload_len(plaintext_len)
583}
584
585/// Encode header and encrypt payload given all the secrets and random inputs.
586/// The length of the output can be be computed with `enc_len`.
587///
588/// The crucial security parameter (besides password strength) is `work_factor`.
589/// Too small work factor (<15) will result in weak key derivation.
590/// Too large work factor (>25) will take too long to derive key.
591/// Recommended minimal value is `RECOMMENDED_MINIMUM_ENCRYPT_WORK_FACTOR`.
592/// `work_factor` must be <64.
593pub fn enc(
594    password: &[u8],
595    salt: &[u8; 16],
596    file_key: &[u8; 16],
597    work_factor: WorkFactor,
598    nonce: &[u8; 16],
599    plaintext: &[u8],
600    age: &mut [u8],
601) -> Result<usize, EncError> {
602    let age_len = enc_len(work_factor, plaintext.len());
603    guard(
604        age_len <= age.len(),
605        EncError::BufferTooSmall {
606            expected: age_len,
607            provided: age.len(),
608        },
609    )?;
610    let h = header_len(work_factor.0);
611    enc_header(password, salt, file_key, work_factor.0, &mut age[..h]);
612    enc_payload(file_key, nonce, plaintext, &mut age[h..age_len]);
613    Ok(age_len)
614}
615
616/// Encode header and encrypt payload given all the secrets and random inputs producing a vector.
617///
618/// The crucial security parameter (besides password strength) is `work_factor`.
619/// Too small work factor (<15) will result in weak key derivation.
620/// Too large work factor (>25) will take too long to derive key.
621/// Recommended minimal value is `RECOMMENDED_MINIMUM_ENCRYPT_WORK_FACTOR`.
622/// `work_factor` must be <64.
623#[cfg(feature = "std")]
624pub fn enc_vec(
625    password: &[u8],
626    salt: &[u8; 16],
627    file_key: &[u8; 16],
628    work_factor: WorkFactor,
629    nonce: &[u8; 16],
630    plaintext: &[u8],
631) -> Vec<u8> {
632    let mut age = vec![0u8; enc_len(work_factor, plaintext.len())];
633    let h = enc_header(password, salt, file_key, work_factor.0, &mut age[..]);
634    enc_payload(file_key, nonce, plaintext, &mut age[h..]);
635    age
636}
637
638/// The recommended minimum work factor used by `encrypt`, or roughly 1 sec on modern PC (2023).
639pub const RECOMMENDED_MINIMUM_ENCRYPT_WORK_FACTOR: u8 = 19;
640
641/// Generate random salt, file key, and nonce and use them to protect plaintext in age format.
642///
643/// The crucial security parameter (besides password strength) is `work_factor`.
644/// Too small work factor (<15) will result in weak key derivation.
645/// Too large work factor (>25) will take too long to derive key.
646/// Recommended minimal value is `RECOMMENDED_MINIMUM_ENCRYPT_WORK_FACTOR`.
647/// `work_factor` must be <64.
648#[cfg(feature = "random")]
649pub fn encrypt(password: &[u8], work_factor: WorkFactor, plaintext: &[u8], age: &mut [u8]) -> Result<usize, EncError> {
650    let mut salt = [0_u8; 16];
651    let mut file_key = [0_u8; 16];
652    let mut nonce = [0_u8; 16];
653    crate::utils::rand::fill(&mut salt[..]).map_err(|_| EncError::RngFailed)?;
654    crate::utils::rand::fill(&mut file_key[..]).map_err(|_| EncError::RngFailed)?;
655    crate::utils::rand::fill(&mut nonce[..]).map_err(|_| EncError::RngFailed)?;
656    let r = enc(password, &salt, &file_key, work_factor, &nonce, plaintext, age);
657    nonce.zeroize();
658    file_key.zeroize();
659    salt.zeroize();
660    r
661}
662
663/// Generate random salt, file key, and nonce and use them to protect plaintext in age format producing a vector.
664#[cfg(all(feature = "random", feature = "std"))]
665pub fn encrypt_vec(password: &[u8], work_factor: WorkFactor, plaintext: &[u8]) -> Result<Vec<u8>, EncError> {
666    let mut salt = [0_u8; 16];
667    let mut file_key = [0_u8; 16];
668    let mut nonce = [0_u8; 16];
669    crate::utils::rand::fill(&mut salt[..]).map_err(|_| EncError::RngFailed)?;
670    crate::utils::rand::fill(&mut file_key[..]).map_err(|_| EncError::RngFailed)?;
671    crate::utils::rand::fill(&mut nonce[..]).map_err(|_| EncError::RngFailed)?;
672    let age = enc_vec(password, &salt, &file_key, work_factor, &nonce, plaintext);
673    nonce.zeroize();
674    file_key.zeroize();
675    salt.zeroize();
676    Ok(age)
677}
678
679/// The recommended maximum work factor used by `decrypt`, or roughly 45 sec on modern PC (2023).
680pub const RECOMMENDED_MAXIMUM_DECRYPT_WORK_FACTOR: u8 = 23;
681
682/// Decrypt age format.
683/// The length of the plaintext depends on the header (work factor) and can be approximated as
684/// `dec_payload_len(age.len() - header_len(10)).unwrap()`.
685///
686/// `max_work_factor` parameter limits the amount of computation that the decryptor is willing to spend.
687/// Too large values of work factor in the protected input age can result in DoS.
688pub fn decrypt(password: &[u8], max_work_factor: u8, age: &[u8], plaintext: &mut [u8]) -> Result<usize, DecError> {
689    let mut file_key = [0_u8; 16];
690    let r = dec_header(password, max_work_factor, age, &mut file_key)
691        .and_then(|header_len| dec_payload(&file_key, &age[header_len..], plaintext));
692    file_key.zeroize();
693    r
694}
695
696/// Decrypt age format producing a vector.
697///
698/// `max_work_factor` parameter limits the amount of computation that the decryptor is willing to spend.
699/// Too large values of work factor in the protected input age can result in DoS.
700#[cfg(feature = "std")]
701pub fn decrypt_vec(password: &[u8], max_work_factor: u8, age: &[u8]) -> Result<Vec<u8>, DecError> {
702    let mut file_key = [0_u8; 16];
703    let r = dec_header(password, max_work_factor, age, &mut file_key).and_then(|header_len| {
704        if let Some(plaintext_len) = dec_payload_len(age.len() - header_len) {
705            let mut plaintext = vec![0u8; plaintext_len];
706            let _ = dec_payload(&file_key, &age[header_len..], &mut plaintext[..])?;
707            Ok(plaintext)
708        } else {
709            Err(DecError::BufferBadLength)
710        }
711    });
712    file_key.zeroize();
713    r
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    const K64: usize = 64 * 1024;
721    const TEST_LENS: [usize; 12] = [
722        0,
723        1,
724        K64 - 16,
725        K64 - 1,
726        K64,
727        K64 + 1,
728        K64 + 16,
729        2 * K64 - 16,
730        2 * K64 - 1,
731        2 * K64,
732        2 * K64 + 1,
733        2 * K64 + 16,
734    ];
735
736    #[test]
737    fn test_payload_len() {
738        for len in TEST_LENS {
739            assert_eq!(Some(len), dec_payload_len(enc_payload_len(len)));
740        }
741        assert_eq!(None, dec_payload_len(0));
742        assert_eq!(None, dec_payload_len(15));
743        assert_eq!(None, dec_payload_len(16));
744        assert_eq!(None, dec_payload_len(31));
745        assert_eq!(Some(0), dec_payload_len(32));
746
747        assert_eq!(Some(K64), dec_payload_len(16 + K64 + 16));
748        assert_eq!(None, dec_payload_len(16 + K64 + 16 + 1));
749        assert_eq!(None, dec_payload_len(16 + K64 + 16 + 16));
750        assert_eq!(Some(K64 + 1), dec_payload_len(16 + K64 + 16 + 17));
751
752        assert_eq!(Some(2 * K64), dec_payload_len(16 + 2 * (K64 + 16)));
753        assert_eq!(None, dec_payload_len(16 + 2 * (K64 + 16) + 1));
754        assert_eq!(None, dec_payload_len(16 + 2 * (K64 + 16) + 16));
755        assert_eq!(Some(2 * K64 + 1), dec_payload_len(16 + 2 * (K64 + 16) + 17));
756    }
757
758    #[test]
759    fn test_nonce() {
760        let mut nonce = [0_u8; 12];
761        for i in 1_usize..258_usize {
762            inc_nonce(&mut nonce);
763            assert_eq!(i.to_be_bytes(), &nonce[3..11]);
764        }
765    }
766
767    fn run_header(
768        password: &[u8],
769        salt: &[u8; 16],
770        file_key: &[u8; 16],
771        work_factor: u8,
772        max_work_factor: u8,
773    ) -> Result<(), DecError> {
774        let mut header = [0_u8; SCRYPT_MAX_HEADER_LEN];
775        let h = enc_header(
776            password,
777            salt,
778            file_key,
779            work_factor,
780            &mut header[..header_len(work_factor)],
781        );
782        let mut dec_file_key = [0_u8; 16];
783        let r = dec_header(password, max_work_factor, &header, &mut dec_file_key);
784        if r.is_ok() {
785            assert_eq!(h, r.unwrap());
786            assert_eq!(file_key, &dec_file_key);
787        }
788        r.map(|_| ())
789    }
790
791    #[test]
792    fn test_header() {
793        let password = [0xaa_u8; 1025];
794        let pwd_lens = [0, 1, 33, 65, 1025];
795        let bits = [[0x00_u8; 16], [0xaa_u8; 16], [0xff_u8; 16]];
796        let work_factor = 1_u8;
797
798        // run_header(&password[..0], &bits[2], &bits[2], 10);
799        for pwd_len in pwd_lens {
800            for salt in bits {
801                for file_key in bits {
802                    let r = run_header(&password[..pwd_len], &salt, &file_key, work_factor, work_factor);
803                    assert!(r.is_ok());
804                }
805            }
806        }
807    }
808
809    #[cfg(feature = "std")]
810    fn enc_crate(plaintext: &[u8]) -> Vec<u8> {
811        let password = "password".as_bytes();
812        let work_factor = 1_u8.try_into().unwrap();
813        let salt = [0x11_u8; 16];
814        let file_key = [0x22_u8; 16];
815        let nonce = [0x33_u8; 16];
816        enc_vec(password, &salt, &file_key, work_factor, &nonce, plaintext)
817    }
818
819    #[cfg(feature = "std")]
820    fn enc_rage(plaintext: &[u8]) -> Vec<u8> {
821        use std::io::Write;
822        let password = "password".to_owned().into();
823        let mut age = Vec::new();
824        let mut writer = age::Encryptor::with_user_passphrase(password)
825            .wrap_output(&mut age)
826            .unwrap();
827        writer.write_all(plaintext).unwrap();
828        writer.finish().unwrap();
829        age
830    }
831
832    #[cfg(feature = "std")]
833    fn dec_crate(age: &[u8], max_work_factor: u8) -> Option<Vec<u8>> {
834        decrypt_vec("password".as_bytes(), max_work_factor, age).ok()
835    }
836
837    #[cfg(feature = "std")]
838    fn dec_rage(age: &[u8]) -> Option<Vec<u8>> {
839        use std::io::Read;
840        let pass = "password".to_owned().into();
841        let mut reader = match age::Decryptor::new(age).unwrap() {
842            age::Decryptor::Recipients(_) => panic!("internal error"),
843            age::Decryptor::Passphrase(d) => d.decrypt(&pass, Some(14)).unwrap(),
844        };
845        let mut decrypted = Vec::new();
846        reader.read_to_end(&mut decrypted).ok()?;
847        Some(decrypted)
848    }
849
850    #[cfg(feature = "std")]
851    #[test]
852    fn test_crate_rage() {
853        for text_len in TEST_LENS {
854            let mut plaintext = Vec::new();
855            plaintext.resize(text_len, 0xaa_u8);
856            let decrypted = dec_rage(&enc_crate(&plaintext));
857            assert_eq!(Some(plaintext), decrypted);
858        }
859    }
860
861    #[cfg(feature = "std")]
862    #[test]
863    fn test_crate_crate() {
864        for text_len in TEST_LENS {
865            let mut plaintext = Vec::new();
866            plaintext.resize(text_len, 0xaa_u8);
867            let decrypted = dec_crate(&enc_crate(&plaintext), 1_u8);
868            assert_eq!(Some(plaintext), decrypted);
869        }
870    }
871
872    #[cfg(feature = "std")]
873    #[test]
874    fn test_rage_crate() {
875        for text_len in [0, 1, 64 * 1024 + 1] {
876            let mut plaintext = Vec::new();
877            plaintext.resize(text_len, 0xaa_u8);
878            let max_work_factor = 22_u8;
879            // dec_crate can fail if max_work_factor is too small
880            let decrypted = dec_crate(&enc_rage(&plaintext), max_work_factor);
881            assert_eq!(Some(plaintext), decrypted);
882        }
883    }
884
885    #[test]
886    fn test_err() {
887        const TEXT_LEN: usize = 5;
888        const TEXT_LEN1: usize = TEXT_LEN - 1;
889        let plain = [0xdd_u8; TEXT_LEN];
890        let mut decrypted = [0_u8; TEXT_LEN];
891        const WORK_FACTOR: WorkFactor = WorkFactor::new(1_u8);
892        const AGE_LEN: usize = enc_len(WORK_FACTOR, TEXT_LEN);
893        const AGE_LEN1: usize = AGE_LEN - 1;
894        let mut age = [0_u8; AGE_LEN];
895
896        let salt = [0x11_u8; 16];
897        let file_key = [0x22_u8; 16];
898        let nonce = [0x33_u8; 16];
899
900        let err = enc(
901            b"password",
902            &salt,
903            &file_key,
904            WORK_FACTOR,
905            &nonce,
906            &plain,
907            &mut age[..AGE_LEN1],
908        )
909        .err()
910        .unwrap();
911        assert!(
912            matches!(
913                err,
914                EncError::BufferTooSmall {
915                    expected: AGE_LEN,
916                    provided: AGE_LEN1,
917                }
918            ),
919            "wrong expected error result"
920        );
921
922        assert_eq!(
923            AGE_LEN,
924            enc(b"password", &salt, &file_key, WORK_FACTOR, &nonce, &plain, &mut age).unwrap()
925        );
926
927        let err = decrypt(b"password", 0_u8, &age, &mut decrypted).err().unwrap();
928        assert!(
929            matches!(
930                err,
931                DecError::WorkFactorTooBig {
932                    required: 1_u8,
933                    allowed: 0_u8,
934                }
935            ),
936            "wrong expected error result"
937        );
938
939        let err = decrypt(b"password", 1_u8, &age, &mut decrypted[..TEXT_LEN1])
940            .err()
941            .unwrap();
942        assert!(
943            matches!(
944                err,
945                DecError::BufferTooSmall {
946                    expected: TEXT_LEN,
947                    provided: TEXT_LEN1,
948                }
949            ),
950            "wrong expected error result"
951        );
952
953        assert_eq!(TEXT_LEN, decrypt(b"password", 1_u8, &age, &mut decrypted).unwrap());
954    }
955
956    #[test]
957    fn test_fuzz() {
958        let plain = [0xdd_u8; 5];
959        let mut decrypted = [0_u8; 5];
960        const WORK_FACTOR: WorkFactor = WorkFactor::new(1_u8);
961        let mut age = [0_u8; enc_len(WORK_FACTOR, 5)];
962
963        let salt = [0x11_u8; 16];
964        let file_key = [0x22_u8; 16];
965        let nonce = [0x33_u8; 16];
966        assert!(enc(b"password", &salt, &file_key, WORK_FACTOR, &nonce, &plain, &mut age).is_ok());
967
968        assert!(decrypt(b"password", 1_u8, &age, &mut decrypted).is_ok());
969        assert_eq!(&plain, &decrypted);
970        assert!(decrypt(b"password", 0_u8, &age, &mut decrypted).is_err());
971
972        assert!(decrypt(b"passphrase", 1_u8, &age, &mut decrypted).is_err());
973        for i in 0..age.len() {
974            age[i] ^= 1;
975            assert!(decrypt(b"password", 2_u8, &age, &mut decrypted).is_err());
976            age[i] ^= 1;
977        }
978    }
979}