encrypt index

This commit is contained in:
Cyryl Płotnicki 2021-10-31 00:25:37 +01:00
parent 94495b040f
commit 57b7ddef8b
8 changed files with 263 additions and 81 deletions

94
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "aead"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.12.1" version = "0.12.1"
@ -40,7 +49,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"blake",
"cargo-husky", "cargo-husky",
"chacha20poly1305",
"criterion", "criterion",
"fail", "fail",
"femme", "femme",
@ -87,6 +98,16 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f76cff23583935d01f1d259e546bf988450648a644da066de24717fdd015aa81"
dependencies = [
"gcc",
"libc",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@ -153,6 +174,40 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha20"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91"
dependencies = [
"cfg-if 1.0.0",
"cipher",
"cpufeatures",
"zeroize",
]
[[package]]
name = "chacha20poly1305"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.3" version = "2.33.3"
@ -339,6 +394,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.4"
@ -534,6 +595,17 @@ dependencies = [
"plotters-backend", "plotters-backend",
] ]
[[package]]
name = "poly1305"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.14" version = "0.2.14"
@ -864,6 +936,12 @@ dependencies = [
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.80" version = "1.0.80"
@ -938,6 +1016,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "universal-hash"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
dependencies = [
"generic-array",
"subtle",
]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "0.8.2" version = "0.8.2"
@ -1091,3 +1179,9 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "zeroize"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970"

View file

@ -10,6 +10,8 @@ description = "modern and simple, yet efficient backup solution"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
base64 = "0.13" base64 = "0.13"
blake = "2"
chacha20poly1305 = "0.9"
fail = "0.4" fail = "0.4"
femme = "2" femme = "2"
hex = "0.4" hex = "0.4"

View file

@ -2,9 +2,13 @@ use std::{
collections::HashMap, collections::HashMap,
fs::{self, File}, fs::{self, File},
io::Read, io::Read,
os::unix::prelude::OsStrExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use chacha20poly1305::aead::{Aead, NewAead};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; // Or `XChaCha20Poly1305`
use uuid::Uuid; use uuid::Uuid;
use crate::index::item::IndexItem; use crate::index::item::IndexItem;
@ -18,14 +22,14 @@ use nix::unistd::getpid;
use std::{cmp::max, io::Write}; use std::{cmp::max, io::Write};
impl Index { impl Index {
pub fn load(repository_path: &Path) -> Result<Self> { pub fn load(repository_path: &Path, secret: &[u8]) -> Result<Self> {
if !repository_path.exists() { if !repository_path.exists() {
let mut index = Index::new()?; let mut index = Index::new()?;
index.save(repository_path)?; index.save(repository_path, secret)?;
} }
let lock = Lock::lock(repository_path)?; let lock = Lock::lock(repository_path)?;
let index_file_path = &Index::index_file_path_for_repository_path(repository_path)?; let index_file_path = &Index::index_file_path_for_repository_path(repository_path)?;
let index = Index::load_from_file(index_file_path)?; let index = Index::load_from_file(index_file_path, secret)?;
lock.release()?; lock.release()?;
log::debug!( log::debug!(
"[{}] loaded index from {}, version: {}; {} items", "[{}] loaded index from {}, version: {}; {} items",
@ -37,19 +41,19 @@ impl Index {
Ok(index) Ok(index)
} }
pub fn save(&mut self, repository_path: &Path) -> Result<()> { pub fn save(&mut self, repository_path: &Path, secret: &[u8]) -> Result<()> {
let lock_id = Uuid::new_v4(); let lock_id = Uuid::new_v4();
let lock = Lock::lock(repository_path)?; let lock = Lock::lock(repository_path)?;
let index_file_path = &Index::index_file_path_for_repository_path(repository_path)?; let index_file_path = &Index::index_file_path_for_repository_path(repository_path)?;
if index_file_path.exists() { if index_file_path.exists() {
let index = Index::load_from_file(&Index::index_file_path_for_repository_path(repository_path)?)?; let index = Index::load_from_file(&Index::index_file_path_for_repository_path(repository_path)?, secret)?;
self.merge_items_by_file_id(index.items_by_file_id); self.merge_items_by_file_id(index.items_by_file_id);
self.merge_newest_items(index.newest_items_by_source_path); self.merge_newest_items(index.newest_items_by_source_path);
self.version = max(self.version, index.version); self.version = max(self.version, index.version);
} }
self.version = self.version.next(); self.version = self.version.next();
self.write_index_to_file(index_file_path)?; self.write_index_to_file(index_file_path, secret)?;
lock.release()?; lock.release()?;
log::debug!( log::debug!(
"[{}] saved index version {} with lock id {} to {}; {} items", "[{}] saved index version {} with lock id {} to {}; {} items",
@ -62,7 +66,7 @@ impl Index {
Ok(()) Ok(())
} }
fn write_index_to_file(&mut self, index_file_path: &Path) -> Result<()> { fn write_index_to_file(&mut self, index_file_path: &Path, secret: &[u8]) -> Result<()> {
let parent = index_file_path.parent(); let parent = index_file_path.parent();
match parent { match parent {
None => Err(anyhow!(format!( None => Err(anyhow!(format!(
@ -75,7 +79,17 @@ impl Index {
let serialised = serde_json::to_string_pretty(&self)?; let serialised = serde_json::to_string_pretty(&self)?;
let bytes = serialised.as_bytes(); let bytes = serialised.as_bytes();
let encoded = error_correcting_encoder::encode(bytes)?;
let mut hash = [0; 32];
blake::hash(256, secret, &mut hash)?;
let key = Key::from_slice(&hash);
let cipher = XChaCha20Poly1305::new(key);
blake::hash(256, index_file_path.as_os_str().as_bytes(), &mut hash)?;
let nonce = XNonce::from_slice(&hash[0..(192 / 8)]);
let encrypted = cipher.encrypt(nonce, bytes.as_ref()).map_err(|e| anyhow!("{}", e))?;
let encoded = error_correcting_encoder::encode(&encrypted)?;
{ {
let mut file = File::create(index_file_path)?; let mut file = File::create(index_file_path)?;
@ -97,13 +111,22 @@ impl Index {
} }
} }
fn load_from_file(index_file_path: &Path) -> Result<Self> { fn load_from_file(index_file_path: &Path, secret: &[u8]) -> Result<Self> {
let mut file = File::open(index_file_path)?; let mut file = File::open(index_file_path)?;
let mut encoded = vec![]; let mut encoded = vec![];
file.read_to_end(&mut encoded)?; file.read_to_end(&mut encoded)?;
let decoded = error_correcting_encoder::decode(&encoded)?; let decoded = error_correcting_encoder::decode(&encoded)?;
let index_text = String::from_utf8(decoded)?;
let mut hash = [0; 32];
blake::hash(256, secret, &mut hash)?;
let key = Key::from_slice(&hash);
let cipher = XChaCha20Poly1305::new(key);
blake::hash(256, index_file_path.as_os_str().as_bytes(), &mut hash)?;
let nonce = XNonce::from_slice(&hash[0..(192 / 8)]);
let decrypted = cipher.decrypt(nonce, decoded.as_ref()).map_err(|e| anyhow!("{}", e))?;
let index_text = String::from_utf8(decrypted)?;
let index: Index = serde_json::from_str(&index_text) let index: Index = serde_json::from_str(&index_text)
.context(format!("cannot read index from: {}", index_file_path.to_string_lossy()))?; .context(format!("cannot read index from: {}", index_file_path.to_string_lossy()))?;
@ -144,7 +167,8 @@ mod must {
let mut index = Index::new()?; let mut index = Index::new()?;
let old_version = index.version; let old_version = index.version;
index.save(temp_dir.path())?; let secret = b"some secret";
index.save(temp_dir.path(), secret)?;
let new_version = index.version; let new_version = index.version;
@ -155,11 +179,13 @@ mod must {
#[test] #[test]
fn be_same_when_loaded_from_disk() -> Result<()> { fn be_same_when_loaded_from_disk() -> Result<()> {
femme::with_level(log::LevelFilter::Debug);
let repository_path = tempdir()?; let repository_path = tempdir()?;
let mut original = Index::new()?; let mut original = Index::new()?;
original.save(repository_path.path())?; let secret = b"some secret";
let loaded = Index::load(repository_path.path())?; original.save(repository_path.path(), secret)?;
let loaded = Index::load(repository_path.path(), secret)?;
assert_eq!(original, loaded); assert_eq!(original, loaded);

View file

@ -26,6 +26,7 @@ pub struct Repository {
/// path to where the repository is stored on disk /// path to where the repository is stored on disk
path: PathBuf, path: PathBuf,
index: Index, index: Index,
secret: String,
} }
const DATA_DIR_NAME: &str = "data"; const DATA_DIR_NAME: &str = "data";
@ -97,20 +98,21 @@ impl Debug for ItemId {
} }
impl<'a> Repository { impl<'a> Repository {
pub fn init(path: &Path) -> Result<Repository> { pub fn init(path: &Path, secret: &str) -> Result<Repository> {
fs::create_dir_all(path)?; fs::create_dir_all(path)?;
let mut index = Index::new()?; let mut index = Index::new()?;
index.save(path)?; index.save(path, secret.as_bytes())?;
let repository = Repository::open(path)?; let repository = Repository::open(path, secret)?;
fs::create_dir_all(repository.data_dir()?)?; fs::create_dir_all(repository.data_dir()?)?;
Ok(repository) Ok(repository)
} }
pub fn open(path: &Path) -> Result<Repository> { pub fn open(path: &Path, secret: &str) -> Result<Repository> {
let index = Index::load(path)?; let index = Index::load(path, secret.as_bytes())?;
let repository = Repository { let repository = Repository {
path: path.to_path_buf(), path: path.to_path_buf(),
index, index,
secret: secret.to_owned(),
}; };
Ok(repository) Ok(repository)
@ -121,7 +123,7 @@ impl<'a> Repository {
} }
pub fn save_index(&mut self) -> Result<()> { pub fn save_index(&mut self) -> Result<()> {
self.index.save(&self.path) self.index.save(&self.path, self.secret.as_bytes())
} }
pub fn store(&mut self, source_path: &Path) -> Result<()> { pub fn store(&mut self, source_path: &Path) -> Result<()> {
@ -232,9 +234,10 @@ mod must {
let file_size2 = 27; let file_size2 = 27;
let source = TestSource::new()?; let source = TestSource::new()?;
let repository_path = tempdir()?; let repository_path = tempdir()?;
Repository::init(repository_path.path())?; let secret = "some secret";
Repository::init(repository_path.path(), secret)?;
let mut backup_repository = Repository::open(repository_path.path())?; let mut backup_repository = Repository::open(repository_path.path(), secret)?;
source.write_random_bytes_to_file("file1", file_size1)?; source.write_random_bytes_to_file("file1", file_size1)?;
backup_repository.store(&source.file_path("file1")?)?; backup_repository.store(&source.file_path("file1")?)?;

View file

@ -19,15 +19,15 @@ pub mod in_memory {
pub fn assert_same_after_restore(source_path: &Path) -> Result<()> { pub fn assert_same_after_restore(source_path: &Path) -> Result<()> {
let repository_path = tempdir()?; let repository_path = tempdir()?;
let restore_target = tempdir()?; let restore_target = tempdir()?;
let secret = "some secret";
Repository::init(repository_path.path())?; Repository::init(repository_path.path(), secret)?;
{ {
let mut backup_repository = Repository::open(repository_path.path())?; let mut backup_repository = Repository::open(repository_path.path(), secret)?;
let mut backup_engine = backup::Engine::new(source_path, &mut backup_repository)?; let mut backup_engine = backup::Engine::new(source_path, &mut backup_repository)?;
backup_engine.backup()?; backup_engine.backup()?;
} }
{ {
let mut restore_repository = Repository::open(repository_path.path())?; let mut restore_repository = Repository::open(repository_path.path(), secret)?;
let mut restore_engine = restore::Engine::new(&mut restore_repository, restore_target.path())?; let mut restore_engine = restore::Engine::new(&mut restore_repository, restore_target.path())?;
restore_engine.restore_all()?; restore_engine.restore_all()?;
@ -37,8 +37,13 @@ pub mod in_memory {
Ok(()) Ok(())
} }
pub fn assert_restored_file_contents(repository_path: &Path, source_file_full_path: &Path, contents: &[u8]) -> Result<()> { pub fn assert_restored_file_contents(
let mut restore_repository = Repository::open(repository_path)?; repository_path: &Path,
secret: &str,
source_file_full_path: &Path,
contents: &[u8],
) -> Result<()> {
let mut restore_repository = Repository::open(repository_path, secret)?;
let item = restore_repository.newest_item_by_source_path(source_file_full_path)?; let item = restore_repository.newest_item_by_source_path(source_file_full_path)?;
let restore_target = tempdir()?; let restore_target = tempdir()?;
let restore_engine = restore::Engine::new(&mut restore_repository, restore_target.path())?; let restore_engine = restore::Engine::new(&mut restore_repository, restore_target.path())?;
@ -51,11 +56,12 @@ pub mod in_memory {
pub fn assert_restored_from_version_has_contents( pub fn assert_restored_from_version_has_contents(
repository_path: &Path, repository_path: &Path,
secret: &str,
source_file_full_path: &Path, source_file_full_path: &Path,
old_contents: &[u8], old_contents: &[u8],
old_id: &ItemId, old_id: &ItemId,
) -> Result<()> { ) -> Result<()> {
let mut restore_repository = Repository::open(repository_path)?; let mut restore_repository = Repository::open(repository_path, secret)?;
let old_item = restore_repository.item_by_id(old_id)?; let old_item = restore_repository.item_by_id(old_id)?;
let restore_target = tempdir()?; let restore_target = tempdir()?;
let restore_engine = restore::Engine::new(&mut restore_repository, restore_target.path())?; let restore_engine = restore::Engine::new(&mut restore_repository, restore_target.path())?;
@ -65,9 +71,9 @@ pub mod in_memory {
assert_target_file_contents(&restored_file_path, old_contents) assert_target_file_contents(&restored_file_path, old_contents)
} }
pub fn newest_item(repository_path: &Path, source_file_full_path: &Path) -> Result<RepositoryItem> { pub fn newest_item(repository_path: &Path, secret: &str, source_file_full_path: &Path) -> Result<RepositoryItem> {
let item = { let item = {
let reading_repository = Repository::open(repository_path)?; let reading_repository = Repository::open(repository_path, secret)?;
let item = reading_repository.newest_item_by_source_path(source_file_full_path)?; let item = reading_repository.newest_item_by_source_path(source_file_full_path)?;
assert!(item.is_some()); assert!(item.is_some());
item.unwrap() item.unwrap()
@ -75,9 +81,9 @@ pub mod in_memory {
Ok(item) Ok(item)
} }
pub fn restore_all_from_reloaded_repository(repository_path: &Path, restore_target: &Path) -> Result<()> { pub fn restore_all_from_reloaded_repository(repository_path: &Path, secret: &str, restore_target: &Path) -> Result<()> {
{ {
let mut restore_repository = Repository::open(repository_path)?; let mut restore_repository = Repository::open(repository_path, secret)?;
let mut restore_engine = restore::Engine::new(&mut restore_repository, restore_target)?; let mut restore_engine = restore::Engine::new(&mut restore_repository, restore_target)?;
restore_engine.restore_all()?; restore_engine.restore_all()?;
Ok(()) Ok(())
@ -87,22 +93,30 @@ pub mod in_memory {
pub fn backup_file_with_text_contents( pub fn backup_file_with_text_contents(
source: &TestSource, source: &TestSource,
repository_path: &Path, repository_path: &Path,
secret: &str,
source_file_relative_path: &str, source_file_relative_path: &str,
contents: &str, contents: &str,
) -> Result<()> { ) -> Result<()> {
{ {
backup_file_with_byte_contents(source, repository_path, source_file_relative_path, contents.as_bytes()) backup_file_with_byte_contents(
source,
repository_path,
secret,
source_file_relative_path,
contents.as_bytes(),
)
} }
} }
pub fn backup_file_with_byte_contents( pub fn backup_file_with_byte_contents(
source: &TestSource, source: &TestSource,
repository_path: &Path, repository_path: &Path,
secret: &str,
source_file_relative_path: &str, source_file_relative_path: &str,
contents: &[u8], contents: &[u8],
) -> Result<()> { ) -> Result<()> {
{ {
let mut backup_repository = Repository::open(repository_path)?; let mut backup_repository = Repository::open(repository_path, secret)?;
let mut backup_engine = backup::Engine::new(source.path(), &mut backup_repository)?; let mut backup_engine = backup::Engine::new(source.path(), &mut backup_repository)?;
source.write_bytes_to_file(source_file_relative_path, contents).unwrap(); source.write_bytes_to_file(source_file_relative_path, contents).unwrap();
@ -111,9 +125,9 @@ pub mod in_memory {
} }
} }
pub fn data_weight(repository_path: &Path) -> Result<u64> { pub fn data_weight(repository_path: &Path, secret: &str) -> Result<u64> {
{ {
let repository = Repository::open(repository_path)?; let repository = Repository::open(repository_path, secret)?;
repository.data_weight() repository.data_weight()
} }
} }

View file

@ -23,18 +23,20 @@ mod must {
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
let repository_path = repository_path.join(&format!("repository-{}", getpid())); let repository_path = repository_path.join(&format!("repository-{}", getpid()));
Repository::init(&repository_path)?; let secret = "some secret";
Repository::init(&repository_path, secret)?;
let parallel_backups_number = 16; let parallel_backups_number = 16;
let files_per_backup_number = 16; let files_per_backup_number = 16;
let total_number_of_files = parallel_backups_number * files_per_backup_number; let total_number_of_files = parallel_backups_number * files_per_backup_number;
let finished_backup_runs = backup_in_parallel(&repository_path, parallel_backups_number, files_per_backup_number)?; let finished_backup_runs =
backup_in_parallel(&repository_path, secret, parallel_backups_number, files_per_backup_number)?;
assert_eq!(finished_backup_runs.len(), parallel_backups_number); assert_eq!(finished_backup_runs.len(), parallel_backups_number);
assert!(data_weight(&repository_path)? > 0); assert!(data_weight(&repository_path, secret)? > 0);
let target_path = tempdir()?; let target_path = tempdir()?;
let all_restored_files = restore_all(&repository_path, target_path.path())?; let all_restored_files = restore_all(&repository_path, secret, target_path.path())?;
assert_eq!(all_restored_files.len(), total_number_of_files); assert_eq!(all_restored_files.len(), total_number_of_files);
assert_all_files_in_place(parallel_backups_number, files_per_backup_number, &all_restored_files)?; assert_all_files_in_place(parallel_backups_number, files_per_backup_number, &all_restored_files)?;
@ -62,6 +64,7 @@ mod must {
fn backup_in_parallel( fn backup_in_parallel(
repository_path: &Path, repository_path: &Path,
secret: &str,
parallel_backups_number: usize, parallel_backups_number: usize,
files_per_backup_number: usize, files_per_backup_number: usize,
) -> Result<Vec<usize>> { ) -> Result<Vec<usize>> {
@ -73,7 +76,7 @@ mod must {
child_pids.push(child); child_pids.push(child);
} }
Ok(ForkResult::Child) => { Ok(ForkResult::Child) => {
backup_process(*task_number, repository_path, files_per_backup_number)?; backup_process(*task_number, repository_path, secret, files_per_backup_number)?;
std::process::exit(0); std::process::exit(0);
} }
@ -93,8 +96,8 @@ mod must {
Ok(task_numbers) Ok(task_numbers)
} }
fn backup_process(task_number: usize, repository_path: &Path, files_per_backup_number: usize) -> Result<()> { fn backup_process(task_number: usize, repository_path: &Path, secret: &str, files_per_backup_number: usize) -> Result<()> {
let mut repository = Repository::open(repository_path)?; let mut repository = Repository::open(repository_path, secret)?;
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let mut backup_engine = backup::Engine::new(source.path(), &mut repository)?; let mut backup_engine = backup::Engine::new(source.path(), &mut repository)?;
for i in 0..files_per_backup_number { for i in 0..files_per_backup_number {
@ -105,8 +108,8 @@ mod must {
Ok(()) Ok(())
} }
fn restore_all(repository_path: &Path, restore_target: &Path) -> Result<Vec<PathBuf>> { fn restore_all(repository_path: &Path, secret: &str, restore_target: &Path) -> Result<Vec<PathBuf>> {
let mut restore_repository = Repository::open(repository_path)?; let mut restore_repository = Repository::open(repository_path, secret)?;
let mut restore_engine = restore::Engine::new(&mut restore_repository, restore_target)?; let mut restore_engine = restore::Engine::new(&mut restore_repository, restore_target)?;
restore_engine.restore_all()?; restore_engine.restore_all()?;
get_sorted_files_recursively(restore_target) get_sorted_files_recursively(restore_target)

View file

@ -11,19 +11,20 @@ mod must {
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path).unwrap(); let secret = "some secret";
assert_eq!(data_weight(repository_path).unwrap(), 0); Repository::init(repository_path, secret).unwrap();
assert_eq!(data_weight(repository_path, secret).unwrap(), 0);
backup_file_with_byte_contents(&source, repository_path, "1", &contents).unwrap(); backup_file_with_byte_contents(&source, repository_path, secret, "1", &contents).unwrap();
let first_weight = data_weight(repository_path).unwrap(); let first_weight = data_weight(repository_path, secret).unwrap();
assert!(first_weight > 0); assert!(first_weight > 0);
backup_file_with_byte_contents(&source, repository_path, "2", &contents).unwrap(); backup_file_with_byte_contents(&source, repository_path, secret, "2", &contents).unwrap();
let second_weight = data_weight(repository_path).unwrap(); let second_weight = data_weight(repository_path, secret).unwrap();
assert_eq!(first_weight, second_weight); assert_eq!(first_weight, second_weight);
assert_restored_file_contents(repository_path, &source.file_path("1").unwrap(), &contents).unwrap(); assert_restored_file_contents(repository_path, secret, &source.file_path("1").unwrap(), &contents).unwrap();
assert_restored_file_contents(repository_path, &source.file_path("2").unwrap(), &contents).unwrap(); assert_restored_file_contents(repository_path, secret, &source.file_path("2").unwrap(), &contents).unwrap();
} }
} }
} }

View file

@ -1,5 +1,8 @@
#[cfg(test)] #[cfg(test)]
mod must { mod must {
use std::fs::File;
use std::io::Read;
use bakare::backup; use bakare::backup;
use bakare::test::assertions::in_memory::*; use bakare::test::assertions::in_memory::*;
use bakare::{repository::Repository, test::source::TestSource}; use bakare::{repository::Repository, test::source::TestSource};
@ -26,18 +29,18 @@ mod must {
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
let restore_target = tempdir()?; let restore_target = tempdir()?;
let secret = "some secret";
Repository::init(repository_path)?; Repository::init(repository_path, secret)?;
let source_file_relative_path = "some file path"; let source_file_relative_path = "some file path";
let original_contents = "some old contents"; let original_contents = "some old contents";
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, original_contents)?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, original_contents)?;
restore_all_from_reloaded_repository(repository_path, restore_target.path())?; restore_all_from_reloaded_repository(repository_path, secret, restore_target.path())?;
let source_file_full_path = &source.file_path(source_file_relative_path)?; let source_file_full_path = &source.file_path(source_file_relative_path)?;
assert_restored_file_contents(repository_path, source_file_full_path, original_contents.as_bytes()) assert_restored_file_contents(repository_path, secret, source_file_full_path, original_contents.as_bytes())
} }
#[test] #[test]
@ -45,21 +48,28 @@ mod must {
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path)?; let secret = "some secret";
Repository::init(repository_path, secret)?;
let source_file_relative_path = "some path"; let source_file_relative_path = "some path";
let source_file_full_path = source.file_path(source_file_relative_path)?; let source_file_full_path = source.file_path(source_file_relative_path)?;
let old_contents = "some old contents"; let old_contents = "some old contents";
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, old_contents)?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, old_contents)?;
let old_item = newest_item(repository_path, &source_file_full_path)?; let old_item = newest_item(repository_path, secret, &source_file_full_path)?;
let old_id = old_item.id(); let old_id = old_item.id();
let new_contents = "totally new contents"; let new_contents = "totally new contents";
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, new_contents)?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, new_contents)?;
assert_restored_from_version_has_contents(repository_path, &source_file_full_path, old_contents.as_bytes(), old_id) assert_restored_from_version_has_contents(
repository_path,
secret,
&source_file_full_path,
old_contents.as_bytes(),
old_id,
)
} }
#[test] #[test]
@ -67,19 +77,20 @@ mod must {
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path)?; let secret = "some secret";
Repository::init(repository_path, secret)?;
let source_file_relative_path = "some path"; let source_file_relative_path = "some path";
let source_file_full_path = source.file_path(source_file_relative_path)?; let source_file_full_path = source.file_path(source_file_relative_path)?;
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, "old")?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, "old")?;
let old_item = newest_item(repository_path, &source_file_full_path)?; let old_item = newest_item(repository_path, secret, &source_file_full_path)?;
let old_version = old_item.version(); let old_version = old_item.version();
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, "new")?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, "new")?;
let new_item = newest_item(repository_path, &source_file_full_path)?; let new_item = newest_item(repository_path, secret, &source_file_full_path)?;
let new_version = new_item.version(); let new_version = new_item.version();
assert!(new_version > old_version); assert!(new_version > old_version);
@ -92,23 +103,25 @@ mod must {
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path)?; let secret = "some secret";
Repository::init(repository_path, secret)?;
let source_file_relative_path = "some path"; let source_file_relative_path = "some path";
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, "old contents")?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, "old contents")?;
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, "newer contents")?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, "newer contents")?;
backup_file_with_text_contents(&source, repository_path, source_file_relative_path, "newest contents")?; backup_file_with_text_contents(&source, repository_path, secret, source_file_relative_path, "newest contents")?;
let source_file_full_path = &source.file_path(source_file_relative_path)?; let source_file_full_path = &source.file_path(source_file_relative_path)?;
assert_restored_file_contents(repository_path, source_file_full_path, b"newest contents") assert_restored_file_contents(repository_path, secret, source_file_full_path, b"newest contents")
} }
#[test] #[test]
fn forbid_backup_of_paths_within_repository() -> Result<()> { fn forbid_backup_of_paths_within_repository() -> Result<()> {
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path)?; let secret = "some secret";
let mut repository = Repository::open(repository_path)?; Repository::init(repository_path, secret)?;
let mut repository = Repository::open(repository_path, secret)?;
let error = backup::Engine::new(repository_path, &mut repository); let error = backup::Engine::new(repository_path, &mut repository);
assert!(error.is_err()); assert!(error.is_err());
@ -121,24 +134,26 @@ mod must {
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path).unwrap(); let secret = "some secret";
Repository::init(repository_path, secret).unwrap();
backup_file_with_text_contents(&source, repository_path, &filename, "some contents").unwrap(); backup_file_with_text_contents(&source, repository_path, secret, &filename, "some contents").unwrap();
let repository = Repository::open(repository_path).unwrap(); let repository = Repository::open(repository_path, secret).unwrap();
let second_file = repository.find_latest_by_path_fragment(&filename).unwrap().unwrap(); let second_file = repository.find_latest_by_path_fragment(&filename).unwrap().unwrap();
assert_eq!(second_file.original_source_path(), source.file_path(&filename).unwrap().as_os_str()); assert_eq!(second_file.original_source_path(), source.file_path(&filename).unwrap().as_os_str());
} }
#[test] #[test]
fn not_leak_file_names(filename in "[a-zA-Z]{4,}"){ fn not_leak_file_names_via_file_names(filename in "[a-zA-Z]{4,}"){
let source = TestSource::new().unwrap(); let source = TestSource::new().unwrap();
let dir = tempdir()?; let dir = tempdir()?;
let repository_path = dir.path(); let repository_path = dir.path();
Repository::init(repository_path).unwrap(); let secret = "some secret";
Repository::init(repository_path, secret).unwrap();
backup_file_with_text_contents(&source, repository_path, &filename, "some contents").unwrap(); backup_file_with_text_contents(&source, repository_path, secret, &filename, "some contents").unwrap();
let walker = WalkDir::new(repository_path); let walker = WalkDir::new(repository_path);
let matching_paths = walker let matching_paths = walker
@ -148,6 +163,30 @@ mod must {
assert_eq!(matching_paths.count(), 0); assert_eq!(matching_paths.count(), 0);
} }
#[test]
fn not_leak_file_names_via_file_contents(test_filename in "[a-zA-Z]{8,}"){
let source = TestSource::new().unwrap();
let dir = tempdir()?;
let repository_path = dir.path();
let secret = "some secret";
Repository::init(repository_path, secret).unwrap();
backup_file_with_text_contents(&source, repository_path, secret, &test_filename, "some contents").unwrap();
let all_repo_files = get_sorted_files_recursively(repository_path).unwrap();
assert!(all_repo_files.len() > 0);
for filepath in all_repo_files {
let mut file = File::open(&filepath).unwrap();
let filename = &filepath.as_os_str().to_string_lossy();
let mut contents = vec![];
file.read_to_end(&mut contents).unwrap();
let test_filename_bytes = test_filename.as_bytes();
let contains = contents.windows(test_filename_bytes.len()).any(move |sub_slice| sub_slice == test_filename_bytes);
assert!(!contains, "file {} in the repository directory contains plain text file name '{}' that was previously backed up", filename, test_filename);
}
}
} }
// TODO: encryption // TODO: encryption
// TODO: resume from sleep while backup in progress // TODO: resume from sleep while backup in progress