diff --git a/CLAUDE.md b/CLAUDE.md index 95c7bd6..26c931b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,6 +106,13 @@ I'm going to add some test data to the project under `tests/` but it's just rand As a minor improvement, make the `perform_close` method also gzip the contents of the tarball. +### Phase 4 + +For phase 4, add encryption. We'll have to add a new error type for the given key not being a valid key for `age`. + +Now, after we create the tar but before we write it to disk, we need to encrypt the array, so that plaintext is never +written to disk. + ## Commands ```bash diff --git a/src/error.rs b/src/error.rs index fa14c36..be45c6b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,8 @@ pub enum DrawerError { DrawerInsideTarget, TarFailed, WriteFailed(PathBuf), + KeyNotValidSsh, + EncryptFailed, } impl fmt::Display for DrawerError { @@ -28,6 +30,8 @@ impl fmt::Display for DrawerError { DrawerError::DrawerInsideTarget => write!(f, "drawer file is inside the target directory"), DrawerError::TarFailed => write!(f, "failed to create tarball"), DrawerError::WriteFailed(p) => write!(f, "failed to write drawer file: {}", p.display()), + DrawerError::KeyNotValidSsh => write!(f, "key is not a valid SSH key"), + DrawerError::EncryptFailed => write!(f, "encryption failed"), } } } diff --git a/src/parsing.rs b/src/parsing.rs index b29c33c..a7a3c67 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -1,4 +1,5 @@ use std::ffi::OsString; +use std::io::Write; use std::path::{Path, PathBuf}; use flate2::Compression; use flate2::write::GzEncoder; @@ -119,7 +120,26 @@ impl DrawerOperation { gz.finish() .map_err(|_| DrawerError::TarFailed)?; } - std::fs::write(&self.drawer_file, &buf) + + let key_path = self.key_path()?; + let file = std::fs::File::open(&key_path) + .map_err(|_| DrawerError::KeyInvalid(key_path.clone()))?; + let reader = std::io::BufReader::new(file); + let identity = age::ssh::Identity::from_buffer(reader, None) + .map_err(|_| DrawerError::EncryptFailed)?; + let recipient = age::ssh::Recipient::try_from(identity) + .map_err(|_| DrawerError::EncryptFailed)?; + + let mut encrypted: Vec = Vec::new(); + let encryptor = age::Encryptor::with_recipients( + std::iter::once(&recipient as &dyn age::Recipient) + ).map_err(|_| DrawerError::EncryptFailed)?; + let mut writer = encryptor.wrap_output(&mut encrypted) + .map_err(|_| DrawerError::EncryptFailed)?; + writer.write_all(&buf).map_err(|_| DrawerError::EncryptFailed)?; + writer.finish().map_err(|_| DrawerError::EncryptFailed)?; + + std::fs::write(&self.drawer_file, &encrypted) .map_err(|_| DrawerError::WriteFailed(self.drawer_file.clone())) } @@ -134,20 +154,28 @@ impl DrawerOperation { } } - fn validate_key(&self) -> Result<(), DrawerError> { - let key_path = match &self.key { + fn key_path(&self) -> Result { + match &self.key { EnvVar => { let val = std::env::var("DRAWER_KEY").map_err(|_| DrawerError::NoKeyPath)?; - PathBuf::from(val) + Ok(PathBuf::from(val)) } - Path(p) => p.clone(), - }; - match std::fs::File::open(&key_path) { - Ok(_) => Ok(()), - Err(_) => Err(DrawerError::KeyInvalid(key_path)), + Path(p) => Ok(p.clone()), } } + fn validate_key(&self) -> Result<(), DrawerError> { + let key_path = self.key_path()?; + let file = std::fs::File::open(&key_path) + .map_err(|_| DrawerError::KeyInvalid(key_path.clone()))?; + let reader = std::io::BufReader::new(file); + let identity = age::ssh::Identity::from_buffer(reader, None) + .map_err(|_| DrawerError::KeyNotValidSsh)?; + age::ssh::Recipient::try_from(identity) + .map_err(|_| DrawerError::KeyNotValidSsh)?; + Ok(()) + } + fn validate_open(&self) -> Result<(), DrawerError> { if !self.drawer_file.exists() { return Err(DrawerError::DrawerFileNotFound(self.drawer_file.clone()));