This commit is contained in:
2026-05-10 19:42:47 -05:00
parent e4a3eaae08
commit bfd4814d70
6 changed files with 317 additions and 7 deletions
Generated
+220
View File
@@ -160,6 +160,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.21.7"
@@ -367,6 +373,18 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -396,6 +414,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
@@ -453,9 +472,65 @@ dependencies = [
"age",
"clap",
"flate2",
"rand",
"ssh-key",
"tar",
]
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2",
"subtle",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"pkcs8",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "errno"
version = "0.3.14"
@@ -466,6 +541,16 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core",
"subtle",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
@@ -641,6 +726,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
"zeroize",
]
[[package]]
@@ -664,6 +750,17 @@ dependencies = [
"polyval",
]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -927,6 +1024,44 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "p384"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "p521"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
dependencies = [
"base16ct",
"ecdsa",
"elliptic-curve",
"primeorder",
"rand_core",
"sha2",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -960,6 +1095,15 @@ dependencies = [
"hmac",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "pin-project"
version = "1.1.12"
@@ -1039,6 +1183,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
@@ -1118,6 +1271,16 @@ dependencies = [
"bitflags",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "rsa"
version = "0.9.10"
@@ -1132,6 +1295,7 @@ dependencies = [
"pkcs1",
"pkcs8",
"rand_core",
"sha2",
"signature",
"spki",
"subtle",
@@ -1247,6 +1411,20 @@ dependencies = [
"sha2",
]
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "secrecy"
version = "0.10.3"
@@ -1362,6 +1540,48 @@ dependencies = [
"der",
]
[[package]]
name = "ssh-cipher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
dependencies = [
"cipher",
"ssh-encoding",
]
[[package]]
name = "ssh-encoding"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
dependencies = [
"base64ct",
"pem-rfc7468",
"sha2",
]
[[package]]
name = "ssh-key"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
dependencies = [
"ed25519-dalek",
"p256",
"p384",
"p521",
"rand_core",
"rsa",
"sec1",
"sha2",
"signature",
"ssh-cipher",
"ssh-encoding",
"subtle",
"zeroize",
]
[[package]]
name = "strsim"
version = "0.11.1"
+2
View File
@@ -7,4 +7,6 @@ edition = "2024"
age = { version = "0.11.3", features = ["ssh"] }
clap = { version = "4.6.1", features = ["derive"] }
flate2 = "1.1.9"
rand = "0.8"
ssh-key = { version = "0.6", features = ["ed25519", "getrandom"] }
tar = "0.4.45"
+12
View File
@@ -0,0 +1,12 @@
# Phase 6
For phase 6, the app is almost finished but I want to include a couple quality of life features:
- If the user runs `drawer open somedrawer.drawer`, this is currently an error because there is no target path given.
If the file basename of the drawer path would be a valid target path (`./somedrawer` in this case) then use that as the
target path, but also print to stdout that that's what it's using. This should still not open the drawer if the target
path exists and is nonempty, unless the user passes `-f`
- Add a new command, `drawer key <filename>` which will create a new key and store it in the given filename. Before
creating the new key, make sure the filename does not exist and is writable. This will need a good source of entropy to
generate a key, so as part of your plan, recommend one.
+5 -1
View File
@@ -17,6 +17,8 @@ pub enum DrawerError {
EncryptFailed,
DecryptFailed(PathBuf, PathBuf),
UntarFailed,
KeyOutputFailed(PathBuf),
KeyGenerateFailed,
}
impl fmt::Display for DrawerError {
@@ -24,7 +26,7 @@ impl fmt::Display for DrawerError {
match self {
DrawerError::ParseError(s) => write!(f, "{s}"),
DrawerError::NoKeyPath => write!(f, "no key specified: set $DRAWER_KEY or use -i"),
DrawerError::DrawerFileInvalidExtension(p) => write!(f, "drawer file must have a .drawer extension: {}", p.display()),
DrawerError::DrawerFileInvalidExtension(p) => write!(f, "drawer file must end in .drawer: {}", p.display()),
DrawerError::DrawerFileNotFound(p) => write!(f, "drawer file not found: {}", p.display()),
DrawerError::TargetInvalid(p) => write!(f, "target path does not exist, is not a directory, or is not readable: {}", p.display()),
DrawerError::KeyInvalid(p) => write!(f, "key not found or not readable: {}", p.display()),
@@ -36,6 +38,8 @@ impl fmt::Display for DrawerError {
DrawerError::EncryptFailed => write!(f, "encryption failed"),
DrawerError::DecryptFailed(drawer, key) => write!(f, "failed to decrypt {} with key {}", drawer.display(), key.display()),
DrawerError::UntarFailed => write!(f, "drawer file does not contain a valid archive"),
DrawerError::KeyOutputFailed(p) => write!(f, "cannot create key file: {} (already exists or directory is not writable)", p.display()),
DrawerError::KeyGenerateFailed => write!(f, "failed to generate key"),
}
}
}
+17
View File
@@ -31,6 +31,7 @@ mod tests {
drawer_file: "blah.drawer".into(),
key: Path("foo.key".into()),
target_path: "blah".into(),
target_inferred: false,
force: false,
}
))
@@ -45,6 +46,22 @@ mod tests {
drawer_file: "blah.drawer".into(),
key: Path("foo.key".into()),
target_path: "blah".into(),
target_inferred: false,
force: false,
}
))
}
#[test]
fn test_inferred_open() {
let op = parse_args(vec!["drawer", "open", "blah.drawer", "-i", "foo.key"]);
assert_eq!(op, Ok(
DrawerOperation {
operation_type: Open,
drawer_file: "blah.drawer".into(),
key: Path("foo.key".into()),
target_path: "blah".into(),
target_inferred: true,
force: false,
}
))
+61 -6
View File
@@ -6,6 +6,7 @@ use flate2::Compression;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use clap::{Parser, Subcommand};
use ssh_key::{Algorithm, LineEnding, PrivateKey};
use crate::error::DrawerError;
use crate::parsing::KeyType::*;
@@ -13,6 +14,7 @@ use crate::parsing::KeyType::*;
pub enum OperationType {
Open,
Close,
Key,
}
#[derive(Debug, PartialEq)]
@@ -22,6 +24,7 @@ pub struct DrawerOperation {
pub key: KeyType,
pub target_path: PathBuf,
pub force: bool,
pub target_inferred: bool,
}
#[derive(Debug, PartialEq)]
@@ -52,6 +55,9 @@ enum Command {
#[arg(short = 'i')]
key: Option<PathBuf>,
},
Key {
filename: PathBuf,
},
}
pub fn parse_args<I, T>(args: I) -> Result<DrawerOperation, DrawerError>
@@ -65,15 +71,17 @@ where
match cli.command {
Command::Open { drawer_file, target_path, key, force } => {
let key = resolve_key(key);
let target = target_path.unwrap_or_else(|| {
PathBuf::from(drawer_file.file_stem().unwrap_or_default())
});
let (target, target_inferred) = match target_path {
Some(p) => (p, false),
None => (PathBuf::from(drawer_file.file_stem().unwrap_or_default()), true),
};
Ok(DrawerOperation {
operation_type: OperationType::Open,
drawer_file,
key,
target_path: target,
force,
target_inferred,
})
}
Command::Close { drawer_file, target_path, key } => {
@@ -85,6 +93,17 @@ where
key,
target_path: target,
force: false,
target_inferred: false,
})
}
Command::Key { filename } => {
Ok(DrawerOperation {
operation_type: OperationType::Key,
drawer_file: filename,
key: KeyType::EnvVar,
target_path: PathBuf::from("."),
force: false,
target_inferred: false,
})
}
}
@@ -103,10 +122,14 @@ impl DrawerOperation {
match self.operation_type {
OperationType::Open => self.perform_open(),
OperationType::Close => self.perform_close(),
OperationType::Key => self.perform_key(),
}
}
fn perform_open(&self) -> Result<(), DrawerError> {
if self.target_inferred {
println!("(assuming into {})", self.target_path.display());
}
let encrypted = std::fs::read(&self.drawer_file)
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
@@ -179,16 +202,39 @@ impl DrawerOperation {
}
pub fn validate(&self) -> Result<(), DrawerError> {
if self.drawer_file.extension().and_then(|e| e.to_str()) != Some("drawer") {
return Err(DrawerError::DrawerFileInvalidExtension(self.drawer_file.clone()));
if self.operation_type != OperationType::Key {
if self.drawer_file.extension().and_then(|e| e.to_str()) != Some("drawer") {
return Err(DrawerError::DrawerFileInvalidExtension(self.drawer_file.clone()));
}
self.validate_key()?;
}
self.validate_key()?;
match self.operation_type {
OperationType::Open => self.validate_open(),
OperationType::Close => self.validate_close(),
OperationType::Key => self.validate_key_gen(),
}
}
fn validate_key_gen(&self) -> Result<(), DrawerError> {
if self.drawer_file.exists() {
return Err(DrawerError::KeyOutputFailed(self.drawer_file.clone()));
}
let parent = match self.drawer_file.parent() {
None => Path::new("."),
Some(p) => {
if p == Path::new("") {
Path::new(".")
} else {
p
}
},
};
if !parent.exists() {
return Err(DrawerError::KeyOutputFailed(self.drawer_file.clone()));
}
Ok(())
}
fn key_path(&self) -> Result<PathBuf, DrawerError> {
match &self.key {
EnvVar => {
@@ -225,6 +271,15 @@ impl DrawerOperation {
Ok(())
}
fn perform_key(&self) -> Result<(), DrawerError> {
let private_key = PrivateKey::random(&mut rand::rngs::OsRng, Algorithm::Ed25519)
.map_err(|_| DrawerError::KeyGenerateFailed)?;
let pem = private_key.to_openssh(LineEnding::LF)
.map_err(|_| DrawerError::KeyGenerateFailed)?;
std::fs::write(&self.drawer_file, pem.as_bytes())
.map_err(|_| DrawerError::KeyOutputFailed(self.drawer_file.clone()))
}
fn validate_close(&self) -> Result<(), DrawerError> {
let meta = std::fs::metadata(&self.target_path)
.map_err(|_| DrawerError::TargetInvalid(self.target_path.clone()))?;