enc_file 0.6.3

Password-based file encryption tool with a versioned header, AEAD, Argon2id KDF, and streaming mode. Library + CLI + GUI.
Documentation
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
#![forbid(unsafe_code)]
#![doc(
    html_logo_url = "https://raw.githubusercontent.com/ArdentEmpiricist/enc_file/main/assets/logo.png"
)]
//! # enc_file — password-based authenticated encryption for files.
//!
//! `enc_file` is a Rust library for encrypting, decrypting, and hashing files or byte arrays.
//! It supports modern AEAD ciphers (XChaCha20-Poly1305, AES-256-GCM-SIV) with Argon2id key derivation.
//!
//! ## Features
//! - **File and byte array encryption/decryption**
//! - **Streaming encryption** for large files (constant memory usage)
//! - **Multiple AEAD algorithms**: XChaCha20-Poly1305, AES-256-GCM-SIV
//! - **Password-based key derivation** using Argon2id
//! - **Key map management** for named symmetric keys
//! - **Flexible hashing API** with support for BLAKE3, SHA2, SHA3, Blake2b, XXH3, and CRC32
//! - **ASCII armor** for encrypted data (Base64 encoding)
//!
//! ## Example: Encrypt and decrypt a byte array
//! ```no_run
//! use enc_file::{encrypt_bytes, decrypt_bytes, EncryptOptions, AeadAlg};
//! use secrecy::SecretString;
//!
//! let password = SecretString::new("mypassword".into());
//! let opts = EncryptOptions {
//!     alg: AeadAlg::XChaCha20Poly1305,
//!     ..Default::default()
//! };
//!
//! let ciphertext = encrypt_bytes(b"Hello, world!", password.clone(), &opts).unwrap();
//! let plaintext = decrypt_bytes(&ciphertext, password).unwrap();
//! assert_eq!(plaintext, b"Hello, world!");
//! ```
//!
//! ## Example: Hash a file
//! ```no_run
//! use enc_file::{hash_file, HashAlg};
//! use std::path::Path;
//!
//! let digest = hash_file(Path::new("myfile.txt"), HashAlg::Blake3).unwrap();
//! println!("Hash: {}", enc_file::to_hex_lower(&digest));
//! ```
//!
//! See function-level documentation for more details.
//!
//! Safety notes
//! - The crate is not audited or reviewed! Protects data at rest. Does not defend against compromised hosts/side channels.

// Module declarations
mod armor;
mod crypto;
mod file;
mod format;
mod hash;
mod kdf;
mod keymap;
mod streaming;
mod types;

// External dependencies
use secrecy::SecretString;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use zeroize::Zeroize;

// Re-export public types and constants
pub use types::{
    AeadAlg, DEFAULT_CHUNK_SIZE, EncFileError, EncryptOptions, HashAlg, KdfAlg, KdfParams, KeyMap,
};

// Re-export public functions
// Core encryption/decryption API
pub use armor::looks_armored;
pub use file::default_decrypt_output_path;
pub use hash::{
    hash_bytes, hash_bytes_keyed_blake3, hash_file, hash_file_keyed_blake3, to_hex_lower,
};
pub use keymap::{load_keymap, save_keymap};
pub use streaming::{encrypt_file_streaming, validate_chunk_size_for_streaming};

// Core encryption and decryption functions

/// Encrypt a byte slice using an AEAD cipher with a password-derived key.
///
/// This is the simplest way to encrypt in-memory data. A random salt and nonce are
/// generated, and the result includes a self-describing header with all necessary
/// metadata for decryption.
///
/// # Options via `EncryptOptions`
/// - `alg: AeadAlg` — Cipher choice: `XChaCha20Poly1305` (default) or `Aes256GcmSiv`.
/// - `kdf: KdfAlg` — Password KDF. Currently `Argon2id` (default).
/// - `kdf_params: KdfParams` — Argon2id tuning:
///   - `t_cost` (passes/iterations)
///   - `mem_kib` (memory in KiB)
///   - `parallelism` (lanes/threads)
/// - `armor: bool` — Wrap output in ASCII armor (Base64) suitable for copy/paste.
/// - `force: bool` — Overwrite existing output files (file APIs only; ignored by byte APIs).
/// - `stream: bool` — Use streaming/chunked framing for constant memory (file APIs only).
/// - `chunk_size: usize` — Chunk size in bytes (streaming only).
///
/// **Ignored fields for this function:** `force`, `stream`, `chunk_size`.
pub fn encrypt_bytes(
    plaintext: &[u8],
    password: SecretString,
    opts: &EncryptOptions,
) -> Result<Vec<u8>, EncFileError> {
    if opts.stream {
        return Err(EncFileError::Invalid("use streaming APIs for stream mode"));
    }

    let salt = crypto::generate_salt()?;
    let key = kdf::derive_key_argon2id(&password, opts.kdf_params, &salt)?;
    let nonce = crypto::generate_nonce(opts.alg)?;

    let ciphertext = crypto::aead_encrypt(opts.alg, &key, &nonce, plaintext)?;
    let header = format::DiskHeader::new_nonstream(
        opts.alg,
        opts.kdf,
        opts.kdf_params,
        salt,
        nonce,
        ciphertext.len() as u64,
    );

    let mut header_bytes = Vec::new();
    ciborium::ser::into_writer(&header, &mut header_bytes)?;
    let mut out = Vec::new();
    out.extend_from_slice(&(header_bytes.len() as u32).to_le_bytes());
    out.extend_from_slice(&header_bytes);
    out.extend_from_slice(&ciphertext);

    let mut key_z = key;
    crypto::zeroize_key(&mut key_z);

    if opts.armor {
        Ok(armor::armor_encode(&out))
    } else {
        Ok(out)
    }
}

/// Decrypt a byte slice that was produced by [`encrypt_bytes`].
///
/// The function parses the self-describing header, derives the key using the embedded
/// Argon2id parameters, and verifies the AEAD tag before returning the plaintext.
pub fn decrypt_bytes(input: &[u8], password: SecretString) -> Result<Vec<u8>, EncFileError> {
    // Handle ASCII armor first (tail-recursive on the dearmored bytes)
    if armor::looks_armored(input) {
        let bin = armor::dearmor_decode(input)?;
        return decrypt_bytes(&bin, password);
    }

    // Minimal header preflight
    if input.len() < 4 {
        return Err(EncFileError::Malformed);
    }
    let header_len = u32::from_le_bytes(input[0..4].try_into().unwrap()) as usize;
    if input.len() < 4 + header_len {
        return Err(EncFileError::Malformed);
    }

    let header_bytes = &input[4..4 + header_len];
    let body = &input[4 + header_len..];

    let header: format::DiskHeader = ciborium::de::from_reader(header_bytes)?;

    // Validate header
    if header.magic != *format::MAGIC {
        return Err(EncFileError::Malformed);
    }
    if header.version != format::VERSION {
        return Err(EncFileError::UnsupportedVersion(header.version));
    }

    // Map algorithms
    let aead_alg = match header.aead_alg {
        1 => AeadAlg::XChaCha20Poly1305,
        2 => AeadAlg::Aes256GcmSiv,
        o => return Err(EncFileError::UnsupportedAead(o)),
    };
    let kdf_alg = match header.kdf_alg {
        1 => KdfAlg::Argon2id,
        o => return Err(EncFileError::UnsupportedKdf(o)),
    };
    let _ = kdf_alg; // currently only Argon2id is supported

    // Validate header-declared chunk size early (streaming only)
    if let Some(stream) = &header.stream {
        streaming::validate_chunk_size_for_streaming(stream.chunk_size as usize)?;
    }

    // Derive key
    let key = kdf::derive_key_argon2id(&password, header.kdf_params, &header.salt)?;

    // Streaming: parse frames into a Vec<u8>
    if let Some(stream) = &header.stream {
        let pt = streaming::decrypt_stream_into_vec(aead_alg, &key, stream, body)?;
        let mut key_z = key;
        crypto::zeroize_key(&mut key_z);
        return Ok(pt);
    }

    // Non-streaming: body length must match `ct_len` from header
    if body.len() as u64 != header.ct_len {
        return Err(EncFileError::Malformed);
    }
    
    // Validate ciphertext size for security
    crypto::validate_ciphertext_length(header.ct_len)?;
    
    let pt = crypto::aead_decrypt(aead_alg, &key, &header.nonce, body)?;
    let mut key_z = key;
    crypto::zeroize_key(&mut key_z);
    Ok(pt)
}

/// Encrypt a file on disk using the specified options.
///
/// For large files, consider using [`encrypt_file_streaming`] instead to maintain
/// constant memory usage.
pub fn encrypt_file(
    input: &Path,
    output: Option<&Path>,
    password: SecretString,
    opts: EncryptOptions,
) -> Result<std::path::PathBuf, EncFileError> {
    if opts.stream {
        return encrypt_file_streaming(input, output, password, opts);
    }
    
    // Validate file size before reading into memory
    let file_metadata = std::fs::metadata(input)?;
    crypto::validate_file_size(file_metadata.len())?;
    
    let mut data = Vec::new();
    File::open(input)?.read_to_end(&mut data)?;
    let out_bytes = encrypt_bytes(&data, password, &opts)?;
    // Zeroize input plaintext buffer after encryption
    data.zeroize();
    let out_path = file::default_out_path(input, output, "enc");
    if out_path.exists() && !opts.force {
        return Err(EncFileError::Invalid(
            "output exists; use --force to overwrite",
        ));
    }
    file::write_all_atomic(&out_path, &out_bytes, false)?;
    Ok(out_path)
}

/// Decrypt a file on disk that was produced by [`encrypt_file`] or [`encrypt_file_streaming`].
pub fn decrypt_file(
    input: &Path,
    output: Option<&Path>,
    password: SecretString,
) -> Result<std::path::PathBuf, EncFileError> {
    let out_path = file::default_out_path_for_decrypt(input, output);
    if out_path.exists() {
        return Err(EncFileError::Invalid(
            "output exists; use --force to overwrite",
        ));
    }

    let mut input_file = File::open(input)?;
    
    // Read a small buffer to check if the file is armored
    let mut peek_buffer = [0u8; 1024];
    let peek_len = input_file.read(&mut peek_buffer)?;
    let peek_data = &peek_buffer[..peek_len];
    
    // If armored, we need to read the entire file to decode it
    if armor::looks_armored(peek_data) {
        // Reset file position and read everything for armor decoding
        input_file.rewind()?;
        let mut file_data = Vec::new();
        input_file.read_to_end(&mut file_data)?;
        let binary_data = armor::dearmor_decode(&file_data)?;
        
        // Process the decoded binary data in memory
        return decrypt_file_from_binary_data(&binary_data, &out_path, password);
    }
    
    // For binary files, we can be more memory-efficient
    // Reset to beginning and parse header without reading entire file
    input_file.rewind()?;
    
    // Read header length
    let mut header_len_buf = [0u8; 4];
    input_file.read_exact(&mut header_len_buf)?;
    let header_len = u32::from_le_bytes(header_len_buf) as usize;
    
    // Read header
    let mut header_buf = vec![0u8; header_len];
    input_file.read_exact(&mut header_buf)?;
    
    let header: format::DiskHeader = ciborium::de::from_reader(&header_buf[..])?;
    
    // Validate format version
    if header.version != format::VERSION {
        return Err(EncFileError::UnsupportedVersion(header.version));
    }

    // Parse algorithms
    let aead_alg = match header.aead_alg {
        1 => types::AeadAlg::XChaCha20Poly1305,
        2 => types::AeadAlg::Aes256GcmSiv,
        o => return Err(EncFileError::UnsupportedAead(o)),
    };

    let kdf_alg = match header.kdf_alg {
        1 => types::KdfAlg::Argon2id,
        o => return Err(EncFileError::UnsupportedKdf(o)),
    };
    let _ = kdf_alg; // currently only Argon2id is supported

    // Derive key
    let key = kdf::derive_key_argon2id(&password, header.kdf_params, &header.salt)?;

    if let Some(stream_info) = &header.stream {
        // Streaming mode: use constant-memory streaming decryption directly from file
        streaming::validate_chunk_size_for_streaming(stream_info.chunk_size as usize)?;
        
        let out_file = File::create(&out_path)?;
        let mut buffered_out = std::io::BufWriter::with_capacity(64 * 1024, out_file);
        
        streaming::decrypt_stream_to_writer(
            &mut input_file,
            &mut buffered_out,
            aead_alg,
            &key,
            stream_info,
        )?;

        buffered_out.flush()?;
        let out_file = buffered_out.into_inner().map_err(|e| EncFileError::Io(e.into_error()))?;
        out_file.sync_all()?;

        // Zeroize derived key
        let mut key_z = key;
        crypto::zeroize_key(&mut key_z);

        Ok(out_path)
    } else {
        // Non-streaming mode: read the body into memory with buffered I/O
        let expected_body_len = header.ct_len as usize;
        
        // Validate ciphertext size for security
        crypto::validate_ciphertext_length(header.ct_len)?;
        
        let mut body = vec![0u8; expected_body_len];
        
        // Use buffered reader for better performance on large non-streaming files
        let mut buffered_input = std::io::BufReader::with_capacity(64 * 1024, input_file);
        buffered_input.read_exact(&mut body)?;

        let mut pt = crypto::aead_decrypt(aead_alg, &key, &header.nonce, &body)?;
        file::write_all_atomic(&out_path, &pt, false)?;

        // Cheap hardening: wipe decrypted plaintext buffer after writing
        pt.zeroize();

        // Zeroize derived key
        let mut key_z = key;
        crypto::zeroize_key(&mut key_z);

        Ok(out_path)
    }
}

/// Helper function to decrypt from binary data in memory (used for armored files).
fn decrypt_file_from_binary_data(
    binary_data: &[u8],
    out_path: &Path,
    password: SecretString,
) -> Result<PathBuf, EncFileError> {
    // Now we have binary data, proceed with normal decryption logic
    if binary_data.len() < 4 {
        return Err(EncFileError::Malformed);
    }

    let header_len = u32::from_le_bytes(binary_data[0..4].try_into().unwrap()) as usize;
    if binary_data.len() < 4 + header_len {
        return Err(EncFileError::Malformed);
    }

    let header_buf = &binary_data[4..4 + header_len];

    let header: format::DiskHeader = ciborium::de::from_reader(header_buf)?;

    // Validate format version
    if header.version != format::VERSION {
        return Err(EncFileError::UnsupportedVersion(header.version));
    }

    // Parse algorithms
    let aead_alg = match header.aead_alg {
        1 => types::AeadAlg::XChaCha20Poly1305,
        2 => types::AeadAlg::Aes256GcmSiv,
        o => return Err(EncFileError::UnsupportedAead(o)),
    };

    let kdf_alg = match header.kdf_alg {
        1 => types::KdfAlg::Argon2id,
        o => return Err(EncFileError::UnsupportedKdf(o)),
    };
    let _ = kdf_alg; // currently only Argon2id is supported

    // Derive key
    let key = kdf::derive_key_argon2id(&password, header.kdf_params, &header.salt)?;

    let body = &binary_data[4 + header_len..];

    if let Some(stream_info) = &header.stream {
        // Streaming mode: use constant-memory streaming decryption
        streaming::validate_chunk_size_for_streaming(stream_info.chunk_size as usize)?;

        // For streaming with armored data, we need to create a cursor from the body data
        use std::io::Cursor;
        let mut reader = Cursor::new(body);
        let mut out_file = File::create(out_path)?;

        streaming::decrypt_stream_to_writer(
            &mut reader,
            &mut out_file,
            aead_alg,
            &key,
            stream_info,
        )?;

        out_file.sync_all()?;

        // Zeroize derived key
        let mut key_z = key;
        crypto::zeroize_key(&mut key_z);

        Ok(out_path.to_path_buf())
    } else {
        // Non-streaming mode: decrypt the body directly

        // Body length must match `ct_len` from header
        if body.len() as u64 != header.ct_len {
            return Err(EncFileError::Malformed);
        }

        let mut pt = crypto::aead_decrypt(aead_alg, &key, &header.nonce, body)?;
        file::write_all_atomic(out_path, &pt, false)?;

        // Cheap hardening: wipe decrypted plaintext buffer after writing
        pt.zeroize();

        // Zeroize derived key
        let mut key_z = key;
        crypto::zeroize_key(&mut key_z);

        Ok(out_path.to_path_buf())
    }
}

/// Decrypt options for file operations.
#[derive(Clone, Copy, Debug, Default)]
pub struct DecryptOptions {
    /// Allow overwriting an existing output file.
    pub force: bool,
}

// Helper to maintain API compatibility
pub fn persist_tempfile_atomic(
    tmp: tempfile::NamedTempFile,
    out: &Path,
    force: bool,
) -> Result<std::path::PathBuf, EncFileError> {
    file::persist_tempfile_atomic(tmp, out, force)
}

#[cfg(test)]
mod tests {
    use super::*;
    use secrecy::SecretString;

    #[test]
    fn round_trip_small_default() {
        let pw = SecretString::new("pw".into());
        let opts = EncryptOptions::default();

        let ct = encrypt_bytes(b"abc", pw.clone(), &opts).unwrap();
        let pt = decrypt_bytes(&ct, pw).unwrap();
        assert_eq!(pt, b"abc");
    }

    #[test]
    fn wrong_password_fails() {
        let pw1 = SecretString::new("pw1".into());
        let pw2 = SecretString::new("pw2".into());
        let opts = EncryptOptions::default();

        let ct = encrypt_bytes(b"abc", pw1, &opts).unwrap();
        let result = decrypt_bytes(&ct, pw2);
        assert!(result.is_err());
    }

    #[test]
    fn armor_works() {
        use secrecy::SecretString;

        let pw = SecretString::new("pw".into());
        let opts = EncryptOptions::default().with_armor(true);

        let ct = encrypt_bytes(b"abc", pw.clone(), &opts).unwrap();
        assert!(looks_armored(&ct));
        let pt = decrypt_bytes(&ct, pw).unwrap();
        assert_eq!(pt, b"abc");
    }
}