Add GocryptFs to simplify usage

This commit is contained in:
oupson 2022-08-11 15:41:42 +02:00
parent c4d1c479aa
commit b4ea1ed372
Signed by: oupson
GPG Key ID: 3BD88615552EFCB7
8 changed files with 125 additions and 157 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target /target
*.py *.py
enc enc
mount

1
Cargo.lock generated
View File

@ -507,6 +507,7 @@ dependencies = [
"hkdf", "hkdf",
"scrypt", "scrypt",
"serde", "serde",
"serde_json",
"sha2", "sha2",
"thiserror", "thiserror",
] ]

View File

@ -16,3 +16,4 @@ sha2 = "0.10"
cipher = "0.4" cipher = "0.4"
eme-mode = "0.3" eme-mode = "0.3"
thiserror = "1.0" thiserror = "1.0"
serde_json = "1.0"

View File

@ -1,3 +1,5 @@
//! Utilities to read gocryptfs config.
use std::collections::HashSet; use std::collections::HashSet;
use aes_gcm::{ use aes_gcm::{
@ -7,8 +9,9 @@ use aes_gcm::{
}; };
use hkdf::Hkdf; 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)] #[derive(serde::Deserialize, Debug, PartialEq, Eq, Hash)]
pub enum FeatureFlag { pub enum FeatureFlag {
/// FlagPlaintextNames indicates that filenames are unencrypted. /// FlagPlaintextNames indicates that filenames are unencrypted.
@ -53,7 +56,7 @@ pub struct CryptConf {
#[serde(rename = "EncryptedKey")] #[serde(rename = "EncryptedKey")]
encrypted_key: String, encrypted_key: String,
#[serde(rename = "ScryptObject")] #[serde(rename = "ScryptObject")]
pub scrypt_object: ScryptObject, scrypt_object: ScryptObject,
#[serde(rename = "Version")] #[serde(rename = "Version")]
version: u8, version: u8,
#[serde(rename = "FeatureFlags")] #[serde(rename = "FeatureFlags")]
@ -61,7 +64,12 @@ pub struct CryptConf {
} }
impl CryptConf { impl CryptConf {
pub fn get_master_key(&self, password: &[u8]) -> Result<Vec<u8>> { /// 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 block = base64::decode(&self.encrypted_key)?;
let key = self.scrypt_object.get_hkdf_key(password)?; let key = self.scrypt_object.get_hkdf_key(password)?;
@ -69,7 +77,7 @@ impl CryptConf {
let tag = &block[block.len() - 16..]; let tag = &block[block.len() - 16..];
let ciphertext = &block[16..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::<Aes256, cipher::consts::U16>::new(Key::from_slice(&key)); let aes = AesGcm::<Aes256, cipher::consts::U16>::new(Key::from_slice(&key));
@ -86,6 +94,28 @@ impl CryptConf {
pub fn have_feature_flag(&self, flag: &FeatureFlag) -> bool { pub fn have_feature_flag(&self, flag: &FeatureFlag) -> bool {
self.feature_flags.contains(flag) 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<FeatureFlag> {
&self.feature_flags
}
} }
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
@ -103,17 +133,19 @@ pub struct ScryptObject {
} }
impl ScryptObject { impl ScryptObject {
pub fn get_hkdf_key(&self, password: &[u8]) -> std::result::Result<Vec<u8>, FilenameDecryptError> { fn get_hkdf_key(&self, password: &[u8]) -> std::result::Result<Vec<u8>, FilenameDecryptError> {
let mut key = [0u8; 32]; 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( scrypt::scrypt(
password, password,
&base64::decode(&self.salt).unwrap(), &base64::decode(&self.salt).unwrap(),
&params, &params,
&mut key, &mut key,
).map_err(|e| ScryptError::from(e))?; )
.map_err(|e| ScryptError::from(e))?;
let hdkf = Hkdf::<sha2::Sha256>::new(None, &key); let hdkf = Hkdf::<sha2::Sha256>::new(None, &key);
hdkf.expand(b"AES-GCM file content encryption", &mut key)?; hdkf.expand(b"AES-GCM file content encryption", &mut key)?;

View File

@ -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<Aes256>;
// TODO RENAME
pub struct FilenameDecoder {
filename_key: Key<Aes256>,
}
impl FilenameDecoder {
pub fn new(master_key: &[u8]) -> Result<Self, FilenameDecryptError> {
let mut key = [0u8; 32];
let hdkf = Hkdf::<sha2::Sha256>::new(None, &master_key);
hdkf.expand(b"EME filename encryption", &mut key)?;
Ok(Self {
filename_key: Key::<EmeCipher>::from(key),
})
}
pub fn get_decoder_for_dir<'a, 'b>(&'a self, iv: &'b [u8]) -> DirFilenameDecoder<'a, 'b> {
let iv = Iv::<EmeCipher>::from_slice(iv);
DirFilenameDecoder {
filename_key: &self.filename_key,
iv,
}
}
}
// TODO RENAME
pub struct DirFilenameDecoder<'a, 'b> {
filename_key: &'a Key<EmeCipher>,
iv: &'b Iv<EmeCipher>,
}
impl<'a, 'b> DirFilenameDecoder<'a, 'b> {
pub fn decode_filename(&self, name: &str) -> Result<String, FilenameDecryptError> {
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::<Pkcs7>(&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<String, FilenameDecryptError> {
let mut cipher = EmeCipher::new(self.filename_key, self.iv);
let mut res = [0u8; 2048];
let filename_encrypted = cipher
.encrypt_padded_inout_mut::<Pkcs7>(
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");
}
}

View File

@ -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 config;
pub mod content_enc; pub mod content_enc;
pub mod error;
pub mod filename; pub mod filename;
pub mod error;
/// 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<P>(encrypted_dir_path: P, password: &str) -> error::Result<Self>
where
P: AsRef<Path>,
{
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
}
}

View File

@ -21,9 +21,9 @@ pub(crate) struct DecryptCommand {
/// The file to decrypt /// The file to decrypt
pub(crate) file_path : String, pub(crate) file_path : String,
/// Path to the gocryptfs.conf /// Path to the gocryptfs directory
#[clap(short('c'), long)] #[clap(short('g'), long)]
pub(crate) gocryptfs_conf_path : Option<String>, pub(crate) gocryptfs_path : Option<String>,
/// The password /// The password
#[clap(short, long)] #[clap(short, long)]
@ -35,9 +35,9 @@ pub(crate) struct LsCommand {
/// The directory /// The directory
pub(crate) folder_path : String, pub(crate) folder_path : String,
/// Path to the gocryptfs.conf /// Path to the gocryptfs directory
#[clap(short('c'), long)] #[clap(short('g'), long)]
pub(crate) gocryptfs_conf_path : Option<String>, pub(crate) gocryptfs_path : Option<String>,
/// The password /// The password
#[clap(short, long)] #[clap(short, long)]

View File

@ -1,14 +1,13 @@
use std::{ use std::{
fs::{self, File}, fs::File,
io::{BufWriter, Read, Write}, io::{BufWriter, Read, Write},
path::{Path, PathBuf}, path::Path,
}; };
use anyhow::Context;
use clap::Parser; use clap::Parser;
use args::{DecryptCommand, LsCommand}; use args::{DecryptCommand, LsCommand};
use rustcryptfs_lib::{config::{self, CryptConf}, filename::FilenameDecoder, content_enc::ContentEnc}; use rustcryptfs_lib::GocryptFs;
mod args; mod args;
@ -25,20 +24,16 @@ fn main() -> anyhow::Result<()> {
fn ls(c: &LsCommand) -> anyhow::Result<()> { fn ls(c: &LsCommand) -> anyhow::Result<()> {
let folder_path = Path::new(&c.folder_path); 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 = let filename_decoder = fs.filename_decoder();
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 iv = std::fs::read(folder_path.join("gocryptfs.diriv"))?; 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 = dir.file_name();
let filename = filename.to_str().unwrap(); let filename = filename.to_str().unwrap();
if filename != "." if filename != "gocryptfs.conf" && filename != "gocryptfs.diriv" {
&& filename != ".."
&& filename != "gocryptfs.conf"
&& filename != "gocryptfs.diriv"
{
if filename.starts_with("gocryptfs.longname.") { if filename.starts_with("gocryptfs.longname.") {
if !filename.ends_with(".name") { if !filename.ends_with(".name") {
let filename = std::fs::read_to_string(folder_path.join(format!("{}.name", filename)))?; let filename =
if let Ok(res) = dir_decoder.decode_filename(&filename) { std::fs::read_to_string(folder_path.join(format!("{}.name", filename)))?;
if let Ok(res) = dir_decoder.decode_filename(filename) {
println!("{}", res); println!("{}", res);
} }
} }
} else { } else {
if let Ok(res) = dir_decoder.decode_filename(&filename) { if let Ok(res) = dir_decoder.decode_filename(filename) {
println!("{}", res); println!("{}", res);
} }
}; };
@ -73,22 +65,17 @@ fn ls(c: &LsCommand) -> anyhow::Result<()> {
fn decrypt_file(c: &DecryptCommand) -> anyhow::Result<()> { fn decrypt_file(c: &DecryptCommand) -> anyhow::Result<()> {
let file_path = Path::new(&c.file_path); let file_path = Path::new(&c.file_path);
let config_path = c let fs = GocryptFs::open(
.gocryptfs_conf_path c.gocryptfs_path
.as_ref() .as_ref()
.map(|p| PathBuf::from(p)) .map(|p| Path::new(p))
.unwrap_or_else(|| file_path.parent().unwrap().join("gocryptfs.conf")); .unwrap_or_else(|| file_path.parent().unwrap()),
c.password.as_ref().expect("Please input a password"),
let content = fs::read_to_string(config_path)?; )?;
let conf: config::CryptConf =
serde_json::from_str(&content).context("Failed to decode configuration")?;
let mut file = File::open(file_path).unwrap(); let mut file = File::open(file_path).unwrap();
let master_key = conf.get_master_key(c.password.as_ref().unwrap().as_bytes())?; let enc = fs.content_decoder();
let enc = ContentEnc::new(&master_key, 16);
let mut buf = [0u8; 18]; let mut buf = [0u8; 18];
let n = file.read(&mut buf)?; let n = file.read(&mut buf)?;