Skip to main content

krypton/
single_file.rs

1//! Single-file encryption (the `.krf` container).
2//!
3//! A `.krf` file is a universal krypton container (see [`crate::container`])
4//! with password-based key derivation (`kind = 1`) and payload type
5//! `file`. The original filename travels inside the authenticated trailer.
6//!
7//! Files are processed in 64 KiB chunks with constant memory usage, so
8//! files of any size can be encrypted without loading them into RAM.
9
10use std::fs::File;
11use std::io::{BufReader, BufWriter, Write};
12use std::path::{Path, PathBuf};
13
14use crate::container::{self, KeyDerivation, PayloadType};
15
16use crate::error::{Error, Result};
17
18/// Encrypts `input` into a `.krf` container.
19///
20/// * `password` - the encryption password.
21/// * `input` - file to encrypt.
22/// * `output` - destination path; when `None` a random-looking hashed name
23///   ending in `.krf` is generated (the original filename never appears on
24///   disk unencrypted).
25///
26/// Returns the path of the written container. Output is produced atomically
27/// (temp file + rename) with owner-only permissions on Unix.
28///
29/// ```no_run
30/// use std::path::Path;
31/// let out = krypton::encrypt_file("correct horse", Path::new("document.pdf"), None).unwrap();
32/// ```
33pub fn encrypt_file(password: &str, input: &Path, output: Option<&Path>) -> Result<PathBuf> {
34    let input_path = input
35        .canonicalize()
36        .map_err(|_| Error::invalid_name(input.display()))?;
37    if !input_path.is_file() {
38        return Err(Error::invalid_name(input.display()));
39    }
40
41    let original_name = input_path
42        .file_name()
43        .map(|n| n.to_string_lossy().to_string())
44        .unwrap_or_else(|| "unknown".into());
45
46    let mut argon_salt = [0u8; container::SALT_LEN];
47    let mut object_salt = [0u8; container::SALT_LEN];
48    crate::crypto::fill_random(&mut argon_salt);
49    crate::crypto::fill_random(&mut object_salt);
50
51    let kd = KeyDerivation::Password {
52        argon_salt,
53        object_salt,
54        params: crate::kdf::KdfParams::new(),
55    };
56
57    let output_path = match output {
58        Some(p) => p.to_path_buf(),
59        None => {
60            // Random-looking name from public randomness; not secret, just opaque.
61            use sha2::{Digest, Sha256};
62            let mut hasher = Sha256::new();
63            hasher.update(argon_salt);
64            hasher.update(object_salt);
65            PathBuf::from(format!("{:x}.krf", hasher.finalize()))
66        }
67    };
68
69    let tmp_path = crate::fsutil::sibling_temp_path(&output_path);
70    let result = (|| -> Result<()> {
71        let mut out_file = File::create(&tmp_path)?;
72        crate::fsutil::restrict_perms(&tmp_path);
73
74        let mut src = File::open(&input_path)?;
75        {
76            let mut w = BufWriter::new(&mut out_file);
77            container::write_container(
78                &mut w,
79                &kd,
80                Some(password),
81                None,
82                PayloadType::File,
83                b"",
84                Some(&mut src),
85                Some(&original_name),
86            )?;
87            w.flush()?;
88        }
89        out_file.sync_all()?;
90        drop(out_file);
91
92        #[cfg(windows)]
93        if output_path.exists() {
94            std::fs::remove_file(&output_path)?;
95        }
96        std::fs::rename(&tmp_path, &output_path)?;
97        crate::fsutil::restrict_perms(&output_path);
98        crate::fsutil::sync_dir(output_path.parent().unwrap_or_else(|| Path::new(".")));
99        Ok(())
100    })();
101
102    match result {
103        Ok(()) => Ok(output_path),
104        Err(e) => {
105            let _ = std::fs::remove_file(&tmp_path);
106            Err(e)
107        }
108    }
109}
110
111/// Decrypts a `.krf` container into `output`.
112///
113/// Returns the original filename recovered from the authenticated trailer.
114///
115/// Security notes:
116/// * The plaintext is written via temp-file + rename; an interrupted
117///   decryption never leaves a half-written file behind.
118/// * The returned filename originates inside the encrypted container. Treat
119///   it as untrusted data; if used to build filesystem paths, pass it through
120///   [`crate::sanitize::sanitize_stored_name`] first.
121pub fn decrypt_file(password: &str, input: &Path, output: &Path) -> Result<String> {
122    let file = BufReader::new(File::open(input)?);
123
124    let tmp_path = crate::fsutil::sibling_temp_path(output);
125    let outcome = (|| -> Result<String> {
126        let tmp = File::create(&tmp_path)?;
127        crate::fsutil::restrict_perms(&tmp_path);
128        let mut sink = BufWriter::new(tmp);
129
130        let trailer = container::read_container(
131            file,
132            Some(password),
133            None,
134            PayloadType::File,
135            b"",
136            |chunk| sink.write_all(chunk).map_err(Error::from),
137        )?;
138        sink.flush()?;
139        drop(sink);
140
141        let name = trailer.name.ok_or(Error::MalformedPayload)?;
142
143        #[cfg(windows)]
144        if output.exists() {
145            std::fs::remove_file(output)?;
146        }
147        std::fs::rename(&tmp_path, output)?;
148        crate::fsutil::sync_dir(output.parent().unwrap_or_else(|| Path::new(".")));
149        Ok(name)
150    })();
151
152    match outcome {
153        Ok(name) => Ok(name),
154        Err(e) => {
155            let _ = std::fs::remove_file(&tmp_path);
156            Err(e)
157        }
158    }
159}