1use rand::{thread_rng, Rng};
16use std::{
17 fs::File,
18 io::{BufWriter, Cursor, Error as IoError, ErrorKind, Read, Write},
19 path::Path,
20};
21
22use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
23
24use aes::{
25 cipher::{KeyIvInit, StreamCipher},
26 Aes256,
27};
28use hkdf::Hkdf;
29use hmac::{Hmac, Mac, NewMac};
30use pbkdf2::pbkdf2;
31use sha2::{Sha256, Sha512};
32
33use tantivy::directory::{
34 error::{
35 DeleteError, IOError as TvIoError, LockError, OpenDirectoryError, OpenReadError,
36 OpenWriteError,
37 },
38 AntiCallToken, Directory, DirectoryLock, Lock, ReadOnlySource, TerminatingWrite, WatchCallback,
39 WatchHandle, WritePtr,
40};
41
42use zeroize::Zeroizing;
43
44type Aes256Ctr = ctr::Ctr128BE<Aes256>;
45
46use crate::index::encrypted_stream::{AesReader, AesWriter};
47
48type KeyBuffer = Zeroizing<Vec<u8>>;
51
52type InitialKeyDerivationResult = (KeyBuffer, KeyBuffer, Vec<u8>);
55
56type KeyDerivationResult = (KeyBuffer, KeyBuffer);
59
60const KEYFILE: &str = "seshat-index.key";
64const SALT_SIZE: usize = 16;
66const IV_SIZE: usize = 16;
68const KEY_SIZE: usize = 32;
70const MAC_LENGTH: usize = 32;
72const VERSION: u8 = 1;
74
75#[cfg(test)]
76pub(crate) const PBKDF_COUNT: u32 = 10;
78
79#[cfg(not(test))]
80pub(crate) const PBKDF_COUNT: u32 = 10_000;
86
87#[derive(Clone, Debug)]
88pub struct EncryptedMmapDirectory {
157 mmap_dir: tantivy::directory::MmapDirectory,
158 encryption_key: KeyBuffer,
159 mac_key: KeyBuffer,
160}
161
162impl EncryptedMmapDirectory {
163 fn new(store_key: KeyBuffer, path: &Path) -> Result<Self, OpenDirectoryError> {
164 let (encryption_key, mac_key) = EncryptedMmapDirectory::expand_store_key(&store_key)?;
166
167 let mmap_dir = tantivy::directory::MmapDirectory::open(path)?;
169
170 Ok(EncryptedMmapDirectory {
171 mmap_dir,
172 encryption_key,
173 mac_key,
174 })
175 }
176 pub fn open_or_create<P: AsRef<Path>>(
197 path: P,
198 passphrase: &str,
199 key_derivation_count: u32,
200 ) -> Result<Self, OpenDirectoryError> {
201 if passphrase.is_empty() {
202 return Err(IoError::new(ErrorKind::Other, "empty passphrase").into());
203 }
204
205 if key_derivation_count == 0 {
206 return Err(IoError::new(ErrorKind::Other, "invalid key derivation count").into());
207 }
208
209 let key_path = path.as_ref().join(KEYFILE);
210 let key_file = File::open(&key_path);
211
212 let store_key = match key_file {
215 Ok(k) => {
216 let (_, key) = EncryptedMmapDirectory::load_store_key(k, passphrase)?;
217 key
218 }
219 Err(e) => {
220 if e.kind() != ErrorKind::NotFound {
221 return Err(e.into());
222 }
223 EncryptedMmapDirectory::create_new_store(
224 &key_path,
225 passphrase,
226 key_derivation_count,
227 )?
228 }
229 };
230 EncryptedMmapDirectory::new(store_key, path.as_ref())
231 }
232
233 #[allow(dead_code)]
246 pub fn open<P: AsRef<Path>>(path: P, passphrase: &str) -> Result<Self, OpenDirectoryError> {
247 if passphrase.is_empty() {
248 return Err(IoError::new(ErrorKind::Other, "empty passphrase").into());
249 }
250
251 let key_path = path.as_ref().join(KEYFILE);
252 let key_file = File::open(key_path)?;
253
254 let (_, store_key) = EncryptedMmapDirectory::load_store_key(key_file, passphrase)?;
256 EncryptedMmapDirectory::new(store_key, path.as_ref())
257 }
258
259 pub fn change_passphrase<P: AsRef<Path>>(
270 path: P,
271 old_passphrase: &str,
272 new_passphrase: &str,
273 new_key_derivation_count: u32,
274 ) -> Result<(), OpenDirectoryError> {
275 if old_passphrase.is_empty() || new_passphrase.is_empty() {
276 return Err(IoError::new(ErrorKind::Other, "empty passphrase").into());
277 }
278 if new_key_derivation_count == 0 {
279 return Err(IoError::new(ErrorKind::Other, "invalid key derivation count").into());
280 }
281
282 let key_path = path.as_ref().join(KEYFILE);
283 let key_file = File::open(&key_path)?;
284
285 let (_, store_key) = EncryptedMmapDirectory::load_store_key(key_file, old_passphrase)?;
287 let (key, hmac_key, salt) =
289 EncryptedMmapDirectory::derive_key(new_passphrase, new_key_derivation_count)?;
290 EncryptedMmapDirectory::encrypt_store_key(
292 &key,
293 &salt,
294 new_key_derivation_count,
295 &hmac_key,
296 &store_key,
297 &key_path,
298 )?;
299
300 Ok(())
301 }
302
303 fn expand_store_key(store_key: &[u8]) -> std::io::Result<KeyDerivationResult> {
305 let mut hkdf_result = Zeroizing::new([0u8; KEY_SIZE * 2]);
306
307 let hkdf = Hkdf::<Sha512>::new(None, store_key);
308 hkdf.expand(&[], &mut *hkdf_result).map_err(|e| {
309 IoError::new(
310 ErrorKind::Other,
311 format!("unable to expand store key: {:?}", e),
312 )
313 })?;
314 let (key, hmac_key) = hkdf_result.split_at(KEY_SIZE);
315 Ok((
316 Zeroizing::new(Vec::from(key)),
317 Zeroizing::new(Vec::from(hmac_key)),
318 ))
319 }
320
321 fn load_store_key(
324 mut key_file: File,
325 passphrase: &str,
326 ) -> Result<(u32, KeyBuffer), OpenDirectoryError> {
327 let mut iv = [0u8; IV_SIZE];
328 let mut salt = [0u8; SALT_SIZE];
329 let mut expected_mac = [0u8; MAC_LENGTH];
330 let mut version = [0u8; 1];
331 let mut encrypted_key = vec![];
332
333 key_file.read_exact(&mut version)?;
335 key_file.read_exact(&mut iv)?;
336 key_file.read_exact(&mut salt)?;
337 let pbkdf_count = key_file.read_u32::<BigEndian>()?;
338 key_file.read_exact(&mut expected_mac)?;
339
340 key_file
345 .take(KEY_SIZE as u64)
346 .read_to_end(&mut encrypted_key)?;
347
348 if version[0] != VERSION {
349 return Err(IoError::new(ErrorKind::Other, "invalid index store version").into());
350 }
351
352 let (key, hmac_key) = EncryptedMmapDirectory::rederive_key(passphrase, &salt, pbkdf_count);
354
355 let mac = EncryptedMmapDirectory::calculate_hmac(
357 version[0],
358 &iv,
359 &salt,
360 &encrypted_key,
361 &hmac_key,
362 )?;
363
364 if mac.verify(&expected_mac).is_err() {
365 return Err(IoError::new(ErrorKind::Other, "invalid MAC of the store key").into());
366 }
367
368 let mut decryptor = Aes256Ctr::new_from_slices(&key, &iv).map_err(|e| {
369 IoError::new(
370 ErrorKind::Other,
371 format!("error initializing cipher {:?}", e),
372 )
373 })?;
374
375 let mut out = Zeroizing::new(encrypted_key);
376 decryptor.try_apply_keystream(&mut out).map_err(|_| {
377 IoError::new(
378 ErrorKind::Other,
379 "Decryption error, reached end of the keystream.",
380 )
381 })?;
382
383 Ok((pbkdf_count, out))
384 }
385
386 fn calculate_hmac(
388 version: u8,
389 iv: &[u8],
390 salt: &[u8],
391 encrypted_data: &[u8],
392 hmac_key: &[u8],
393 ) -> std::io::Result<Hmac<Sha256>> {
394 let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key)
395 .map_err(|e| IoError::new(ErrorKind::Other, format!("error creating hmac: {:?}", e)))?;
396 hmac.update(&[version]);
397 hmac.update(iv);
398 hmac.update(salt);
399 hmac.update(encrypted_data);
400 Ok(hmac)
401 }
402
403 fn create_new_store(
406 key_path: &Path,
407 passphrase: &str,
408 pbkdf_count: u32,
409 ) -> Result<KeyBuffer, OpenDirectoryError> {
410 let (key, hmac_key, salt) = EncryptedMmapDirectory::derive_key(passphrase, pbkdf_count)?;
413 let store_key = EncryptedMmapDirectory::generate_key()?;
417
418 EncryptedMmapDirectory::encrypt_store_key(
420 &key,
421 &salt,
422 pbkdf_count,
423 &hmac_key,
424 &store_key,
425 key_path,
426 )?;
427
428 Ok(store_key)
429 }
430
431 fn encrypt_store_key(
433 key: &[u8],
434 salt: &[u8],
435 pbkdf_count: u32,
436 hmac_key: &[u8],
437 store_key: &[u8],
438 key_path: &Path,
439 ) -> Result<(), OpenDirectoryError> {
440 let iv = EncryptedMmapDirectory::generate_iv()?;
442 let mut encryptor = Aes256Ctr::new_from_slices(key, &iv).map_err(|e| {
443 IoError::new(
444 ErrorKind::Other,
445 format!("error initializing cipher: {:?}", e),
446 )
447 })?;
448
449 let mut encrypted_key = [0u8; KEY_SIZE];
450 encrypted_key.copy_from_slice(store_key);
451
452 let mut key_file = File::create(key_path)?;
453
454 key_file.write_all(&[VERSION])?;
457 key_file.write_all(&iv)?;
458 key_file.write_all(salt)?;
459 key_file.write_u32::<BigEndian>(pbkdf_count)?;
460
461 encryptor
463 .try_apply_keystream(&mut encrypted_key)
464 .map_err(|e| {
465 IoError::new(
466 ErrorKind::Other,
467 format!("unable to encrypt store key: {:?}", e),
468 )
469 })?;
470
471 let mac =
474 EncryptedMmapDirectory::calculate_hmac(VERSION, &iv, salt, &encrypted_key, hmac_key)?;
475 let mac = mac.finalize();
476 let mac = mac.into_bytes();
477 key_file.write_all(mac.as_slice())?;
478
479 key_file.write_all(&encrypted_key)?;
481
482 Ok(())
483 }
484
485 fn generate_iv() -> Result<[u8; IV_SIZE], OpenDirectoryError> {
487 let mut iv = [0u8; IV_SIZE];
488 let mut rng = thread_rng();
489 rng.try_fill(&mut iv[..])
490 .map_err(|e| IoError::new(ErrorKind::Other, format!("error generating iv: {:?}", e)))?;
491 Ok(iv)
492 }
493
494 fn generate_key() -> Result<KeyBuffer, OpenDirectoryError> {
496 let mut key = Zeroizing::new(vec![0u8; KEY_SIZE]);
497 let mut rng = thread_rng();
498 rng.try_fill(&mut key[..]).map_err(|e| {
499 IoError::new(ErrorKind::Other, format!("error generating key: {:?}", e))
500 })?;
501 Ok(key)
502 }
503
504 fn rederive_key(passphrase: &str, salt: &[u8], pbkdf_count: u32) -> KeyDerivationResult {
506 let mut pbkdf_result = Zeroizing::new([0u8; KEY_SIZE * 2]);
507
508 pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt, pbkdf_count, &mut *pbkdf_result);
509 let (key, hmac_key) = pbkdf_result.split_at(KEY_SIZE);
510 (
511 Zeroizing::new(Vec::from(key)),
512 Zeroizing::new(Vec::from(hmac_key)),
513 )
514 }
515
516 fn derive_key(
519 passphrase: &str,
520 pbkdf_count: u32,
521 ) -> Result<InitialKeyDerivationResult, OpenDirectoryError> {
522 let mut rng = thread_rng();
523 let mut salt = vec![0u8; SALT_SIZE];
524 rng.try_fill(&mut salt[..]).map_err(|e| {
525 IoError::new(ErrorKind::Other, format!("error generating salt: {:?}", e))
526 })?;
527
528 let (key, hmac_key) = EncryptedMmapDirectory::rederive_key(passphrase, &salt, pbkdf_count);
529 Ok((key, hmac_key, salt))
530 }
531}
532
533impl Directory for EncryptedMmapDirectory {
536 fn open_read(&self, path: &Path) -> Result<ReadOnlySource, OpenReadError> {
537 let source = self.mmap_dir.open_read(path)?;
538
539 let mut reader = AesReader::<Aes256Ctr, _>::new::<Hmac<Sha256>>(
540 Cursor::new(source.as_slice()),
541 &self.encryption_key,
542 &self.mac_key,
543 IV_SIZE,
544 MAC_LENGTH,
545 )
546 .map_err(TvIoError::from)?;
547
548 let mut decrypted = Vec::new();
549 reader
550 .read_to_end(&mut decrypted)
551 .map_err(TvIoError::from)?;
552
553 Ok(ReadOnlySource::from(decrypted))
554 }
555
556 fn delete(&self, path: &Path) -> Result<(), DeleteError> {
557 self.mmap_dir.delete(path)
558 }
559
560 fn exists(&self, path: &Path) -> bool {
561 self.mmap_dir.exists(path)
562 }
563
564 fn open_write(&mut self, path: &Path) -> Result<WritePtr, OpenWriteError> {
565 let file = match self.mmap_dir.open_write(path)?.into_inner() {
566 Ok(f) => f,
567 Err(e) => {
568 let error = IoError::from(e);
569 return Err(TvIoError::from(error).into());
570 }
571 };
572
573 let writer = AesWriter::<Aes256Ctr, Hmac<Sha256>, _>::new(
574 file,
575 &self.encryption_key,
576 &self.mac_key,
577 IV_SIZE,
578 )
579 .map_err(TvIoError::from)?;
580 Ok(BufWriter::new(Box::new(writer)))
581 }
582
583 fn atomic_read(&self, path: &Path) -> Result<Vec<u8>, OpenReadError> {
584 let data = self.mmap_dir.atomic_read(path)?;
585
586 let mut reader = AesReader::<Aes256Ctr, _>::new::<Hmac<Sha256>>(
587 Cursor::new(data),
588 &self.encryption_key,
589 &self.mac_key,
590 IV_SIZE,
591 MAC_LENGTH,
592 )
593 .map_err(TvIoError::from)?;
594 let mut decrypted = Vec::new();
595
596 reader
597 .read_to_end(&mut decrypted)
598 .map_err(TvIoError::from)?;
599 Ok(decrypted)
600 }
601
602 fn atomic_write(&mut self, path: &Path, data: &[u8]) -> std::io::Result<()> {
603 let mut encrypted = Vec::new();
604 {
605 let mut writer = AesWriter::<Aes256Ctr, Hmac<Sha256>, _>::new(
606 &mut encrypted,
607 &self.encryption_key,
608 &self.mac_key,
609 IV_SIZE,
610 )?;
611 writer.write_all(data)?;
612 }
613
614 self.mmap_dir.atomic_write(path, &encrypted)
615 }
616
617 fn watch(&self, watch_callback: WatchCallback) -> Result<WatchHandle, tantivy::TantivyError> {
618 self.mmap_dir.watch(watch_callback)
619 }
620
621 fn acquire_lock(&self, lock: &Lock) -> Result<DirectoryLock, LockError> {
622 self.mmap_dir.acquire_lock(lock)
626 }
627}
628
629impl<E: StreamCipher + KeyIvInit, M: Mac + NewMac, W: Write> TerminatingWrite
632 for AesWriter<E, M, W>
633{
634 fn terminate_ref(&mut self, _: AntiCallToken) -> std::io::Result<()> {
635 self.finalize()
636 }
637}
638
639#[cfg(test)]
640use tempfile::tempdir;
641
642#[test]
643fn create_new_store_and_reopen() {
644 let tmpdir = tempdir().unwrap();
645 let dir = EncryptedMmapDirectory::open_or_create(tmpdir.path(), "wordpass", PBKDF_COUNT)
646 .expect("Can't create a new store");
647 drop(dir);
648 let dir = EncryptedMmapDirectory::open(tmpdir.path(), "wordpass")
649 .expect("Can't open the existing store");
650 drop(dir);
651 let dir = EncryptedMmapDirectory::open(tmpdir.path(), "password");
652 assert!(
653 dir.is_err(),
654 "Opened an existing store with the wrong passphrase"
655 );
656}
657
658#[test]
659fn create_store_with_empty_passphrase() {
660 let tmpdir = tempdir().unwrap();
661 let dir = EncryptedMmapDirectory::open(tmpdir.path(), "");
662 assert!(
663 dir.is_err(),
664 "Opened an existing store with the wrong passphrase"
665 );
666}
667
668#[test]
669fn change_passphrase() {
670 let tmpdir = tempdir().unwrap();
671 let dir = EncryptedMmapDirectory::open_or_create(tmpdir.path(), "wordpass", PBKDF_COUNT)
672 .expect("Can't create a new store");
673
674 drop(dir);
675 EncryptedMmapDirectory::change_passphrase(tmpdir.path(), "wordpass", "password", PBKDF_COUNT)
676 .expect("Can't change passphrase");
677 let dir = EncryptedMmapDirectory::open(tmpdir.path(), "wordpass");
678 assert!(
679 dir.is_err(),
680 "Opened an existing store with the old passphrase"
681 );
682 let _ = EncryptedMmapDirectory::open(tmpdir.path(), "password")
683 .expect("Can't open the store with the new passphrase");
684}