use rand::{thread_rng, Rng};
use std::{
fs::File,
io::{BufWriter, Cursor, Error as IoError, ErrorKind, Read, Write},
path::Path,
};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use aes::{
cipher::{NewCipher, StreamCipher},
Aes256Ctr,
};
use hkdf::Hkdf;
use hmac::{Hmac, Mac, NewMac};
use pbkdf2::pbkdf2;
use sha2::{Sha256, Sha512};
use tantivy::directory::{
error::{
DeleteError, IOError as TvIoError, LockError, OpenDirectoryError, OpenReadError,
OpenWriteError,
},
AntiCallToken, Directory, DirectoryLock, Lock, ReadOnlySource, TerminatingWrite, WatchCallback,
WatchHandle, WritePtr,
};
use zeroize::Zeroizing;
use crate::index::encrypted_stream::{AesReader, AesWriter};
type KeyBuffer = Zeroizing<Vec<u8>>;
type InitialKeyDerivationResult = (KeyBuffer, KeyBuffer, Vec<u8>);
type KeyDerivationResult = (KeyBuffer, KeyBuffer);
const KEYFILE: &str = "seshat-index.key";
const SALT_SIZE: usize = 16;
const IV_SIZE: usize = 16;
const KEY_SIZE: usize = 32;
const MAC_LENGTH: usize = 32;
const VERSION: u8 = 1;
#[cfg(test)]
pub(crate) const PBKDF_COUNT: u32 = 10;
#[cfg(not(test))]
pub(crate) const PBKDF_COUNT: u32 = 10_000;
#[derive(Clone, Debug)]
pub struct EncryptedMmapDirectory {
mmap_dir: tantivy::directory::MmapDirectory,
encryption_key: KeyBuffer,
mac_key: KeyBuffer,
}
impl EncryptedMmapDirectory {
fn new(store_key: KeyBuffer, path: &Path) -> Result<Self, OpenDirectoryError> {
let (encryption_key, mac_key) = EncryptedMmapDirectory::expand_store_key(&store_key)?;
let mmap_dir = tantivy::directory::MmapDirectory::open(&path)?;
Ok(EncryptedMmapDirectory {
mmap_dir,
encryption_key,
mac_key,
})
}
pub fn open_or_create<P: AsRef<Path>>(
path: P,
passphrase: &str,
key_derivation_count: u32,
) -> Result<Self, OpenDirectoryError> {
if passphrase.is_empty() {
return Err(IoError::new(ErrorKind::Other, "empty passphrase").into());
}
if key_derivation_count == 0 {
return Err(IoError::new(ErrorKind::Other, "invalid key derivation count").into());
}
let key_path = path.as_ref().join(KEYFILE);
let key_file = File::open(&key_path);
let store_key = match key_file {
Ok(k) => {
let (_, key) = EncryptedMmapDirectory::load_store_key(k, passphrase)?;
key
}
Err(e) => {
if e.kind() != ErrorKind::NotFound {
return Err(e.into());
}
EncryptedMmapDirectory::create_new_store(
&key_path,
passphrase,
key_derivation_count,
)?
}
};
EncryptedMmapDirectory::new(store_key, path.as_ref())
}
#[allow(dead_code)]
pub fn open<P: AsRef<Path>>(path: P, passphrase: &str) -> Result<Self, OpenDirectoryError> {
if passphrase.is_empty() {
return Err(IoError::new(ErrorKind::Other, "empty passphrase").into());
}
let key_path = path.as_ref().join(KEYFILE);
let key_file = File::open(&key_path)?;
let (_, store_key) = EncryptedMmapDirectory::load_store_key(key_file, passphrase)?;
EncryptedMmapDirectory::new(store_key, path.as_ref())
}
pub fn change_passphrase<P: AsRef<Path>>(
path: P,
old_passphrase: &str,
new_passphrase: &str,
new_key_derivation_count: u32,
) -> Result<(), OpenDirectoryError> {
if old_passphrase.is_empty() || new_passphrase.is_empty() {
return Err(IoError::new(ErrorKind::Other, "empty passphrase").into());
}
if new_key_derivation_count == 0 {
return Err(IoError::new(ErrorKind::Other, "invalid key derivation count").into());
}
let key_path = path.as_ref().join(KEYFILE);
let key_file = File::open(&key_path)?;
let (_, store_key) = EncryptedMmapDirectory::load_store_key(key_file, old_passphrase)?;
let (key, hmac_key, salt) =
EncryptedMmapDirectory::derive_key(new_passphrase, new_key_derivation_count)?;
EncryptedMmapDirectory::encrypt_store_key(
&key,
&salt,
new_key_derivation_count,
&hmac_key,
&store_key,
&key_path,
)?;
Ok(())
}
fn expand_store_key(store_key: &[u8]) -> std::io::Result<KeyDerivationResult> {
let mut hkdf_result = Zeroizing::new([0u8; KEY_SIZE * 2]);
let hkdf = Hkdf::<Sha512>::new(None, store_key);
hkdf.expand(&[], &mut *hkdf_result).map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("unable to expand store key: {:?}", e),
)
})?;
let (key, hmac_key) = hkdf_result.split_at(KEY_SIZE);
Ok((
Zeroizing::new(Vec::from(key)),
Zeroizing::new(Vec::from(hmac_key)),
))
}
fn load_store_key(
mut key_file: File,
passphrase: &str,
) -> Result<(u32, KeyBuffer), OpenDirectoryError> {
let mut iv = [0u8; IV_SIZE];
let mut salt = [0u8; SALT_SIZE];
let mut expected_mac = [0u8; MAC_LENGTH];
let mut version = [0u8; 1];
let mut encrypted_key = vec![];
key_file.read_exact(&mut version)?;
key_file.read_exact(&mut iv)?;
key_file.read_exact(&mut salt)?;
let pbkdf_count = key_file.read_u32::<BigEndian>()?;
key_file.read_exact(&mut expected_mac)?;
key_file
.take(KEY_SIZE as u64)
.read_to_end(&mut encrypted_key)?;
if version[0] != VERSION {
return Err(IoError::new(ErrorKind::Other, "invalid index store version").into());
}
let (key, hmac_key) = EncryptedMmapDirectory::rederive_key(passphrase, &salt, pbkdf_count);
let mac = EncryptedMmapDirectory::calculate_hmac(
version[0],
&iv,
&salt,
&encrypted_key,
&hmac_key,
)?;
if mac.verify(&expected_mac).is_err() {
return Err(IoError::new(ErrorKind::Other, "invalid MAC of the store key").into());
}
let mut decryptor = Aes256Ctr::new_from_slices(&key, &iv).map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("error initializing cipher {:?}", e),
)
})?;
let mut out = Zeroizing::new(encrypted_key);
decryptor.try_apply_keystream(&mut out).map_err(|_| {
IoError::new(
ErrorKind::Other,
"Decryption error, reached end of the keystream.",
)
})?;
Ok((pbkdf_count, out))
}
fn calculate_hmac(
version: u8,
iv: &[u8],
salt: &[u8],
encrypted_data: &[u8],
hmac_key: &[u8],
) -> std::io::Result<Hmac<Sha256>> {
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key)
.map_err(|e| IoError::new(ErrorKind::Other, format!("error creating hmac: {:?}", e)))?;
hmac.update(&[version]);
hmac.update(iv);
hmac.update(salt);
hmac.update(encrypted_data);
Ok(hmac)
}
fn create_new_store(
key_path: &Path,
passphrase: &str,
pbkdf_count: u32,
) -> Result<KeyBuffer, OpenDirectoryError> {
let (key, hmac_key, salt) = EncryptedMmapDirectory::derive_key(passphrase, pbkdf_count)?;
let store_key = EncryptedMmapDirectory::generate_key()?;
EncryptedMmapDirectory::encrypt_store_key(
&key,
&salt,
pbkdf_count,
&hmac_key,
&store_key,
key_path,
)?;
Ok(store_key)
}
fn encrypt_store_key(
key: &[u8],
salt: &[u8],
pbkdf_count: u32,
hmac_key: &[u8],
store_key: &[u8],
key_path: &Path,
) -> Result<(), OpenDirectoryError> {
let iv = EncryptedMmapDirectory::generate_iv()?;
let mut encryptor = Aes256Ctr::new_from_slices(key, &iv).map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("error initializing cipher: {:?}", e),
)
})?;
let mut encrypted_key = [0u8; KEY_SIZE];
encrypted_key.copy_from_slice(store_key);
let mut key_file = File::create(key_path)?;
key_file.write_all(&[VERSION])?;
key_file.write_all(&iv)?;
key_file.write_all(salt)?;
key_file.write_u32::<BigEndian>(pbkdf_count)?;
encryptor
.try_apply_keystream(&mut encrypted_key)
.map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("unable to encrypt store key: {:?}", e),
)
})?;
let mac =
EncryptedMmapDirectory::calculate_hmac(VERSION, &iv, salt, &encrypted_key, hmac_key)?;
let mac = mac.finalize();
let mac = mac.into_bytes();
key_file.write_all(mac.as_slice())?;
key_file.write_all(&encrypted_key)?;
Ok(())
}
fn generate_iv() -> Result<[u8; IV_SIZE], OpenDirectoryError> {
let mut iv = [0u8; IV_SIZE];
let mut rng = thread_rng();
rng.try_fill(&mut iv[..])
.map_err(|e| IoError::new(ErrorKind::Other, format!("error generating iv: {:?}", e)))?;
Ok(iv)
}
fn generate_key() -> Result<KeyBuffer, OpenDirectoryError> {
let mut key = Zeroizing::new(vec![0u8; KEY_SIZE]);
let mut rng = thread_rng();
rng.try_fill(&mut key[..]).map_err(|e| {
IoError::new(ErrorKind::Other, format!("error generating key: {:?}", e))
})?;
Ok(key)
}
fn rederive_key(passphrase: &str, salt: &[u8], pbkdf_count: u32) -> KeyDerivationResult {
let mut pbkdf_result = Zeroizing::new([0u8; KEY_SIZE * 2]);
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt, pbkdf_count, &mut *pbkdf_result);
let (key, hmac_key) = pbkdf_result.split_at(KEY_SIZE);
(
Zeroizing::new(Vec::from(key)),
Zeroizing::new(Vec::from(hmac_key)),
)
}
fn derive_key(
passphrase: &str,
pbkdf_count: u32,
) -> Result<InitialKeyDerivationResult, OpenDirectoryError> {
let mut rng = thread_rng();
let mut salt = vec![0u8; SALT_SIZE];
rng.try_fill(&mut salt[..]).map_err(|e| {
IoError::new(ErrorKind::Other, format!("error generating salt: {:?}", e))
})?;
let (key, hmac_key) = EncryptedMmapDirectory::rederive_key(passphrase, &salt, pbkdf_count);
Ok((key, hmac_key, salt))
}
}
impl Directory for EncryptedMmapDirectory {
fn open_read(&self, path: &Path) -> Result<ReadOnlySource, OpenReadError> {
let source = self.mmap_dir.open_read(path)?;
let mut reader = AesReader::<Aes256Ctr, _>::new::<Hmac<Sha256>>(
Cursor::new(source.as_slice()),
&self.encryption_key,
&self.mac_key,
IV_SIZE,
MAC_LENGTH,
)
.map_err(TvIoError::from)?;
let mut decrypted = Vec::new();
reader
.read_to_end(&mut decrypted)
.map_err(TvIoError::from)?;
Ok(ReadOnlySource::from(decrypted))
}
fn delete(&self, path: &Path) -> Result<(), DeleteError> {
self.mmap_dir.delete(path)
}
fn exists(&self, path: &Path) -> bool {
self.mmap_dir.exists(path)
}
fn open_write(&mut self, path: &Path) -> Result<WritePtr, OpenWriteError> {
let file = match self.mmap_dir.open_write(path)?.into_inner() {
Ok(f) => f,
Err(e) => {
let error = IoError::from(e);
return Err(TvIoError::from(error).into());
}
};
let writer = AesWriter::<Aes256Ctr, Hmac<Sha256>, _>::new(
file,
&self.encryption_key,
&self.mac_key,
IV_SIZE,
)
.map_err(TvIoError::from)?;
Ok(BufWriter::new(Box::new(writer)))
}
fn atomic_read(&self, path: &Path) -> Result<Vec<u8>, OpenReadError> {
let data = self.mmap_dir.atomic_read(path)?;
let mut reader = AesReader::<Aes256Ctr, _>::new::<Hmac<Sha256>>(
Cursor::new(data),
&self.encryption_key,
&self.mac_key,
IV_SIZE,
MAC_LENGTH,
)
.map_err(TvIoError::from)?;
let mut decrypted = Vec::new();
reader
.read_to_end(&mut decrypted)
.map_err(TvIoError::from)?;
Ok(decrypted)
}
fn atomic_write(&mut self, path: &Path, data: &[u8]) -> std::io::Result<()> {
let mut encrypted = Vec::new();
{
let mut writer = AesWriter::<Aes256Ctr, Hmac<Sha256>, _>::new(
&mut encrypted,
&self.encryption_key,
&self.mac_key,
IV_SIZE,
)?;
writer.write_all(data)?;
}
self.mmap_dir.atomic_write(path, &encrypted)
}
fn watch(&self, watch_callback: WatchCallback) -> Result<WatchHandle, tantivy::TantivyError> {
self.mmap_dir.watch(watch_callback)
}
fn acquire_lock(&self, lock: &Lock) -> Result<DirectoryLock, LockError> {
self.mmap_dir.acquire_lock(lock)
}
}
impl<E: NewCipher + StreamCipher, M: Mac + NewMac, W: Write> TerminatingWrite
for AesWriter<E, M, W>
{
fn terminate_ref(&mut self, _: AntiCallToken) -> std::io::Result<()> {
self.finalize()
}
}
#[cfg(test)]
use tempfile::tempdir;
#[test]
fn create_new_store_and_reopen() {
let tmpdir = tempdir().unwrap();
let dir = EncryptedMmapDirectory::open_or_create(tmpdir.path(), "wordpass", PBKDF_COUNT)
.expect("Can't create a new store");
drop(dir);
let dir = EncryptedMmapDirectory::open(tmpdir.path(), "wordpass")
.expect("Can't open the existing store");
drop(dir);
let dir = EncryptedMmapDirectory::open(tmpdir.path(), "password");
assert!(
dir.is_err(),
"Opened an existing store with the wrong passphrase"
);
}
#[test]
fn create_store_with_empty_passphrase() {
let tmpdir = tempdir().unwrap();
let dir = EncryptedMmapDirectory::open(tmpdir.path(), "");
assert!(
dir.is_err(),
"Opened an existing store with the wrong passphrase"
);
}
#[test]
fn change_passphrase() {
let tmpdir = tempdir().unwrap();
let dir = EncryptedMmapDirectory::open_or_create(tmpdir.path(), "wordpass", PBKDF_COUNT)
.expect("Can't create a new store");
drop(dir);
EncryptedMmapDirectory::change_passphrase(tmpdir.path(), "wordpass", "password", PBKDF_COUNT)
.expect("Can't change passphrase");
let dir = EncryptedMmapDirectory::open(tmpdir.path(), "wordpass");
assert!(
dir.is_err(),
"Opened an existing store with the old passphrase"
);
let _ = EncryptedMmapDirectory::open(tmpdir.path(), "password")
.expect("Can't open the store with the new passphrase");
}