feat(12-02): implement Argon2id KDF, rpassword prompt, and salt format support
- Add argon2 0.5 and rpassword 7.4 dependencies - Implement derive_key_from_password() using Argon2id with 16-byte salt - Implement prompt_password() with optional confirmation for pack - Add resolve_key_for_pack() (generates random salt) and resolve_key_for_unpack() (reads salt from archive) - Add FLAG_KDF_SALT (bit 4), SALT_SIZE constant, read_salt/write_salt functions to format.rs - Relax flags validation to allow bit 4 (bits 5-7 must be zero)
This commit is contained in:
170
Cargo.lock
generated
170
Cargo.lock
generated
@@ -64,7 +64,7 @@ version = "1.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -75,7 +75,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell_polyfill",
|
"once_cell_polyfill",
|
||||||
"windows-sys",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -84,6 +84,18 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert_cmd"
|
name = "assert_cmd"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
@@ -105,12 +117,27 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -293,15 +320,18 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"argon2",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"cbc",
|
"cbc",
|
||||||
"clap",
|
"clap",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"hex",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
"hmac",
|
"hmac",
|
||||||
"predicates",
|
"predicates",
|
||||||
"rand",
|
"rand",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"rpassword",
|
||||||
"sha2",
|
"sha2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
@@ -313,7 +343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -369,6 +399,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex-literal"
|
name = "hex-literal"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -455,6 +491,17 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -525,7 +572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -535,9 +582,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@@ -596,6 +649,27 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpassword"
|
||||||
|
version = "7.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rtoolbox",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rtoolbox"
|
||||||
|
version = "0.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -606,7 +680,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -688,7 +762,7 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -745,6 +819,24 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -754,6 +846,70 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ rand = "0.9"
|
|||||||
rayon = "1.11"
|
rayon = "1.11"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
argon2 = "0.5"
|
||||||
|
rpassword = "7.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.16"
|
tempfile = "3.16"
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ pub const VERSION: u8 = 2;
|
|||||||
/// Fixed header size in bytes.
|
/// Fixed header size in bytes.
|
||||||
pub const HEADER_SIZE: u32 = 40;
|
pub const HEADER_SIZE: u32 = 40;
|
||||||
|
|
||||||
|
/// KDF salt size in bytes (placed between header and TOC when present).
|
||||||
|
pub const SALT_SIZE: u32 = 16;
|
||||||
|
|
||||||
|
/// Flag bit 4: KDF salt is present after header (password-derived key).
|
||||||
|
pub const FLAG_KDF_SALT: u8 = 0x10;
|
||||||
|
|
||||||
/// Fixed 8-byte XOR obfuscation key (FORMAT.md Section 9.1).
|
/// Fixed 8-byte XOR obfuscation key (FORMAT.md Section 9.1).
|
||||||
pub const XOR_KEY: [u8; 8] = [0xA5, 0x3C, 0x96, 0x0F, 0xE1, 0x7B, 0x4D, 0xC8];
|
pub const XOR_KEY: [u8; 8] = [0xA5, 0x3C, 0x96, 0x0F, 0xE1, 0x7B, 0x4D, 0xC8];
|
||||||
|
|
||||||
@@ -112,15 +118,15 @@ pub fn write_header_to_buf(header: &Header) -> [u8; 40] {
|
|||||||
|
|
||||||
/// Parse a header from a 40-byte buffer (already validated for magic).
|
/// Parse a header from a 40-byte buffer (already validated for magic).
|
||||||
///
|
///
|
||||||
/// Verifies: version == 2, reserved flags bits 4-7 are zero.
|
/// Verifies: version == 2, reserved flags bits 5-7 are zero (bit 4 = KDF salt).
|
||||||
fn parse_header_from_buf(buf: &[u8; 40]) -> anyhow::Result<Header> {
|
fn parse_header_from_buf(buf: &[u8; 40]) -> anyhow::Result<Header> {
|
||||||
let version = buf[4];
|
let version = buf[4];
|
||||||
anyhow::ensure!(version == VERSION, "Unsupported version: {}", version);
|
anyhow::ensure!(version == VERSION, "Unsupported version: {}", version);
|
||||||
|
|
||||||
let flags = buf[5];
|
let flags = buf[5];
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
flags & 0xF0 == 0,
|
flags & 0xE0 == 0,
|
||||||
"Unknown flags set: 0x{:02X} (bits 4-7 must be zero)",
|
"Unknown flags set: 0x{:02X} (bits 5-7 must be zero)",
|
||||||
flags
|
flags
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -191,7 +197,7 @@ pub fn read_toc_from_buf(buf: &[u8], file_count: u16) -> anyhow::Result<Vec<TocE
|
|||||||
|
|
||||||
/// Read and parse the 40-byte archive header.
|
/// Read and parse the 40-byte archive header.
|
||||||
///
|
///
|
||||||
/// Verifies: magic bytes, version == 2, reserved flags bits 4-7 are zero.
|
/// Verifies: magic bytes, version == 2, reserved flags bits 5-7 are zero.
|
||||||
pub fn read_header(reader: &mut impl Read) -> anyhow::Result<Header> {
|
pub fn read_header(reader: &mut impl Read) -> anyhow::Result<Header> {
|
||||||
let mut buf = [0u8; 40];
|
let mut buf = [0u8; 40];
|
||||||
reader.read_exact(&mut buf)?;
|
reader.read_exact(&mut buf)?;
|
||||||
@@ -209,8 +215,8 @@ pub fn read_header(reader: &mut impl Read) -> anyhow::Result<Header> {
|
|||||||
|
|
||||||
let flags = buf[5];
|
let flags = buf[5];
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
flags & 0xF0 == 0,
|
flags & 0xE0 == 0,
|
||||||
"Unknown flags set: 0x{:02X} (bits 4-7 must be zero)",
|
"Unknown flags set: 0x{:02X} (bits 5-7 must be zero)",
|
||||||
flags
|
flags
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -334,6 +340,24 @@ pub fn compute_toc_size(entries: &[TocEntry]) -> u32 {
|
|||||||
entries.iter().map(entry_size).sum()
|
entries.iter().map(entry_size).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the 16-byte KDF salt from an archive, if present (flags bit 4 set).
|
||||||
|
/// Must be called after reading the header, before seeking to TOC.
|
||||||
|
pub fn read_salt(reader: &mut impl Read, header: &Header) -> anyhow::Result<Option<[u8; 16]>> {
|
||||||
|
if header.flags & FLAG_KDF_SALT != 0 {
|
||||||
|
let mut salt = [0u8; 16];
|
||||||
|
reader.read_exact(&mut salt)?;
|
||||||
|
Ok(Some(salt))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the 16-byte KDF salt after the header.
|
||||||
|
pub fn write_salt(writer: &mut impl Write, salt: &[u8; 16]) -> anyhow::Result<()> {
|
||||||
|
writer.write_all(salt)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
99
src/key.rs
99
src/key.rs
@@ -17,14 +17,40 @@ pub enum KeySource {
|
|||||||
Password(Option<String>), // None = interactive prompt
|
Password(Option<String>), // None = interactive prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a KeySource into a 32-byte AES-256 key.
|
/// Result of key resolution, including optional salt for password-derived keys.
|
||||||
///
|
pub struct ResolvedKey {
|
||||||
/// For Hex: decode 64-char hex string into [u8; 32].
|
pub key: [u8; 32],
|
||||||
/// For File: read exactly 32 bytes from file.
|
pub salt: Option<[u8; 16]>, // Some if password-derived (new archive)
|
||||||
/// For Password: placeholder that returns error (implemented in Plan 02).
|
}
|
||||||
pub fn resolve_key(source: &KeySource) -> anyhow::Result<[u8; 32]> {
|
|
||||||
match source {
|
/// Derive a 32-byte key from a password and salt using Argon2id.
|
||||||
KeySource::Hex(hex_str) => {
|
pub fn derive_key_from_password(password: &[u8], salt: &[u8; 16]) -> anyhow::Result<[u8; 32]> {
|
||||||
|
use argon2::Argon2;
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
Argon2::default()
|
||||||
|
.hash_password_into(password, salt, &mut key)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Argon2 key derivation failed: {}", e))?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompt user for password interactively (stdin).
|
||||||
|
/// For pack: prompts twice (confirm). For unpack: prompts once.
|
||||||
|
pub fn prompt_password(confirm: bool) -> anyhow::Result<String> {
|
||||||
|
let password = rpassword::prompt_password("Password: ")
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to read password: {}", e))?;
|
||||||
|
anyhow::ensure!(!password.is_empty(), "Password cannot be empty");
|
||||||
|
|
||||||
|
if confirm {
|
||||||
|
let confirmation = rpassword::prompt_password("Confirm password: ")
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to read password confirmation: {}", e))?;
|
||||||
|
anyhow::ensure!(password == confirmation, "Passwords do not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a hex key string into a 32-byte key.
|
||||||
|
fn decode_hex_key(hex_str: &str) -> anyhow::Result<[u8; 32]> {
|
||||||
let bytes = hex::decode(hex_str)
|
let bytes = hex::decode(hex_str)
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid hex key: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Invalid hex key: {}", e))?;
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
@@ -37,7 +63,9 @@ pub fn resolve_key(source: &KeySource) -> anyhow::Result<[u8; 32]> {
|
|||||||
key.copy_from_slice(&bytes);
|
key.copy_from_slice(&bytes);
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
KeySource::File(path) => {
|
|
||||||
|
/// Read a 32-byte key from a file.
|
||||||
|
fn read_key_file(path: &PathBuf) -> anyhow::Result<[u8; 32]> {
|
||||||
let bytes = std::fs::read(path)
|
let bytes = std::fs::read(path)
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to read key file '{}': {}", path.display(), e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to read key file '{}': {}", path.display(), e))?;
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
@@ -50,8 +78,59 @@ pub fn resolve_key(source: &KeySource) -> anyhow::Result<[u8; 32]> {
|
|||||||
key.copy_from_slice(&bytes);
|
key.copy_from_slice(&bytes);
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve key for a NEW archive (pack). Generates salt for password.
|
||||||
|
pub fn resolve_key_for_pack(source: &KeySource) -> anyhow::Result<ResolvedKey> {
|
||||||
|
match source {
|
||||||
|
KeySource::Hex(hex_str) => {
|
||||||
|
let key = decode_hex_key(hex_str)?;
|
||||||
|
Ok(ResolvedKey { key, salt: None })
|
||||||
|
}
|
||||||
|
KeySource::File(path) => {
|
||||||
|
let key = read_key_file(path)?;
|
||||||
|
Ok(ResolvedKey { key, salt: None })
|
||||||
|
}
|
||||||
|
KeySource::Password(password_opt) => {
|
||||||
|
let password = match password_opt {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => prompt_password(true)?, // confirm for pack
|
||||||
|
};
|
||||||
|
let mut salt = [0u8; 16];
|
||||||
|
rand::Fill::fill(&mut salt, &mut rand::rng());
|
||||||
|
let key = derive_key_from_password(password.as_bytes(), &salt)?;
|
||||||
|
Ok(ResolvedKey { key, salt: Some(salt) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve key for an EXISTING archive (unpack/inspect).
|
||||||
|
/// If password, requires salt from the archive.
|
||||||
|
pub fn resolve_key_for_unpack(source: &KeySource, archive_salt: Option<&[u8; 16]>) -> anyhow::Result<[u8; 32]> {
|
||||||
|
match source {
|
||||||
|
KeySource::Hex(hex_str) => decode_hex_key(hex_str),
|
||||||
|
KeySource::File(path) => read_key_file(path),
|
||||||
|
KeySource::Password(password_opt) => {
|
||||||
|
let salt = archive_salt
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Archive does not contain a salt (was not created with --password)"))?;
|
||||||
|
let password = match password_opt {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => prompt_password(false)?, // no confirm for unpack
|
||||||
|
};
|
||||||
|
derive_key_from_password(password.as_bytes(), salt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a KeySource into a 32-byte AES-256 key.
|
||||||
|
///
|
||||||
|
/// Legacy wrapper kept for backward compatibility with inspect (keyless case).
|
||||||
|
/// For pack, use resolve_key_for_pack(). For unpack, use resolve_key_for_unpack().
|
||||||
|
pub fn resolve_key(source: &KeySource) -> anyhow::Result<[u8; 32]> {
|
||||||
|
match source {
|
||||||
|
KeySource::Hex(hex_str) => decode_hex_key(hex_str),
|
||||||
|
KeySource::File(path) => read_key_file(path),
|
||||||
KeySource::Password(_) => {
|
KeySource::Password(_) => {
|
||||||
anyhow::bail!("Password-based key derivation not yet implemented (coming in Plan 02)")
|
anyhow::bail!("Use resolve_key_for_pack() or resolve_key_for_unpack() for password-based keys")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user