Compare commits
12
Commits
f44cfc6190
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d6e7d3bcf
|
||
|
|
e899411e65
|
||
|
|
71b3900518
|
||
|
|
7c499c1de0
|
||
|
|
298a42f24b
|
||
|
|
304bdb8eb8
|
||
|
|
792f2f174b
|
||
|
|
7f49d034ae
|
||
|
|
f370beb19f
|
||
|
|
77d3037e98
|
||
|
|
655013f86e
|
||
|
|
2f16e735c3
|
Generated
+15
-273
@@ -62,12 +62,6 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@@ -164,9 +158,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.63"
|
||||
version = "1.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -326,12 +320,6 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -350,7 +338,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fcry"
|
||||
version = "0.12.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"assert_cmd",
|
||||
@@ -358,7 +346,7 @@ dependencies = [
|
||||
"chacha20poly1305",
|
||||
"clap",
|
||||
"crossbeam-channel",
|
||||
"getrandom 0.4.2",
|
||||
"getrandom 0.4.3",
|
||||
"libc",
|
||||
"rlimit",
|
||||
"same-file",
|
||||
@@ -375,12 +363,6 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -404,56 +386,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -469,18 +416,6 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
@@ -493,17 +428,11 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.1"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
@@ -588,16 +517,6 @@ dependencies = [
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -680,12 +599,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -715,19 +628,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
@@ -748,9 +648,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -764,7 +664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
@@ -812,12 +712,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@@ -861,58 +755,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -959,122 +801,22 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fcry"
|
||||
version = "0.12.0"
|
||||
version = "1.0.1"
|
||||
edition = "2024"
|
||||
license = "MIT-0"
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ clippy:
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
test:
|
||||
cargo test --workspace
|
||||
cargo test --workspace --all-targets --all-features
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
imports_layout = "HorizontalVertical"
|
||||
+153
-172
@@ -1,22 +1,35 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce, aead::AeadInPlace};
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::header::{
|
||||
AlgId, FLAG_KEY_COMMITTED, FLAG_LENGTH_COMMITTED, Header, KdfParams, NONCE_PREFIX_LEN, TAG_LEN,
|
||||
VERSION_CURRENT,
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Read, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use crate::pipeline;
|
||||
use crate::policy;
|
||||
use crate::reader::{AheadReader, ReadInfoChunk};
|
||||
use crate::secrets::{SecretBytes32, SecretVec};
|
||||
use crate::utils::*;
|
||||
|
||||
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce, aead::AeadInPlace};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::{
|
||||
error::*,
|
||||
header::{
|
||||
AlgId,
|
||||
FLAG_KEY_COMMITTED,
|
||||
FLAG_LENGTH_COMMITTED,
|
||||
Header,
|
||||
HeaderReadOptions,
|
||||
KdfParams,
|
||||
NONCE_PREFIX_LEN,
|
||||
TAG_LEN,
|
||||
VERSION_CURRENT,
|
||||
},
|
||||
pipeline,
|
||||
policy,
|
||||
reader::{AheadReader, ReadInfoChunk},
|
||||
secrets::{SecretBytes32, SecretVec},
|
||||
utils::*,
|
||||
};
|
||||
|
||||
/// XChaCha20Poly1305 nonce: 24 bytes total. STREAM splits the trailing 5 bytes
|
||||
/// into a 4-byte big-endian counter and a 1-byte "last block" flag.
|
||||
pub(crate) const NONCE_LEN: usize = 24;
|
||||
@@ -104,40 +117,74 @@ pub(crate) fn bump_counter(counter: u32) -> Result<u32, FcryError> {
|
||||
.ok_or_else(|| FcryError::Format("STREAM counter overflow (input too large)".into()))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn encrypt<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
output_file: Option<S>,
|
||||
key: &SecretBytes32,
|
||||
chunk_size: u32,
|
||||
kdf: KdfParams,
|
||||
threads: usize,
|
||||
) -> Result<(), FcryError> {
|
||||
encrypt_with_output_options(
|
||||
input_file,
|
||||
output_file,
|
||||
key,
|
||||
chunk_size,
|
||||
kdf,
|
||||
threads,
|
||||
&OutSinkOptions::default(),
|
||||
)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EncryptOptions {
|
||||
pub input_file: Option<PathBuf>,
|
||||
pub output_file: Option<PathBuf>,
|
||||
pub chunk_size: u32,
|
||||
pub threads: usize,
|
||||
pub output: OutputOptions,
|
||||
}
|
||||
|
||||
pub fn encrypt_with_output_options<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
output_file: Option<S>,
|
||||
impl Default for EncryptOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input_file: None,
|
||||
output_file: None,
|
||||
chunk_size: DEFAULT_CHUNK_SIZE,
|
||||
threads: policy::normalize_worker_threads(None).0,
|
||||
output: OutputOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DecryptOptions {
|
||||
pub input_file: Option<PathBuf>,
|
||||
pub output_file: Option<PathBuf>,
|
||||
pub threads: usize,
|
||||
pub max_argon_memory_mib: u32,
|
||||
pub output: OutputOptions,
|
||||
}
|
||||
|
||||
impl Default for DecryptOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input_file: None,
|
||||
output_file: None,
|
||||
threads: policy::normalize_worker_threads(None).0,
|
||||
max_argon_memory_mib: policy::default_argon_decrypt_cap_mib(),
|
||||
output: OutputOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DecryptRangeOptions {
|
||||
pub input_file: PathBuf,
|
||||
pub output_file: Option<PathBuf>,
|
||||
pub offset: u64,
|
||||
pub length: u64,
|
||||
pub max_argon_memory_mib: u32,
|
||||
pub output: OutputOptions,
|
||||
}
|
||||
|
||||
pub fn encrypt(
|
||||
options: &EncryptOptions,
|
||||
key: &SecretBytes32,
|
||||
chunk_size: u32,
|
||||
kdf: KdfParams,
|
||||
threads: usize,
|
||||
output_options: &OutSinkOptions,
|
||||
) -> Result<(), FcryError> {
|
||||
let chunk_sz = policy::validate_chunk_size(chunk_size)?;
|
||||
let input = open_input(input_file)?;
|
||||
let chunk_sz = policy::validate_chunk_size(options.chunk_size)?;
|
||||
let input = open_input(options.input_file.as_deref())?;
|
||||
let plaintext_length = input.length;
|
||||
let mut f_plain = AheadReader::from(input.reader, chunk_sz);
|
||||
let mut f_encrypted = OutSink::open_with_options(output_file, output_options)?;
|
||||
let mut f_encrypted = OutSink::open_with_options(
|
||||
OutSinkPaths {
|
||||
output_file: options.output_file.as_deref(),
|
||||
input_file: options.input_file.as_deref(),
|
||||
},
|
||||
&options.output,
|
||||
)?;
|
||||
|
||||
let mut nonce_prefix = [0u8; NONCE_PREFIX_LEN];
|
||||
getrandom::fill(&mut nonce_prefix)?;
|
||||
@@ -151,7 +198,7 @@ pub fn encrypt_with_output_options<S: AsRef<str>>(
|
||||
version: VERSION_CURRENT,
|
||||
alg: AlgId::XChaCha20Poly1305,
|
||||
flags,
|
||||
chunk_size,
|
||||
chunk_size: options.chunk_size,
|
||||
kdf,
|
||||
nonce_prefix,
|
||||
plaintext_length,
|
||||
@@ -163,7 +210,7 @@ pub fn encrypt_with_output_options<S: AsRef<str>>(
|
||||
|
||||
let aead = build_aead(key);
|
||||
|
||||
if threads > 1 {
|
||||
if options.threads > 1 {
|
||||
return pipeline::encrypt_parallel(
|
||||
f_plain,
|
||||
f_encrypted,
|
||||
@@ -171,7 +218,7 @@ pub fn encrypt_with_output_options<S: AsRef<str>>(
|
||||
aad,
|
||||
nonce_prefix,
|
||||
chunk_sz,
|
||||
threads,
|
||||
options.threads,
|
||||
plaintext_length,
|
||||
);
|
||||
}
|
||||
@@ -182,7 +229,7 @@ pub fn encrypt_with_output_options<S: AsRef<str>>(
|
||||
|
||||
loop {
|
||||
match f_plain.read_ahead(&mut buf)? {
|
||||
ReadInfoChunk::Normal(_) => {
|
||||
ReadInfoChunk::Normal => {
|
||||
let nonce = make_nonce(&nonce_prefix, counter, false);
|
||||
aead.encrypt_in_place(&nonce, &aad, &mut *buf)?;
|
||||
f_encrypted.write_all(&buf)?;
|
||||
@@ -225,55 +272,18 @@ pub fn encrypt_with_output_options<S: AsRef<str>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn decrypt<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
output_file: Option<S>,
|
||||
pub fn decrypt(
|
||||
options: &DecryptOptions,
|
||||
raw_key: Option<&SecretBytes32>,
|
||||
passphrase: Option<&SecretVec>,
|
||||
threads: usize,
|
||||
) -> Result<(), FcryError> {
|
||||
decrypt_with_argon_cap(
|
||||
input_file,
|
||||
output_file,
|
||||
raw_key,
|
||||
passphrase,
|
||||
threads,
|
||||
policy::default_argon_decrypt_cap_mib(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn decrypt_with_argon_cap<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
output_file: Option<S>,
|
||||
raw_key: Option<&SecretBytes32>,
|
||||
passphrase: Option<&SecretVec>,
|
||||
threads: usize,
|
||||
max_argon_memory_mib: u32,
|
||||
) -> Result<(), FcryError> {
|
||||
decrypt_with_output_options(
|
||||
input_file,
|
||||
output_file,
|
||||
raw_key,
|
||||
passphrase,
|
||||
threads,
|
||||
max_argon_memory_mib,
|
||||
&OutSinkOptions::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decrypt_with_output_options<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
output_file: Option<S>,
|
||||
raw_key: Option<&SecretBytes32>,
|
||||
passphrase: Option<&SecretVec>,
|
||||
threads: usize,
|
||||
max_argon_memory_mib: u32,
|
||||
output_options: &OutSinkOptions,
|
||||
) -> Result<(), FcryError> {
|
||||
let mut reader = open_input(input_file)?.reader;
|
||||
let header = Header::read_with_argon_cap(&mut reader, max_argon_memory_mib)?;
|
||||
let mut reader = open_input(options.input_file.as_deref())?.reader;
|
||||
let header = Header::read_with_options(
|
||||
&mut reader,
|
||||
HeaderReadOptions {
|
||||
max_argon_memory_mib: options.max_argon_memory_mib,
|
||||
},
|
||||
)?;
|
||||
let aad = Arc::new(header.encode());
|
||||
|
||||
let key = derive_key(&header.kdf, raw_key, passphrase)?;
|
||||
@@ -283,11 +293,17 @@ pub fn decrypt_with_output_options<S: AsRef<str>>(
|
||||
let cipher_chunk = policy::cipher_chunk_len(chunk_sz)?;
|
||||
|
||||
let mut f_encrypted = AheadReader::from(reader, cipher_chunk);
|
||||
let mut f_plain = OutSink::open_with_options(output_file, output_options)?;
|
||||
let mut f_plain = OutSink::open_with_options(
|
||||
OutSinkPaths {
|
||||
output_file: options.output_file.as_deref(),
|
||||
input_file: options.input_file.as_deref(),
|
||||
},
|
||||
&options.output,
|
||||
)?;
|
||||
|
||||
let aead = build_aead(&key);
|
||||
|
||||
if threads > 1 {
|
||||
if options.threads > 1 {
|
||||
return pipeline::decrypt_parallel(
|
||||
f_encrypted,
|
||||
f_plain,
|
||||
@@ -295,7 +311,7 @@ pub fn decrypt_with_output_options<S: AsRef<str>>(
|
||||
aad,
|
||||
header.nonce_prefix,
|
||||
cipher_chunk,
|
||||
threads,
|
||||
options.threads,
|
||||
header.plaintext_length,
|
||||
);
|
||||
}
|
||||
@@ -306,7 +322,7 @@ pub fn decrypt_with_output_options<S: AsRef<str>>(
|
||||
|
||||
loop {
|
||||
match f_encrypted.read_ahead(&mut buf)? {
|
||||
ReadInfoChunk::Normal(_) => {
|
||||
ReadInfoChunk::Normal => {
|
||||
let nonce = make_nonce(&header.nonce_prefix, counter, false);
|
||||
aead.decrypt_in_place(&nonce, &aad, &mut *buf)?;
|
||||
f_plain.write_all(&buf)?;
|
||||
@@ -348,65 +364,22 @@ pub fn decrypt_with_output_options<S: AsRef<str>>(
|
||||
/// whose header has `FLAG_LENGTH_COMMITTED` set, so we know exactly where
|
||||
/// each ciphertext chunk lives and which chunk is the last (its nonce uses
|
||||
/// the STREAM last-block flag).
|
||||
#[allow(dead_code)]
|
||||
pub fn decrypt_range<S: AsRef<str>>(
|
||||
input_file: &str,
|
||||
output_file: Option<S>,
|
||||
pub fn decrypt_range(
|
||||
options: &DecryptRangeOptions,
|
||||
raw_key: Option<&SecretBytes32>,
|
||||
passphrase: Option<&SecretVec>,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
) -> Result<(), FcryError> {
|
||||
decrypt_range_with_argon_cap(
|
||||
input_file,
|
||||
output_file,
|
||||
raw_key,
|
||||
passphrase,
|
||||
offset,
|
||||
length,
|
||||
policy::default_argon_decrypt_cap_mib(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn decrypt_range_with_argon_cap<S: AsRef<str>>(
|
||||
input_file: &str,
|
||||
output_file: Option<S>,
|
||||
raw_key: Option<&SecretBytes32>,
|
||||
passphrase: Option<&SecretVec>,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
max_argon_memory_mib: u32,
|
||||
) -> Result<(), FcryError> {
|
||||
decrypt_range_with_output_options(
|
||||
input_file,
|
||||
output_file,
|
||||
raw_key,
|
||||
passphrase,
|
||||
offset,
|
||||
length,
|
||||
max_argon_memory_mib,
|
||||
&OutSinkOptions::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn decrypt_range_with_output_options<S: AsRef<str>>(
|
||||
input_file: &str,
|
||||
output_file: Option<S>,
|
||||
raw_key: Option<&SecretBytes32>,
|
||||
passphrase: Option<&SecretVec>,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
max_argon_memory_mib: u32,
|
||||
output_options: &OutSinkOptions,
|
||||
) -> Result<(), FcryError> {
|
||||
if length == 0 {
|
||||
if options.length == 0 {
|
||||
return Err(FcryError::Format("--length 0 is not allowed".into()));
|
||||
}
|
||||
let file = File::open(input_file)?;
|
||||
let file = File::open(&options.input_file)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let header = Header::read_with_argon_cap(&mut reader, max_argon_memory_mib)?;
|
||||
let header = Header::read_with_options(
|
||||
&mut reader,
|
||||
HeaderReadOptions {
|
||||
max_argon_memory_mib: options.max_argon_memory_mib,
|
||||
},
|
||||
)?;
|
||||
let aad = header.encode();
|
||||
let header_len = aad.len() as u64;
|
||||
|
||||
@@ -416,12 +389,14 @@ pub fn decrypt_range_with_output_options<S: AsRef<str>>(
|
||||
)
|
||||
})?;
|
||||
|
||||
let end = offset
|
||||
.checked_add(length)
|
||||
let end = options
|
||||
.offset
|
||||
.checked_add(options.length)
|
||||
.ok_or_else(|| FcryError::Format("offset + length overflows u64".into()))?;
|
||||
if end > total {
|
||||
return Err(FcryError::Format(format!(
|
||||
"range [{offset}, {end}) exceeds plaintext length {total}"
|
||||
"range [{}, {end}) exceeds plaintext length {total}",
|
||||
options.offset
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -451,9 +426,15 @@ pub fn decrypt_range_with_output_options<S: AsRef<str>>(
|
||||
};
|
||||
let last_idx = n_chunks - 1;
|
||||
|
||||
let mut out = OutSink::open_with_options(output_file, output_options)?;
|
||||
let mut out = OutSink::open_with_options(
|
||||
OutSinkPaths {
|
||||
output_file: options.output_file.as_deref(),
|
||||
input_file: Some(&options.input_file),
|
||||
},
|
||||
&options.output,
|
||||
)?;
|
||||
|
||||
let start_chunk = offset / chunk_sz;
|
||||
let start_chunk = options.offset / chunk_sz;
|
||||
let end_chunk = (end - 1) / chunk_sz;
|
||||
|
||||
// Reusable buffer sized to a full chunk + tag.
|
||||
@@ -490,7 +471,7 @@ pub fn decrypt_range_with_output_options<S: AsRef<str>>(
|
||||
// window in absolute bytes and intersect with the requested range.
|
||||
let chunk_start = policy::checked_mul_u64(i, chunk_sz, "plaintext chunk offset")?;
|
||||
let chunk_end = policy::checked_count_add(chunk_start, buf.len(), "plaintext chunk end")?;
|
||||
let lo = offset.max(chunk_start) - chunk_start;
|
||||
let lo = options.offset.max(chunk_start) - chunk_start;
|
||||
let hi = end.min(chunk_end) - chunk_start;
|
||||
out.write_all(&buf[lo as usize..hi as usize])?;
|
||||
}
|
||||
@@ -506,10 +487,12 @@ mod tests {
|
||||
//! must match the bytes that were authenticated when the file was
|
||||
//! written. The v1 test below catches the regression where `encode()`
|
||||
//! used to hard-code the current version on output.
|
||||
use std::fs;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::header::{Header, KdfParams, NONCE_PREFIX_LEN};
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn write_v1_ciphertext(path: &std::path::Path, key: &SecretBytes32, plaintext: &[u8]) {
|
||||
// Build a v1 header by hand: same wire format as v2 with flags=0,
|
||||
@@ -583,14 +566,13 @@ mod tests {
|
||||
let plain: Vec<u8> = (0..200u8).collect();
|
||||
write_v1_ciphertext(&ct, &key, &plain);
|
||||
|
||||
decrypt(
|
||||
Some(ct.to_str().unwrap()),
|
||||
Some(rt.to_str().unwrap()),
|
||||
Some(&key),
|
||||
None,
|
||||
1,
|
||||
)
|
||||
.expect("v1 decrypt should succeed");
|
||||
let options = DecryptOptions {
|
||||
input_file: Some(ct.clone()),
|
||||
output_file: Some(rt.clone()),
|
||||
threads: 1,
|
||||
..DecryptOptions::default()
|
||||
};
|
||||
decrypt(&options, Some(&key), None).expect("v1 decrypt should succeed");
|
||||
let got = fs::read(&rt).unwrap();
|
||||
assert_eq!(got, plain);
|
||||
}
|
||||
@@ -608,14 +590,13 @@ mod tests {
|
||||
let plain: Vec<u8> = (0..200u8).collect();
|
||||
write_v1_ciphertext(&ct, &key, &plain);
|
||||
|
||||
decrypt(
|
||||
Some(ct.to_str().unwrap()),
|
||||
Some(rt.to_str().unwrap()),
|
||||
Some(&key),
|
||||
None,
|
||||
4,
|
||||
)
|
||||
.expect("v1 parallel decrypt should succeed");
|
||||
let options = DecryptOptions {
|
||||
input_file: Some(ct.clone()),
|
||||
output_file: Some(rt.clone()),
|
||||
threads: 4,
|
||||
..DecryptOptions::default()
|
||||
};
|
||||
decrypt(&options, Some(&key), None).expect("v1 parallel decrypt should succeed");
|
||||
assert_eq!(fs::read(&rt).unwrap(), plain);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-2
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
use std::{fmt, io};
|
||||
|
||||
use chacha20poly1305::aead;
|
||||
use std::io;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum FcryError {
|
||||
Io(io::Error),
|
||||
@@ -15,6 +15,34 @@ pub enum FcryError {
|
||||
WrongKey,
|
||||
}
|
||||
|
||||
impl fmt::Display for FcryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Self::Crypto(_) => write!(f, "cryptographic authentication failed"),
|
||||
Self::Rng(e) => write!(f, "randomness error: {e}"),
|
||||
Self::Format(msg) => write!(f, "format error: {msg}"),
|
||||
Self::Kdf(msg) => write!(f, "KDF error: {msg}"),
|
||||
Self::Passphrase(msg) => write!(f, "passphrase error: {msg}"),
|
||||
Self::WrongKey => write!(f, "wrong key or passphrase"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FcryError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(e) => Some(e),
|
||||
Self::Rng(e) => Some(e),
|
||||
Self::Crypto(_)
|
||||
| Self::Format(_)
|
||||
| Self::Kdf(_)
|
||||
| Self::Passphrase(_)
|
||||
| Self::WrongKey => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for FcryError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
FcryError::Io(e)
|
||||
|
||||
+20
-8
@@ -34,8 +34,7 @@
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use crate::error::FcryError;
|
||||
use crate::policy;
|
||||
use crate::{error::FcryError, policy};
|
||||
|
||||
const MAGIC: [u8; 4] = *b"fcry";
|
||||
pub const VERSION_CURRENT: u8 = 3;
|
||||
@@ -152,6 +151,19 @@ pub struct Header {
|
||||
pub key_commitment: Option<[u8; KEY_COMMITMENT_LEN]>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HeaderReadOptions {
|
||||
pub max_argon_memory_mib: u32,
|
||||
}
|
||||
|
||||
impl Default for HeaderReadOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_argon_memory_mib: policy::default_argon_decrypt_cap_mib(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn encode_without_commitment(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(104);
|
||||
@@ -188,14 +200,13 @@ impl Header {
|
||||
self.encode_without_commitment()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn read(r: &mut impl Read) -> Result<Self, FcryError> {
|
||||
Self::read_with_argon_cap(r, policy::default_argon_decrypt_cap_mib())
|
||||
Self::read_with_options(r, HeaderReadOptions::default())
|
||||
}
|
||||
|
||||
pub fn read_with_argon_cap(
|
||||
pub fn read_with_options(
|
||||
r: &mut impl Read,
|
||||
max_argon_memory_mib: u32,
|
||||
options: HeaderReadOptions,
|
||||
) -> Result<Self, FcryError> {
|
||||
let mut magic = [0u8; 4];
|
||||
r.read_exact(&mut magic)?;
|
||||
@@ -238,7 +249,7 @@ impl Header {
|
||||
let mut kdf_id = [0u8; 1];
|
||||
r.read_exact(&mut kdf_id)?;
|
||||
let kdf = KdfParams::read_from(kdf_id[0], r)?;
|
||||
policy::validate_header_kdf(&kdf, max_argon_memory_mib)?;
|
||||
policy::validate_header_kdf(&kdf, options.max_argon_memory_mib)?;
|
||||
|
||||
let mut nonce_prefix = [0u8; NONCE_PREFIX_LEN];
|
||||
r.read_exact(&mut nonce_prefix)?;
|
||||
@@ -274,9 +285,10 @@ impl Header {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let h = Header {
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod header;
|
||||
mod pipeline;
|
||||
mod policy;
|
||||
mod reader;
|
||||
mod secrets;
|
||||
mod utils;
|
||||
|
||||
pub use crate::{
|
||||
crypto::{
|
||||
DecryptOptions,
|
||||
DecryptRangeOptions,
|
||||
EncryptOptions,
|
||||
decrypt,
|
||||
decrypt_range,
|
||||
derive_key,
|
||||
encrypt,
|
||||
},
|
||||
error::FcryError,
|
||||
header::{
|
||||
ARGON2_SALT_LEN,
|
||||
AlgId,
|
||||
FLAG_KEY_COMMITTED,
|
||||
FLAG_LENGTH_COMMITTED,
|
||||
Header,
|
||||
HeaderReadOptions,
|
||||
KEY_COMMITMENT_LEN,
|
||||
KdfParams,
|
||||
NONCE_PREFIX_LEN,
|
||||
TAG_LEN,
|
||||
VERSION_CURRENT,
|
||||
},
|
||||
policy::{
|
||||
ArgonDecryptCap,
|
||||
DEFAULT_ARGON_MEMORY_MIB,
|
||||
DEFAULT_ARGON_PARALLELISM,
|
||||
MAX_WORKER_THREADS,
|
||||
MIN_ARGON_PASSES,
|
||||
default_argon_decrypt_cap_mib,
|
||||
normalize_worker_threads,
|
||||
resolve_argon_decrypt_cap,
|
||||
validate_new_argon_params,
|
||||
validate_new_passphrase,
|
||||
},
|
||||
secrets::{SecretBytes32, SecretVec, normalize_passphrase, read_key_file, read_passphrase_tty},
|
||||
utils::{DEFAULT_CHUNK_SIZE, OutputOptions},
|
||||
};
|
||||
+42
-105
@@ -1,25 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod header;
|
||||
mod pipeline;
|
||||
mod policy;
|
||||
mod reader;
|
||||
mod secrets;
|
||||
mod utils;
|
||||
|
||||
use crypto::*;
|
||||
use error::FcryError;
|
||||
use header::{ARGON2_SALT_LEN, KdfParams};
|
||||
use secrets::{SecretBytes32, SecretVec, read_passphrase_tty};
|
||||
use utils::{DEFAULT_CHUNK_SIZE, OutSinkOptions};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
use fcry::*;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// fcry - [f]ile[cry]pt: A file en-/decryption tool for easy use
|
||||
@@ -57,15 +41,15 @@ struct Cli {
|
||||
chunk_size: u32,
|
||||
|
||||
/// Argon2id memory in MiB (encryption only). Default: 1024 (= 1 GiB).
|
||||
#[clap(long, default_value_t = policy::DEFAULT_ARGON_MEMORY_MIB)]
|
||||
#[clap(long, default_value_t = DEFAULT_ARGON_MEMORY_MIB)]
|
||||
argon_memory: u32,
|
||||
|
||||
/// Argon2id passes / iterations (encryption only).
|
||||
#[clap(long, default_value_t = policy::MIN_ARGON_PASSES)]
|
||||
#[clap(long, default_value_t = MIN_ARGON_PASSES)]
|
||||
argon_passes: u32,
|
||||
|
||||
/// Argon2id parallelism / lanes (encryption only).
|
||||
#[clap(long, default_value_t = policy::DEFAULT_ARGON_PARALLELISM)]
|
||||
#[clap(long, default_value_t = DEFAULT_ARGON_PARALLELISM)]
|
||||
argon_parallelism: u32,
|
||||
|
||||
/// Permit intentionally weak passphrase/KDF parameters for tests or legacy interop.
|
||||
@@ -116,43 +100,6 @@ struct Cli {
|
||||
length: Option<u64>,
|
||||
}
|
||||
|
||||
fn read_key_file(path: &Path) -> Result<SecretBytes32, FcryError> {
|
||||
warn_if_key_file_world_readable(path);
|
||||
let mut file = File::open(path)?;
|
||||
let mut buf = Zeroizing::new([0u8; 33]);
|
||||
let mut n = 0usize;
|
||||
while n < buf.len() {
|
||||
match file.read(&mut buf[n..]) {
|
||||
Ok(0) => break,
|
||||
Ok(read) => n += read,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
if n < 32 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"key file {} is too short: expected exactly 32 bytes, got {n}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
if n > 32 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"key file {} is too long: expected exactly 32 bytes; possible trailing newline",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut extra = Zeroizing::new([0u8; 1]);
|
||||
if file.read(&mut *extra)? != 0 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"key file {} is too long: expected exactly 32 bytes; possible trailing newline",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut key = SecretBytes32::zeroed();
|
||||
key.with_mut_array(|key| key.copy_from_slice(&buf[..32]));
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn warn_if_key_file_world_readable(path: &Path) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -170,22 +117,17 @@ fn warn_if_key_file_world_readable(path: &Path) {
|
||||
#[cfg(not(unix))]
|
||||
fn warn_if_key_file_world_readable(_path: &Path) {}
|
||||
|
||||
fn read_key_file_cli(path: &Path) -> Result<SecretBytes32, FcryError> {
|
||||
warn_if_key_file_world_readable(path);
|
||||
read_key_file(path)
|
||||
}
|
||||
|
||||
/// Source of a passphrase: either the terminal or a named env var.
|
||||
enum PassphraseSource {
|
||||
Tty,
|
||||
EnvVar(String),
|
||||
}
|
||||
|
||||
fn normalize_passphrase(pw: SecretVec) -> Result<SecretVec, FcryError> {
|
||||
let normalized = pw.with_slice(|bytes| {
|
||||
let s = std::str::from_utf8(bytes).map_err(|_| {
|
||||
FcryError::Passphrase("passphrase must be valid UTF-8 after normalization".into())
|
||||
})?;
|
||||
Ok::<Zeroizing<String>, FcryError>(Zeroizing::new(s.nfc().collect::<String>()))
|
||||
})?;
|
||||
Ok(SecretVec::from_vec(normalized.as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
fn read_passphrase(src: &PassphraseSource, confirm: bool) -> Result<SecretVec, FcryError> {
|
||||
match src {
|
||||
PassphraseSource::EnvVar(var) => {
|
||||
@@ -196,8 +138,7 @@ fn read_passphrase(src: &PassphraseSource, confirm: bool) -> Result<SecretVec, F
|
||||
let v = Zeroizing::new(std::env::var(var).map_err(|_| {
|
||||
FcryError::Passphrase(format!("environment variable {var} not set or not unicode"))
|
||||
})?);
|
||||
let normalized = Zeroizing::new(v.as_str().nfc().collect::<String>());
|
||||
Ok(SecretVec::from_vec(normalized.as_bytes().to_vec()))
|
||||
normalize_passphrase(SecretVec::from_vec(v.as_bytes().to_vec()))
|
||||
}
|
||||
PassphraseSource::Tty => {
|
||||
let pw = normalize_passphrase(
|
||||
@@ -251,18 +192,18 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
|
||||
let argon_passes = cli.argon_passes;
|
||||
let argon_parallelism = cli.argon_parallelism;
|
||||
let allow_weak_kdf = cli.allow_weak_kdf;
|
||||
let argon_cap = policy::resolve_argon_decrypt_cap(cli.max_argon_memory_mib)?;
|
||||
let argon_cap = resolve_argon_decrypt_cap(cli.max_argon_memory_mib)?;
|
||||
if argon_cap.overridden && argon_cap.effective_mib > argon_cap.default_mib {
|
||||
eprintln!(
|
||||
"Warning: --max-argon-memory-mib raises the Argon2 decrypt trust ceiling from {} MiB to {} MiB; this can OOM constrained machines",
|
||||
argon_cap.default_mib, argon_cap.effective_mib
|
||||
);
|
||||
}
|
||||
let (threads, thread_warning) = policy::normalize_worker_threads(cli.threads);
|
||||
let (threads, thread_warning) = normalize_worker_threads(cli.threads);
|
||||
if let Some(requested) = thread_warning {
|
||||
eprintln!(
|
||||
"Warning: requested {requested} worker threads; capped at {}",
|
||||
policy::MAX_WORKER_THREADS
|
||||
MAX_WORKER_THREADS
|
||||
);
|
||||
}
|
||||
let force = cli.force;
|
||||
@@ -288,16 +229,15 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
|
||||
));
|
||||
}
|
||||
|
||||
let output_options = OutSinkOptions {
|
||||
let output_options = OutputOptions {
|
||||
force,
|
||||
input_file: input.as_ref().map(PathBuf::from),
|
||||
temp_dir,
|
||||
buffer_verify_stdout: buffer_verify,
|
||||
};
|
||||
|
||||
if decrypt_mode {
|
||||
let raw_key = match key_file.as_deref() {
|
||||
Some(path) => Some(read_key_file(path)?),
|
||||
Some(path) => Some(read_key_file_cli(path)?),
|
||||
None => None,
|
||||
};
|
||||
let pw = match &pw_src {
|
||||
@@ -313,27 +253,25 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
|
||||
"--offset/--length require --input-file (random-access needs a seekable file)".into(),
|
||||
)
|
||||
})?;
|
||||
decrypt_range_with_output_options(
|
||||
path,
|
||||
output,
|
||||
raw_key.as_ref(),
|
||||
pw.as_ref(),
|
||||
o,
|
||||
l,
|
||||
argon_cap.effective_mib,
|
||||
&output_options,
|
||||
)?;
|
||||
let options = DecryptRangeOptions {
|
||||
input_file: PathBuf::from(path),
|
||||
output_file: output.as_deref().map(PathBuf::from),
|
||||
offset: o,
|
||||
length: l,
|
||||
max_argon_memory_mib: argon_cap.effective_mib,
|
||||
output: output_options,
|
||||
};
|
||||
decrypt_range(&options, raw_key.as_ref(), pw.as_ref())?;
|
||||
}
|
||||
(None, None) => {
|
||||
decrypt_with_output_options(
|
||||
input,
|
||||
output,
|
||||
raw_key.as_ref(),
|
||||
pw.as_ref(),
|
||||
let options = DecryptOptions {
|
||||
input_file: input.as_deref().map(PathBuf::from),
|
||||
output_file: output.as_deref().map(PathBuf::from),
|
||||
threads,
|
||||
argon_cap.effective_mib,
|
||||
&output_options,
|
||||
)?;
|
||||
max_argon_memory_mib: argon_cap.effective_mib,
|
||||
output: output_options,
|
||||
};
|
||||
decrypt(&options, raw_key.as_ref(), pw.as_ref())?;
|
||||
}
|
||||
_ => {
|
||||
return Err(FcryError::Format(
|
||||
@@ -345,7 +283,7 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
|
||||
let (key, kdf) = if let Some(src) = &pw_src {
|
||||
let mut salt = [0u8; ARGON2_SALT_LEN];
|
||||
getrandom::fill(&mut salt)?;
|
||||
let m_cost_kib = policy::validate_new_argon_params(
|
||||
let m_cost_kib = validate_new_argon_params(
|
||||
argon_memory,
|
||||
argon_passes,
|
||||
argon_parallelism,
|
||||
@@ -358,22 +296,21 @@ fn run(mut cli: Cli) -> Result<(), FcryError> {
|
||||
p_cost: argon_parallelism,
|
||||
};
|
||||
let pw = read_passphrase(src, true)?;
|
||||
policy::validate_new_passphrase(&pw, allow_weak_kdf)?;
|
||||
validate_new_passphrase(&pw, allow_weak_kdf)?;
|
||||
let key = derive_key(&kdf, None, Some(&pw))?;
|
||||
(key, kdf)
|
||||
} else {
|
||||
let key = read_key_file(key_file.as_deref().unwrap())?;
|
||||
let key = read_key_file_cli(key_file.as_deref().unwrap())?;
|
||||
(key, KdfParams::Raw)
|
||||
};
|
||||
encrypt_with_output_options(
|
||||
input,
|
||||
output,
|
||||
&key,
|
||||
let options = EncryptOptions {
|
||||
input_file: input.as_deref().map(PathBuf::from),
|
||||
output_file: output.as_deref().map(PathBuf::from),
|
||||
chunk_size,
|
||||
kdf,
|
||||
threads,
|
||||
&output_options,
|
||||
)?;
|
||||
output: output_options,
|
||||
};
|
||||
encrypt(&options, &key, kdf)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -383,7 +320,7 @@ fn main() {
|
||||
disable_core_dumps();
|
||||
let cli = Cli::parse();
|
||||
if let Err(e) = run(cli) {
|
||||
eprintln!("Error: {:?}", e);
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-15
@@ -29,24 +29,30 @@
|
||||
//! cores (cap = 32) that's ~34 MiB. Adjust `in_flight_capacity` if you need
|
||||
//! a different memory/throughput tradeoff.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::Write,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chacha20poly1305::{XChaCha20Poly1305, aead::AeadInPlace};
|
||||
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, bounded};
|
||||
|
||||
use crate::crypto::{bump_counter, make_nonce};
|
||||
use crate::error::FcryError;
|
||||
use crate::header::NONCE_PREFIX_LEN;
|
||||
use crate::policy;
|
||||
use crate::reader::{AheadReader, ReadInfoChunk};
|
||||
use crate::utils::OutSink;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::{
|
||||
crypto::{bump_counter, make_nonce},
|
||||
error::FcryError,
|
||||
header::NONCE_PREFIX_LEN,
|
||||
policy,
|
||||
reader::{AheadReader, ReadInfoChunk},
|
||||
utils::OutSink,
|
||||
};
|
||||
|
||||
struct Job {
|
||||
counter: u32,
|
||||
last: bool,
|
||||
@@ -197,7 +203,7 @@ fn run_pipeline(
|
||||
}
|
||||
let mut buf = Zeroizing::new(vec![0u8; chunk_sz]);
|
||||
match input.read_ahead(&mut buf)? {
|
||||
ReadInfoChunk::Normal(_) => {
|
||||
ReadInfoChunk::Normal => {
|
||||
if jobs_tx
|
||||
.send(Job {
|
||||
counter,
|
||||
@@ -370,6 +376,5 @@ fn ordered_writer(
|
||||
|
||||
// Compile-time check that the job type is Send+Sync (channel sends across
|
||||
// threads). Kept as a footgun for future struct edits.
|
||||
#[allow(dead_code)]
|
||||
fn _assert_send_sync<T: Send + Sync>() {}
|
||||
const _: fn() = || _assert_send_sync::<Sender<Job>>();
|
||||
|
||||
+5
-3
@@ -4,9 +4,11 @@
|
||||
|
||||
use std::fs;
|
||||
|
||||
use crate::error::FcryError;
|
||||
use crate::header::{KdfParams, TAG_LEN};
|
||||
use crate::secrets::SecretVec;
|
||||
use crate::{
|
||||
error::FcryError,
|
||||
header::{KdfParams, TAG_LEN},
|
||||
secrets::SecretVec,
|
||||
};
|
||||
|
||||
pub const MAX_CHUNK_SIZE: u32 = 64 * 1024 * 1024;
|
||||
pub const DEFAULT_CHUNK_SIZE: u32 = 1024 * 1024;
|
||||
|
||||
+12
-9
@@ -1,16 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
use std::io;
|
||||
use std::io::{BufRead, Read};
|
||||
use std::{
|
||||
io,
|
||||
io::{BufRead, Read},
|
||||
};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub enum ReadInfoChunk {
|
||||
Normal(#[allow(dead_code)] usize),
|
||||
pub(crate) enum ReadInfoChunk {
|
||||
Normal,
|
||||
Last(usize),
|
||||
Empty,
|
||||
}
|
||||
|
||||
pub struct AheadReader {
|
||||
pub(crate) struct AheadReader {
|
||||
inner: Box<dyn BufRead + Send>,
|
||||
buf: Zeroizing<Vec<u8>>,
|
||||
bufsz: usize,
|
||||
@@ -18,7 +21,7 @@ pub struct AheadReader {
|
||||
}
|
||||
|
||||
impl AheadReader {
|
||||
pub fn from(reader: Box<dyn BufRead + Send>, capacity: usize) -> Self {
|
||||
pub(crate) fn from(reader: Box<dyn BufRead + Send>, capacity: usize) -> Self {
|
||||
Self {
|
||||
inner: reader,
|
||||
buf: Zeroizing::new(vec![0; capacity]),
|
||||
@@ -47,7 +50,7 @@ impl AheadReader {
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
pub fn read_ahead(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
||||
pub(crate) fn read_ahead(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
||||
if self.bufsz == 0 {
|
||||
return self.first_read(userbuf);
|
||||
}
|
||||
@@ -70,7 +73,7 @@ impl AheadReader {
|
||||
return Ok(ReadInfoChunk::Last(n));
|
||||
}
|
||||
|
||||
Ok(ReadInfoChunk::Normal(n))
|
||||
Ok(ReadInfoChunk::Normal)
|
||||
}
|
||||
|
||||
fn normal_read(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
||||
@@ -87,6 +90,6 @@ impl AheadReader {
|
||||
return Ok(ReadInfoChunk::Last(userbuf_sz));
|
||||
}
|
||||
|
||||
Ok(ReadInfoChunk::Normal(userbuf_sz))
|
||||
Ok(ReadInfoChunk::Normal)
|
||||
}
|
||||
}
|
||||
|
||||
+92
-12
@@ -14,9 +14,17 @@
|
||||
//! Windows Console API). Reads into a pre-reserved `SecretVec` so no
|
||||
//! reallocation can leave stale unzeroed copies on the heap.
|
||||
|
||||
use std::io;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Read},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use protected_secrets::{SecretBox as ProtectedSecretBox, SecretVec as ProtectedSecretVec};
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::error::FcryError;
|
||||
|
||||
/// Maximum passphrase length we accept on the tty.
|
||||
/// Pre-reserved so the underlying Vec never reallocates while reading.
|
||||
@@ -99,6 +107,10 @@ impl SecretVec {
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for SecretVec {
|
||||
@@ -117,6 +129,61 @@ impl PartialEq for SecretVec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a raw 32-byte key from `path`, rejecting files that are not exactly
|
||||
/// 32 bytes long (a likely trailing newline is called out in the error).
|
||||
///
|
||||
/// Performs **no permission checking** on the file. Library callers who care
|
||||
/// whether the key file is readable by others must check themselves; the fcry
|
||||
/// CLI does this and prints a warning (see `read_key_file_cli` in the binary).
|
||||
pub fn read_key_file(path: &Path) -> Result<SecretBytes32, FcryError> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buf = Zeroizing::new([0u8; 33]);
|
||||
let mut n = 0usize;
|
||||
while n < buf.len() {
|
||||
match file.read(&mut buf[n..]) {
|
||||
Ok(0) => break,
|
||||
Ok(read) => n += read,
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
if n < 32 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"key file {} is too short: expected exactly 32 bytes, got {n}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
if n > 32 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"key file {} is too long: expected exactly 32 bytes; possible trailing newline",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut extra = Zeroizing::new([0u8; 1]);
|
||||
if file.read(&mut *extra)? != 0 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"key file {} is too long: expected exactly 32 bytes; possible trailing newline",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let mut key = SecretBytes32::zeroed();
|
||||
key.with_mut_array(|key| key.copy_from_slice(&buf[..32]));
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Normalizes a passphrase to Unicode NFC so the same visual passphrase
|
||||
/// always derives the same key regardless of how the platform or input
|
||||
/// method composed it. Fails if the bytes are not valid UTF-8.
|
||||
pub fn normalize_passphrase(pw: SecretVec) -> Result<SecretVec, FcryError> {
|
||||
let normalized = pw.with_slice(|bytes| {
|
||||
let s = std::str::from_utf8(bytes).map_err(|_| {
|
||||
FcryError::Passphrase("passphrase must be valid UTF-8 after normalization".into())
|
||||
})?;
|
||||
Ok::<Zeroizing<String>, FcryError>(Zeroizing::new(s.nfc().collect::<String>()))
|
||||
})?;
|
||||
Ok(SecretVec::from_vec(normalized.as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// tty passphrase reader
|
||||
// ============================================================================
|
||||
@@ -132,10 +199,13 @@ pub fn read_passphrase_tty(prompt: &str) -> io::Result<SecretVec> {
|
||||
|
||||
#[cfg(unix)]
|
||||
mod imp {
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, Read, Write},
|
||||
os::unix::io::AsRawFd,
|
||||
};
|
||||
|
||||
use super::{MAX_PASSPHRASE_LEN, SecretVec};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
/// RAII guard that restores the original termios on drop.
|
||||
struct TermiosGuard {
|
||||
@@ -194,18 +264,28 @@ mod imp {
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use super::{MAX_PASSPHRASE_LEN, SecretVec};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::System::Console::{
|
||||
ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT, GetConsoleMode, ReadConsoleW,
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, Write},
|
||||
os::windows::io::AsRawHandle,
|
||||
ptr,
|
||||
};
|
||||
|
||||
use windows_sys::Win32::{
|
||||
Foundation::HANDLE,
|
||||
System::Console::{
|
||||
ENABLE_ECHO_INPUT,
|
||||
ENABLE_LINE_INPUT,
|
||||
ENABLE_PROCESSED_INPUT,
|
||||
GetConsoleMode,
|
||||
ReadConsoleW,
|
||||
SetConsoleMode,
|
||||
},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{MAX_PASSPHRASE_LEN, SecretVec};
|
||||
|
||||
struct ConsoleModeGuard {
|
||||
handle: HANDLE,
|
||||
orig: u32,
|
||||
|
||||
+26
-20
@@ -1,8 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufRead, BufReader, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::policy;
|
||||
|
||||
@@ -22,10 +24,10 @@ pub(crate) struct Input {
|
||||
pub length: Option<u64>,
|
||||
}
|
||||
|
||||
pub(crate) fn open_input<S: AsRef<str>>(input_file: Option<S>) -> io::Result<Input> {
|
||||
pub(crate) fn open_input(input_file: Option<&Path>) -> io::Result<Input> {
|
||||
match input_file {
|
||||
Some(f) => {
|
||||
let file = File::open(f.as_ref())?;
|
||||
let file = File::open(f)?;
|
||||
// Stat the open FD (not the path) so we can't be raced between
|
||||
// stat and open.
|
||||
let length = file
|
||||
@@ -48,9 +50,8 @@ pub(crate) fn open_input<S: AsRef<str>>(input_file: Option<S>) -> io::Result<Inp
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct OutSinkOptions {
|
||||
pub struct OutputOptions {
|
||||
pub force: bool,
|
||||
pub input_file: Option<PathBuf>,
|
||||
pub temp_dir: Option<PathBuf>,
|
||||
pub buffer_verify_stdout: bool,
|
||||
}
|
||||
@@ -197,6 +198,16 @@ fn output_aliases_input(output: &Path, input: Option<&Path>) -> io::Result<bool>
|
||||
}
|
||||
}
|
||||
|
||||
/// Paths for [`OutSink::open_with_options`], named so the output and input
|
||||
/// roles cannot be swapped silently at a call site (both are `Option<&Path>`).
|
||||
pub(crate) struct OutSinkPaths<'a> {
|
||||
/// Where the finished output is renamed to; `None` means stdout.
|
||||
pub output_file: Option<&'a Path>,
|
||||
/// The input being read; only consulted by the aliasing guard that
|
||||
/// permits in-place overwrite of the input without `--force`.
|
||||
pub input_file: Option<&'a Path>,
|
||||
}
|
||||
|
||||
/// Output sink that supports atomic file replacement.
|
||||
///
|
||||
/// For file outputs: bytes are written to a private, randomly named temp file.
|
||||
@@ -205,7 +216,7 @@ fn output_aliases_input(output: &Path, input: Option<&Path>) -> io::Result<bool>
|
||||
/// partial/garbage file does not replace any existing target.
|
||||
///
|
||||
/// For stdout: behaves as a passthrough; `commit()` is a no-op.
|
||||
pub enum OutSink {
|
||||
pub(crate) enum OutSink {
|
||||
Stdout(io::Stdout),
|
||||
BufferVerify {
|
||||
temp: SecureTempFile,
|
||||
@@ -217,16 +228,11 @@ pub enum OutSink {
|
||||
}
|
||||
|
||||
impl OutSink {
|
||||
#[allow(dead_code)]
|
||||
pub fn open<S: AsRef<str>>(output_file: Option<S>) -> io::Result<Self> {
|
||||
Self::open_with_options(output_file, &OutSinkOptions::default())
|
||||
}
|
||||
|
||||
pub fn open_with_options<S: AsRef<str>>(
|
||||
output_file: Option<S>,
|
||||
options: &OutSinkOptions,
|
||||
pub(crate) fn open_with_options(
|
||||
paths: OutSinkPaths<'_>,
|
||||
options: &OutputOptions,
|
||||
) -> io::Result<Self> {
|
||||
match output_file {
|
||||
match paths.output_file {
|
||||
None if options.buffer_verify_stdout => {
|
||||
let dir = temp_dir_for_stdout(options.temp_dir.as_deref());
|
||||
Ok(Self::BufferVerify {
|
||||
@@ -235,10 +241,10 @@ impl OutSink {
|
||||
}
|
||||
None => Ok(Self::Stdout(io::stdout())),
|
||||
Some(f) => {
|
||||
let final_path = PathBuf::from(f.as_ref());
|
||||
let final_path = f.to_path_buf();
|
||||
if final_path.exists()
|
||||
&& !options.force
|
||||
&& !output_aliases_input(&final_path, options.input_file.as_deref())?
|
||||
&& !output_aliases_input(&final_path, paths.input_file)?
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
@@ -256,7 +262,7 @@ impl OutSink {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit(mut self) -> io::Result<()> {
|
||||
pub(crate) fn commit(mut self) -> io::Result<()> {
|
||||
match &mut self {
|
||||
Self::Stdout(s) => s.flush()?,
|
||||
Self::BufferVerify { .. } => {}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
//
|
||||
// Fixtures shared by the integration-test crates (tests/*.rs). The unit
|
||||
// tests inside src/ cannot reach this module and keep their own copies.
|
||||
|
||||
// Each test crate compiles its own copy of this module and not every crate
|
||||
// uses every fixture, so the dead-code lint misfires here.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use fcry::SecretBytes32;
|
||||
|
||||
/// The raw 32-byte key used by all integration tests.
|
||||
pub const KEY: &[u8; 32] = b"0123456789abcdef0123456789abcdef";
|
||||
|
||||
/// [`KEY`] wrapped in the `SecretBytes32` the library API takes.
|
||||
pub fn test_key() -> SecretBytes32 {
|
||||
let mut key = SecretBytes32::zeroed();
|
||||
key.with_mut_array(|key| key.copy_from_slice(KEY));
|
||||
key
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::fs;
|
||||
|
||||
use fcry::{
|
||||
DecryptOptions,
|
||||
DecryptRangeOptions,
|
||||
EncryptOptions,
|
||||
KdfParams,
|
||||
OutputOptions,
|
||||
decrypt,
|
||||
decrypt_range,
|
||||
default_argon_decrypt_cap_mib,
|
||||
encrypt,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
mod common;
|
||||
use common::test_key;
|
||||
|
||||
#[test]
|
||||
fn library_file_roundtrip_raw_key() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let plain = dir.path().join("plain.bin");
|
||||
let ct = dir.path().join("cipher.fcry");
|
||||
let out = dir.path().join("out.bin");
|
||||
let data: Vec<u8> = (0..=255).cycle().take(100_000).collect();
|
||||
fs::write(&plain, &data).unwrap();
|
||||
|
||||
let key = test_key();
|
||||
let encrypt_options = EncryptOptions {
|
||||
input_file: Some(plain),
|
||||
output_file: Some(ct.clone()),
|
||||
chunk_size: 4096,
|
||||
threads: 1,
|
||||
output: OutputOptions::default(),
|
||||
};
|
||||
encrypt(&encrypt_options, &key, KdfParams::Raw).unwrap();
|
||||
|
||||
let decrypt_options = DecryptOptions {
|
||||
input_file: Some(ct),
|
||||
output_file: Some(out.clone()),
|
||||
threads: 1,
|
||||
..DecryptOptions::default()
|
||||
};
|
||||
decrypt(&decrypt_options, Some(&key), None).unwrap();
|
||||
|
||||
assert_eq!(fs::read(out).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_range_decrypt_raw_key() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let plain = dir.path().join("plain.bin");
|
||||
let ct = dir.path().join("cipher.fcry");
|
||||
let out = dir.path().join("slice.bin");
|
||||
let data: Vec<u8> = (0..=255).cycle().take(50_000).collect();
|
||||
fs::write(&plain, &data).unwrap();
|
||||
|
||||
let key = test_key();
|
||||
let encrypt_options = EncryptOptions {
|
||||
input_file: Some(plain),
|
||||
output_file: Some(ct.clone()),
|
||||
chunk_size: 1024,
|
||||
threads: 2,
|
||||
output: OutputOptions::default(),
|
||||
};
|
||||
encrypt(&encrypt_options, &key, KdfParams::Raw).unwrap();
|
||||
|
||||
let range_options = DecryptRangeOptions {
|
||||
input_file: ct,
|
||||
output_file: Some(out.clone()),
|
||||
offset: 1234,
|
||||
length: 20_000,
|
||||
max_argon_memory_mib: default_argon_decrypt_cap_mib(),
|
||||
output: OutputOptions::default(),
|
||||
};
|
||||
decrypt_range(&range_options, Some(&key), None).unwrap();
|
||||
|
||||
assert_eq!(fs::read(out).unwrap(), data[1234..21_234]);
|
||||
}
|
||||
+10
-11
@@ -6,14 +6,17 @@
|
||||
// plaintext bytes are preserved, plus a handful of failure cases (tampering,
|
||||
// wrong key, truncation, bad magic).
|
||||
|
||||
use std::fs;
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{
|
||||
fs,
|
||||
io::{ErrorKind, Write},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use assert_cmd::cargo::CommandCargoExt;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const KEY: &[u8; 32] = b"0123456789abcdef0123456789abcdef";
|
||||
mod common;
|
||||
use common::KEY;
|
||||
|
||||
fn fcry() -> Command {
|
||||
Command::cargo_bin("fcry").unwrap()
|
||||
@@ -209,8 +212,8 @@ fn rejects_wrong_key() {
|
||||
.unwrap();
|
||||
assert!(!out.status.success(), "decrypt with wrong key should fail");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&out.stderr).contains("WrongKey"),
|
||||
"expected distinct WrongKey error, got {}",
|
||||
String::from_utf8_lossy(&out.stderr).contains("wrong key or passphrase"),
|
||||
"expected distinct wrong-key error, got {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
@@ -422,11 +425,7 @@ fn non_utf8_key_file_roundtrips() {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn split_fifo_key_file_read_roundtrips() {
|
||||
use std::ffi::CString;
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{ffi::CString, fs::OpenOptions, os::unix::ffi::OsStrExt, thread, time::Duration};
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let fifo = dir.path().join("key.fifo");
|
||||
|
||||
Reference in New Issue
Block a user