From b4ea1ed372ab2da4c2766ec62a970bc13386de33 Mon Sep 17 00:00:00 2001 From: oupson Date: Thu, 11 Aug 2022 15:41:42 +0200 Subject: [PATCH] Add GocryptFs to simplify usage --- .gitignore | 3 +- Cargo.lock | 1 + rustcryptfs-lib/Cargo.toml | 1 + rustcryptfs-lib/src/config.rs | 46 +++++++++++--- rustcryptfs-lib/src/filename.rs | 105 -------------------------------- rustcryptfs-lib/src/lib.rs | 53 +++++++++++++++- rustcryptfs/src/args.rs | 12 ++-- rustcryptfs/src/main.rs | 61 ++++++++----------- 8 files changed, 125 insertions(+), 157 deletions(-) delete mode 100644 rustcryptfs-lib/src/filename.rs diff --git a/.gitignore b/.gitignore index 28e0a97..d87d379 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target *.py -enc \ No newline at end of file +enc +mount \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3e53ed3..6d5298e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,7 @@ dependencies = [ "hkdf", "scrypt", "serde", + "serde_json", "sha2", "thiserror", ] diff --git a/rustcryptfs-lib/Cargo.toml b/rustcryptfs-lib/Cargo.toml index 3f9816e..0f14533 100644 --- a/rustcryptfs-lib/Cargo.toml +++ b/rustcryptfs-lib/Cargo.toml @@ -16,3 +16,4 @@ sha2 = "0.10" cipher = "0.4" eme-mode = "0.3" thiserror = "1.0" +serde_json = "1.0" \ No newline at end of file diff --git a/rustcryptfs-lib/src/config.rs b/rustcryptfs-lib/src/config.rs index c4ccffd..97b2f2e 100644 --- a/rustcryptfs-lib/src/config.rs +++ b/rustcryptfs-lib/src/config.rs @@ -1,3 +1,5 @@ +//! Utilities to read gocryptfs config. + use std::collections::HashSet; use aes_gcm::{ @@ -7,8 +9,9 @@ use aes_gcm::{ }; use hkdf::Hkdf; -use crate::error::{Result, FilenameDecryptError, ScryptError}; +use crate::error::{FilenameDecryptError, Result, ScryptError}; +/// An enum that contain all the feature flag a gocryptfs config can have. #[derive(serde::Deserialize, Debug, PartialEq, Eq, Hash)] pub enum FeatureFlag { /// FlagPlaintextNames indicates that filenames are unencrypted. @@ -53,7 +56,7 @@ pub struct CryptConf { #[serde(rename = "EncryptedKey")] encrypted_key: String, #[serde(rename = "ScryptObject")] - pub scrypt_object: ScryptObject, + scrypt_object: ScryptObject, #[serde(rename = "Version")] version: u8, #[serde(rename = "FeatureFlags")] @@ -61,7 +64,12 @@ pub struct CryptConf { } impl CryptConf { - pub fn get_master_key(&self, password: &[u8]) -> Result> { + /// Get the masterkey from configuration. + /// + /// See gocryptfs documentation about [master key](https://nuetzlich.net/gocryptfs/forward_mode_crypto/#master-key-storage). + /// + /// ![TODO NAME THIS IMAGE](https://nuetzlich.net/gocryptfs/img/master-key.svg) + pub fn get_master_key(&self, password: &[u8]) -> Result<[u8; 32]> { let block = base64::decode(&self.encrypted_key)?; let key = self.scrypt_object.get_hkdf_key(password)?; @@ -69,7 +77,7 @@ impl CryptConf { let tag = &block[block.len() - 16..]; let ciphertext = &block[16..block.len() - 16]; - let mut buf = Vec::from(ciphertext); + let mut buf: [u8; 32] = ciphertext.try_into().expect("TODO"); let aes = AesGcm::::new(Key::from_slice(&key)); @@ -86,6 +94,28 @@ impl CryptConf { pub fn have_feature_flag(&self, flag: &FeatureFlag) -> bool { self.feature_flags.contains(flag) } + + /// Get the gocryptfs encrypted directory creator. + pub fn creator(&self) -> &str { + self.creator.as_ref() + } + + /// Get the gocryptfs.conf encrypted key. + pub fn encrypted_key(&self) -> &str { + self.encrypted_key.as_ref() + } + + pub fn scrypt_object(&self) -> &ScryptObject { + &self.scrypt_object + } + + pub fn version(&self) -> u8 { + self.version + } + + pub fn feature_flags(&self) -> &HashSet { + &self.feature_flags + } } #[derive(serde::Deserialize, Debug)] @@ -103,17 +133,19 @@ pub struct ScryptObject { } impl ScryptObject { - pub fn get_hkdf_key(&self, password: &[u8]) -> std::result::Result, FilenameDecryptError> { + fn get_hkdf_key(&self, password: &[u8]) -> std::result::Result, FilenameDecryptError> { let mut key = [0u8; 32]; - let params = scrypt::Params::new((self.n as f64).log2() as u8, self.r, self.p).map_err(|e| ScryptError::from(e))?; + let params = scrypt::Params::new((self.n as f64).log2() as u8, self.r, self.p) + .map_err(|e| ScryptError::from(e))?; scrypt::scrypt( password, &base64::decode(&self.salt).unwrap(), ¶ms, &mut key, - ).map_err(|e| ScryptError::from(e))?; + ) + .map_err(|e| ScryptError::from(e))?; let hdkf = Hkdf::::new(None, &key); hdkf.expand(b"AES-GCM file content encryption", &mut key)?; diff --git a/rustcryptfs-lib/src/filename.rs b/rustcryptfs-lib/src/filename.rs deleted file mode 100644 index 275d514..0000000 --- a/rustcryptfs-lib/src/filename.rs +++ /dev/null @@ -1,105 +0,0 @@ -use aes::Aes256; -use cipher::{block_padding::Pkcs7, inout::InOutBufReserved, Iv, Key, KeyIvInit}; -use eme_mode::DynamicEme; -use hkdf::Hkdf; - -use crate::error::FilenameDecryptError; - -pub(crate) type EmeCipher = DynamicEme; - -// TODO RENAME -pub struct FilenameDecoder { - filename_key: Key, -} - -impl FilenameDecoder { - pub fn new(master_key: &[u8]) -> Result { - let mut key = [0u8; 32]; - let hdkf = Hkdf::::new(None, &master_key); - hdkf.expand(b"EME filename encryption", &mut key)?; - - Ok(Self { - filename_key: Key::::from(key), - }) - } - - pub fn get_decoder_for_dir<'a, 'b>(&'a self, iv: &'b [u8]) -> DirFilenameDecoder<'a, 'b> { - let iv = Iv::::from_slice(iv); - DirFilenameDecoder { - filename_key: &self.filename_key, - iv, - } - } -} - -// TODO RENAME -pub struct DirFilenameDecoder<'a, 'b> { - filename_key: &'a Key, - iv: &'b Iv, -} - -impl<'a, 'b> DirFilenameDecoder<'a, 'b> { - pub fn decode_filename(&self, name: &str) -> Result { - let cipher = EmeCipher::new(self.filename_key, self.iv); - - let mut filename = base64::decode_config(name, base64::URL_SAFE_NO_PAD)?; - let filename_decoded = cipher - .decrypt_padded_mut::(&mut filename) - .map_err(|_| FilenameDecryptError::DecryptError())?; - - Ok(String::from_utf8_lossy(filename_decoded).to_string()) - } - - pub fn encrypt_filename(&self, plain_text_name: &str) -> Result { - let mut cipher = EmeCipher::new(self.filename_key, self.iv); - let mut res = [0u8; 2048]; - - let filename_encrypted = cipher - .encrypt_padded_inout_mut::( - InOutBufReserved::from_slices(plain_text_name.as_bytes(), &mut res).unwrap(), - ) - .map_err(|_| FilenameDecryptError::DecryptError())?; // TODO RENAME ERROR - - // TODO LONG FILENAME - - Ok(base64::encode_config( - filename_encrypted, - base64::URL_SAFE_NO_PAD, - )) - } -} - -#[cfg(test)] -mod test { - use super::FilenameDecoder; - - #[test] - fn test_encrypt() { - let master_key = base64::decode("9gtUW9XiiefEgEXEkbONI6rnUsd2yh5UZZLG0V8Bxgk=").unwrap(); - let dir_iv = base64::decode("6ysCeWOp2euF1x39gth8KQ==").unwrap(); - - let decoder = FilenameDecoder::new(&master_key).expect("Failed to get file decoder"); - let dir_decoder = decoder.get_decoder_for_dir(&dir_iv); - - let encoded = dir_decoder - .encrypt_filename("7.mp4") - .expect("Failed to encrypt filename"); - - assert_eq!(encoded, "vTBajRt-yCpxB7Sly0E7lQ"); - } - - #[test] - fn test_decrypt() { - let master_key = base64::decode("9gtUW9XiiefEgEXEkbONI6rnUsd2yh5UZZLG0V8Bxgk=").unwrap(); - let dir_iv = base64::decode("6ysCeWOp2euF1x39gth8KQ==").unwrap(); - - let decoder = FilenameDecoder::new(&master_key).expect("Failed to get file decoder"); - let dir_decoder = decoder.get_decoder_for_dir(&dir_iv); - - let decrypted = dir_decoder - .decode_filename("vTBajRt-yCpxB7Sly0E7lQ") - .expect("Failed to decrypt filename"); - - assert_eq!(decrypted, "7.mp4"); - } -} diff --git a/rustcryptfs-lib/src/lib.rs b/rustcryptfs-lib/src/lib.rs index 13f3b03..759ebc5 100644 --- a/rustcryptfs-lib/src/lib.rs +++ b/rustcryptfs-lib/src/lib.rs @@ -1,4 +1,55 @@ +//! A library to write gocryptfs compatible programs + +use std::{fs::File, path::Path}; + +use content_enc::ContentEnc; +use filename::FilenameDecoder; + pub mod config; pub mod content_enc; +pub mod error; pub mod filename; -pub mod error; \ No newline at end of file + +/// A GocryptFs encrypted directory +pub struct GocryptFs { + filename_decoder: FilenameDecoder, + content_decoder: ContentEnc, +} + +impl GocryptFs { + /// Open an existing gocryptfs directory + /// + /// The directory must contain a valid `gocryptfs.conf` + pub fn open

(encrypted_dir_path: P, password: &str) -> error::Result + where + P: AsRef, + { + let base_path = encrypted_dir_path.as_ref(); + + let config = { + let mut config_file = + File::open(base_path.join("gocryptfs.conf")).expect("failed to get config"); + + serde_json::from_reader::<_, config::CryptConf>(&mut config_file) + .expect("failed to parse config") + }; + + let master_key = config.get_master_key(password.as_bytes())?; + + let filename_decoder = FilenameDecoder::new(&master_key)?; + let content_decoder = ContentEnc::new(&master_key, 16); // TODO IV LEN + + Ok(Self { + filename_decoder, + content_decoder, + }) + } + + pub fn filename_decoder(&self) -> &FilenameDecoder { + &self.filename_decoder + } + + pub fn content_decoder(&self) -> &ContentEnc { + &self.content_decoder + } +} diff --git a/rustcryptfs/src/args.rs b/rustcryptfs/src/args.rs index 38c0ae0..b57bba8 100644 --- a/rustcryptfs/src/args.rs +++ b/rustcryptfs/src/args.rs @@ -21,9 +21,9 @@ pub(crate) struct DecryptCommand { /// The file to decrypt pub(crate) file_path : String, - /// Path to the gocryptfs.conf - #[clap(short('c'), long)] - pub(crate) gocryptfs_conf_path : Option, + /// Path to the gocryptfs directory + #[clap(short('g'), long)] + pub(crate) gocryptfs_path : Option, /// The password #[clap(short, long)] @@ -35,9 +35,9 @@ pub(crate) struct LsCommand { /// The directory pub(crate) folder_path : String, - /// Path to the gocryptfs.conf - #[clap(short('c'), long)] - pub(crate) gocryptfs_conf_path : Option, + /// Path to the gocryptfs directory + #[clap(short('g'), long)] + pub(crate) gocryptfs_path : Option, /// The password #[clap(short, long)] diff --git a/rustcryptfs/src/main.rs b/rustcryptfs/src/main.rs index 4e0b592..a8e9b1a 100644 --- a/rustcryptfs/src/main.rs +++ b/rustcryptfs/src/main.rs @@ -1,14 +1,13 @@ use std::{ - fs::{self, File}, + fs::File, io::{BufWriter, Read, Write}, - path::{Path, PathBuf}, + path::Path, }; -use anyhow::Context; use clap::Parser; use args::{DecryptCommand, LsCommand}; -use rustcryptfs_lib::{config::{self, CryptConf}, filename::FilenameDecoder, content_enc::ContentEnc}; +use rustcryptfs_lib::GocryptFs; mod args; @@ -25,20 +24,16 @@ fn main() -> anyhow::Result<()> { fn ls(c: &LsCommand) -> anyhow::Result<()> { let folder_path = Path::new(&c.folder_path); - let config_path = c - .gocryptfs_conf_path - .as_ref() - .map(|p| PathBuf::from(p)) - .unwrap_or_else(|| folder_path.join("gocryptfs.conf")); - let content = fs::read_to_string(config_path)?; + let fs = GocryptFs::open( + c.gocryptfs_path + .as_ref() + .map(|p| Path::new(p)) + .unwrap_or(folder_path), + c.password.as_ref().expect("Please input a password"), + )?; - let conf: CryptConf = - serde_json::from_str(&content).context("Failed to decode configuration")?; - - let master_key = conf.get_master_key(c.password.as_ref().unwrap().as_bytes()).context("Failed to get master key")?; - - let filename_decoder = FilenameDecoder::new(&master_key)?; + let filename_decoder = fs.filename_decoder(); let iv = std::fs::read(folder_path.join("gocryptfs.diriv"))?; @@ -48,20 +43,17 @@ fn ls(c: &LsCommand) -> anyhow::Result<()> { let filename = dir.file_name(); let filename = filename.to_str().unwrap(); - if filename != "." - && filename != ".." - && filename != "gocryptfs.conf" - && filename != "gocryptfs.diriv" - { + if filename != "gocryptfs.conf" && filename != "gocryptfs.diriv" { if filename.starts_with("gocryptfs.longname.") { if !filename.ends_with(".name") { - let filename = std::fs::read_to_string(folder_path.join(format!("{}.name", filename)))?; - if let Ok(res) = dir_decoder.decode_filename(&filename) { + let filename = + std::fs::read_to_string(folder_path.join(format!("{}.name", filename)))?; + if let Ok(res) = dir_decoder.decode_filename(filename) { println!("{}", res); } } } else { - if let Ok(res) = dir_decoder.decode_filename(&filename) { + if let Ok(res) = dir_decoder.decode_filename(filename) { println!("{}", res); } }; @@ -73,22 +65,17 @@ fn ls(c: &LsCommand) -> anyhow::Result<()> { fn decrypt_file(c: &DecryptCommand) -> anyhow::Result<()> { let file_path = Path::new(&c.file_path); - let config_path = c - .gocryptfs_conf_path - .as_ref() - .map(|p| PathBuf::from(p)) - .unwrap_or_else(|| file_path.parent().unwrap().join("gocryptfs.conf")); - - let content = fs::read_to_string(config_path)?; - - let conf: config::CryptConf = - serde_json::from_str(&content).context("Failed to decode configuration")?; + let fs = GocryptFs::open( + c.gocryptfs_path + .as_ref() + .map(|p| Path::new(p)) + .unwrap_or_else(|| file_path.parent().unwrap()), + c.password.as_ref().expect("Please input a password"), + )?; let mut file = File::open(file_path).unwrap(); - let master_key = conf.get_master_key(c.password.as_ref().unwrap().as_bytes())?; - - let enc = ContentEnc::new(&master_key, 16); + let enc = fs.content_decoder(); let mut buf = [0u8; 18]; let n = file.read(&mut buf)?;