phase 5
This commit is contained in:
@@ -52,66 +52,8 @@ the user (me) will review the code, write tests, and possibly make changes, befo
|
||||
|
||||
At to time should Claude delete or modify any tests whatsoever.
|
||||
|
||||
### Phase 1
|
||||
|
||||
Write code that uses `clap` to parse the command line arguments and create a `DrawerOperation` struct from them. The
|
||||
`DrawerOperation` struct will contain:
|
||||
|
||||
- An `operation_type` (`OperationType`, enum of `Open` or `Close`)
|
||||
- A `drawer_file` (`Path` for the drawer file to open or close)
|
||||
- A `key_path` (`Path` to the key we'll use, which is either the value of the `-i` flag or the value of the `$DRAWER_KEY`
|
||||
env var)
|
||||
- A `target_path` (`Path` to the contents we're going to put into or take out of the drawer)
|
||||
- A `force` (bool for whether or not `-f` was passed)
|
||||
|
||||
In order for this to be testable, it needs to be a function that returns a `Result<DrawerOperation, DrawerError>`. The
|
||||
main function should call that and then do nothing with the result.
|
||||
|
||||
Certain things should cause the function to return `Err`:
|
||||
|
||||
- Not passing a drawer file
|
||||
- Passing a subcommand other than `open` or `close`
|
||||
- Passing any flags but `-f` or `-i`
|
||||
- Not passing `-i` when the `$DRAWER_KEY` environment variable is undefined
|
||||
|
||||
### Phase 2
|
||||
|
||||
Phase 2 is about error handling. We want to add some variants to `DrawerError` and a `validate` method to `DrawerOperation`
|
||||
that will catch various problems and ensure that we can do the operation correctly. Here are some error cases in no particular order:
|
||||
|
||||
- If the command is `Open` and the drawer file does not exist.
|
||||
- If the command is `Close` and the target path either does not exist, is not a directory, or is not readable.
|
||||
- If the `key` is `EnvVar` but the `$DRAWER_KEY` environment variable is not set, or is not set to a path that exists and is readable.
|
||||
- If the `key` is `Path()` but the path does not exist or is not readable.
|
||||
- If the command is `Open` and the target is a non-empty directory that exists, and the `force` flag is false
|
||||
- If the command is `Close` and the target contains the drawer file (at any level; the target is any parent of the drawer file's path)
|
||||
|
||||
The `validate` method should take `&self` and return `Result<(), DrawerError>`. Let's have `main`, which is currently
|
||||
creating a `DrawerOperation` and not using it, also call `validate` and print out the error if there is one. This means
|
||||
we also need to `impl Display` on `DrawerError`.
|
||||
|
||||
### Phase 3
|
||||
|
||||
For phase 3 we want to start implementing the `close` command. We'll get the tar part working but not worry about encryption yet.
|
||||
|
||||
- Make a new method `perform` on `DrawerOperation` which takes `&self` and returns `Result<(), DrawerError>`.
|
||||
- `perform` should first call `validate` and return the error if there is one.
|
||||
- After that, it should call either `open(&self)` or `close(&self)` depending on the operation type. For now, `open` will just be a `todo!()`
|
||||
- The new `close` method should create a vec<u8> consisting of the contents of the target directory as a tarball (use the tar create we depend on)
|
||||
- After creating the vec, it should write it out to the drawer file path.
|
||||
|
||||
I'm going to add some test data to the project under `tests/` but it's just random data to test, ignore the contents of all those files.
|
||||
|
||||
### Phase 3a
|
||||
|
||||
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.
|
||||
The implementation plans for each phase are in markdown files under the `plans/` directory. Claude should read these but
|
||||
should not modify them.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Phase 1
|
||||
|
||||
Write code that uses `clap` to parse the command line arguments and create a `DrawerOperation` struct from them. The
|
||||
`DrawerOperation` struct will contain:
|
||||
|
||||
- An `operation_type` (`OperationType`, enum of `Open` or `Close`)
|
||||
- A `drawer_file` (`Path` for the drawer file to open or close)
|
||||
- A `key_path` (`Path` to the key we'll use, which is either the value of the `-i` flag or the value of the `$DRAWER_KEY`
|
||||
env var)
|
||||
- A `target_path` (`Path` to the contents we're going to put into or take out of the drawer)
|
||||
- A `force` (bool for whether or not `-f` was passed)
|
||||
|
||||
In order for this to be testable, it needs to be a function that returns a `Result<DrawerOperation, DrawerError>`. The
|
||||
main function should call that and then do nothing with the result.
|
||||
|
||||
Certain things should cause the function to return `Err`:
|
||||
|
||||
- Not passing a drawer file
|
||||
- Passing a subcommand other than `open` or `close`
|
||||
- Passing any flags but `-f` or `-i`
|
||||
- Not passing `-i` when the `$DRAWER_KEY` environment variable is undefined
|
||||
@@ -0,0 +1,15 @@
|
||||
# Phase 2
|
||||
|
||||
Phase 2 is about error handling. We want to add some variants to `DrawerError` and a `validate` method to `DrawerOperation`
|
||||
that will catch various problems and ensure that we can do the operation correctly. Here are some error cases in no particular order:
|
||||
|
||||
- If the command is `Open` and the drawer file does not exist.
|
||||
- If the command is `Close` and the target path either does not exist, is not a directory, or is not readable.
|
||||
- If the `key` is `EnvVar` but the `$DRAWER_KEY` environment variable is not set, or is not set to a path that exists and is readable.
|
||||
- If the `key` is `Path()` but the path does not exist or is not readable.
|
||||
- If the command is `Open` and the target is a non-empty directory that exists, and the `force` flag is false
|
||||
- If the command is `Close` and the target contains the drawer file (at any level; the target is any parent of the drawer file's path)
|
||||
|
||||
The `validate` method should take `&self` and return `Result<(), DrawerError>`. Let's have `main`, which is currently
|
||||
creating a `DrawerOperation` and not using it, also call `validate` and print out the error if there is one. This means
|
||||
we also need to `impl Display` on `DrawerError`.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Phase 3
|
||||
|
||||
For phase 3 we want to start implementing the `close` command. We'll get the tar part working but not worry about encryption yet.
|
||||
|
||||
- Make a new method `perform` on `DrawerOperation` which takes `&self` and returns `Result<(), DrawerError>`.
|
||||
- `perform` should first call `validate` and return the error if there is one.
|
||||
- After that, it should call either `open(&self)` or `close(&self)` depending on the operation type. For now, `open` will just be a `todo!()`
|
||||
- The new `close` method should create a vec<u8> consisting of the contents of the target directory as a tarball (use the tar create we depend on)
|
||||
- After creating the vec, it should write it out to the drawer file path.
|
||||
|
||||
I'm going to add some test data to the project under `tests/` but it's just random data to test, ignore the contents of all those files.
|
||||
|
||||
# Phase 3a
|
||||
|
||||
As a minor improvement, make the `perform_close` method also gzip the contents of the tarball.
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Phase 5
|
||||
|
||||
For phase 5, implement the `open` command. This will provide an implementation for the `perform_open` method, and will
|
||||
also need another couple error variants, unless they already exist:
|
||||
|
||||
- The given key might not open the given drawer file
|
||||
- The drawer file might not decrypt to a valid tarball
|
||||
|
||||
The code for the open command is pretty much the same as the close command, in reverse order:
|
||||
|
||||
- Read the drawer file into an array
|
||||
- Decrypt it with the key (failing if it won't decrypt, or if it's not a valid tarball)
|
||||
- Expand it with the `tar` crate into the target path
|
||||
@@ -15,6 +15,8 @@ pub enum DrawerError {
|
||||
WriteFailed(PathBuf),
|
||||
KeyNotValidSsh,
|
||||
EncryptFailed,
|
||||
DecryptFailed(PathBuf, PathBuf),
|
||||
UntarFailed,
|
||||
}
|
||||
|
||||
impl fmt::Display for DrawerError {
|
||||
@@ -32,6 +34,8 @@ impl fmt::Display for DrawerError {
|
||||
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"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-11
@@ -1,7 +1,9 @@
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use age::ssh::Identity;
|
||||
use flate2::Compression;
|
||||
use flate2::read::GzDecoder;
|
||||
use flate2::write::GzEncoder;
|
||||
use clap::{Parser, Subcommand};
|
||||
use crate::error::DrawerError;
|
||||
@@ -105,7 +107,40 @@ impl DrawerOperation {
|
||||
}
|
||||
|
||||
fn perform_open(&self) -> Result<(), DrawerError> {
|
||||
todo!()
|
||||
let encrypted = std::fs::read(&self.drawer_file)
|
||||
.map_err(|_| DrawerError::DrawerFileNotFound(self.drawer_file.clone()))?;
|
||||
|
||||
let decrypted = self.decrypt(encrypted)?;
|
||||
|
||||
let cursor = std::io::Cursor::new(decrypted);
|
||||
let gz = GzDecoder::new(cursor);
|
||||
let mut archive = tar::Archive::new(gz);
|
||||
archive.unpack(&self.target_path)
|
||||
.map_err(|_| DrawerError::UntarFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt(&self, encrypted: Vec<u8>) -> Result<Vec<u8>, DrawerError> {
|
||||
let (key_path, identity) = self.identity()?;
|
||||
|
||||
let decryptor = age::Decryptor::new(encrypted.as_slice())
|
||||
.map_err(|_| DrawerError::DecryptFailed(self.drawer_file.clone(), key_path.clone()))?;
|
||||
let mut decrypted: Vec<u8> = Vec::new();
|
||||
let mut stream = decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity))
|
||||
.map_err(|_| DrawerError::DecryptFailed(self.drawer_file.clone(), key_path.clone()))?;
|
||||
std::io::Read::read_to_end(&mut stream, &mut decrypted)
|
||||
.map_err(|_| DrawerError::DecryptFailed(self.drawer_file.clone(), key_path.clone()))?;
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
fn identity(&self) -> Result<(PathBuf, Identity), 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::DecryptFailed(self.drawer_file.clone(), key_path.clone()))?;
|
||||
Ok((key_path, identity))
|
||||
}
|
||||
|
||||
fn perform_close(&self) -> Result<(), DrawerError> {
|
||||
@@ -121,12 +156,14 @@ impl DrawerOperation {
|
||||
.map_err(|_| DrawerError::TarFailed)?;
|
||||
}
|
||||
|
||||
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 encrypted = self.encrypt(&buf)?;
|
||||
|
||||
std::fs::write(&self.drawer_file, &encrypted)
|
||||
.map_err(|_| DrawerError::WriteFailed(self.drawer_file.clone()))
|
||||
}
|
||||
|
||||
fn encrypt(&self, buf: &[u8]) -> Result<Vec<u8>, DrawerError> {
|
||||
let (_, identity) = self.identity()?;
|
||||
let recipient = age::ssh::Recipient::try_from(identity)
|
||||
.map_err(|_| DrawerError::EncryptFailed)?;
|
||||
|
||||
@@ -136,11 +173,9 @@ impl DrawerOperation {
|
||||
).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.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()))
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), DrawerError> {
|
||||
|
||||
Reference in New Issue
Block a user