Add GocryptFs to simplify usage
This commit is contained in:
parent
c4d1c479aa
commit
b4ea1ed372
|
@ -1,3 +1,4 @@
|
||||||
/target
|
/target
|
||||||
*.py
|
*.py
|
||||||
enc
|
enc
|
||||||
|
mount
|
|
@ -507,6 +507,7 @@ dependencies = [
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"scrypt",
|
"scrypt",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
|
@ -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(),
|
||||||
¶ms,
|
¶ms,
|
||||||
&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)?;
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
Loading…
Reference in New Issue