Skip to content

Commit

Permalink
Encrypt repo files details data when locked
Browse files Browse the repository at this point in the history
  • Loading branch information
bancek committed Dec 2, 2023
1 parent 10021b6 commit c50e767
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 41 deletions.
97 changes: 93 additions & 4 deletions vault-core-tests/tests/integration/repo_files_details_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ use vault_core::{
self,
state::{
RepoFilesDetails, RepoFilesDetailsContent, RepoFilesDetailsContentData,
RepoFilesDetailsContentLoading, RepoFilesDetailsInfo, RepoFilesDetailsLocation,
RepoFilesDetailsOptions, RepoFilesDetailsState,
RepoFilesDetailsContentDataBytes, RepoFilesDetailsContentLoading, RepoFilesDetailsInfo,
RepoFilesDetailsLocation, RepoFilesDetailsOptions, RepoFilesDetailsState,
},
},
repos,
repos::errors::{RepoInfoError, RepoLockedError, RepoNotFoundError},
store,
transfers::errors::{DownloadableError, TransferError},
Expand All @@ -45,6 +46,10 @@ use vault_store::{test_helpers::StateRecorder, NextId};
fn test_content() {
with_repo(|fixture| {
async move {
let cipher = fixture.vault.store.with_state(|state| {
repos::selectors::select_cipher_owned(state, &fixture.repo_id).unwrap()
});

let (upload_result, _) = fixture.upload_file("/file.txt", "test").await;

// remove file from state so that it is loaded before the content is loaded to prevent flaky tests
Expand Down Expand Up @@ -149,7 +154,10 @@ fn test_content() {
if let Some(location) = details.location.as_mut() {
location.content.status = Status::Loaded;
location.content.data = Some(RepoFilesDetailsContentData {
bytes: "test".as_bytes().to_owned(),
bytes: RepoFilesDetailsContentDataBytes::Decrypted(
"test".as_bytes().to_owned(),
cipher.clone(),
),
remote_size: upload_result.remote_file.size,
remote_modified: upload_result.remote_file.modified,
remote_hash: upload_result.remote_file.hash.clone(),
Expand Down Expand Up @@ -960,6 +968,26 @@ fn test_repo_lock_unlock_remove() {
is_locked: false,
}
);
assert_eq!(
state_before_lock
.repo_files_details
.details
.get(&1)
.unwrap()
.location
.as_ref()
.unwrap()
.content
.data
.as_ref()
.unwrap()
.bytes,
RepoFilesDetailsContentDataBytes::Decrypted(
"test".as_bytes().to_owned(),
repos::selectors::select_cipher_owned(&state_before_lock, &fixture.repo_id)
.unwrap()
)
);

fixture.lock();

Expand Down Expand Up @@ -995,6 +1023,22 @@ fn test_repo_lock_unlock_remove() {
is_locked: true,
}
);
assert!(matches!(
state_after_lock
.repo_files_details
.details
.get(&1)
.expect("a")
.location
.as_ref()
.expect("b")
.content
.data
.as_ref()
.expect("c")
.bytes,
RepoFilesDetailsContentDataBytes::Encrypted(_)
));

unlock_wait_for_details_loaded(&fixture).await;

Expand All @@ -1003,6 +1047,26 @@ fn test_repo_lock_unlock_remove() {
select_info(&state_after_unlock),
select_info(&state_before_lock)
);
assert_eq!(
state_after_unlock
.repo_files_details
.details
.get(&1)
.unwrap()
.location
.as_ref()
.unwrap()
.content
.data
.as_ref()
.unwrap()
.bytes,
RepoFilesDetailsContentDataBytes::Decrypted(
"test".as_bytes().to_owned(),
repos::selectors::select_cipher_owned(&state_after_unlock, &fixture.repo_id)
.unwrap()
)
);

fixture.remove().await;

Expand Down Expand Up @@ -1041,6 +1105,26 @@ fn test_repo_lock_unlock_remove() {
is_locked: false,
}
);
assert_eq!(
state_after_remove
.repo_files_details
.details
.get(&1)
.unwrap()
.location
.as_ref()
.unwrap()
.content
.data
.as_ref()
.unwrap()
.bytes,
RepoFilesDetailsContentDataBytes::Decrypted(
"test".as_bytes().to_owned(),
repos::selectors::select_cipher_owned(&state_after_unlock, &fixture.repo_id)
.unwrap()
)
);

fixture
.vault
Expand Down Expand Up @@ -1199,7 +1283,12 @@ fn test_eventstream() {
.content
.data
.as_ref()
.map(|x| String::from_utf8(x.bytes.clone()) == Ok("test1".to_string()))
.filter(|x| match &x.bytes {
RepoFilesDetailsContentDataBytes::Encrypted(_) => false,
RepoFilesDetailsContentDataBytes::Decrypted(bytes, _) => {
bytes.as_slice() == "test1".as_bytes()
}
})
.is_some()
})
.await;
Expand Down
32 changes: 32 additions & 0 deletions vault-core/src/repo_files_details/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ impl From<LoadFilesError> for LoadDetailsError {

#[derive(Error, Debug, Clone, PartialEq)]
pub enum LoadContentError {
#[error("{0}")]
RepoNotFound(#[from] RepoNotFoundError),
#[error("{0}")]
RepoLocked(#[from] RepoLockedError),
#[error("{0}")]
TransferError(#[from] TransferError),
#[error("file not found")]
Expand All @@ -56,6 +60,8 @@ pub enum LoadContentError {
impl UserError for LoadContentError {
fn user_error(&self) -> String {
match self {
Self::RepoNotFound(err) => err.user_error(),
Self::RepoLocked(err) => err.user_error(),
Self::TransferError(err) => err.user_error(),
Self::FileNotFound => "File not found".into(),
Self::DecryptFilenameError(err) => err.user_error(),
Expand All @@ -65,6 +71,15 @@ impl UserError for LoadContentError {
}
}

impl From<GetCipherError> for LoadContentError {
fn from(err: GetCipherError) -> Self {
match err {
GetCipherError::RepoNotFound(err) => Self::RepoNotFound(err),
GetCipherError::RepoLocked(err) => Self::RepoLocked(err),
}
}
}

#[derive(Error, Debug, Clone, PartialEq)]
pub enum SaveError {
#[error("{0}")]
Expand All @@ -73,6 +88,8 @@ pub enum SaveError {
RepoLocked(#[from] RepoLockedError),
#[error("{0}")]
DecryptFilenameError(#[from] DecryptFilenameError),
#[error("{0}")]
DecryptDataError(String),
#[error("already saving")]
AlreadySaving,
#[error("not dirty")]
Expand All @@ -97,6 +114,7 @@ impl UserError for SaveError {
Self::RepoNotFound(err) => err.user_error(),
Self::RepoLocked(err) => err.user_error(),
Self::DecryptFilenameError(err) => err.user_error(),
Self::DecryptDataError(err) => err.clone(),
Self::AlreadySaving => self.to_string(),
Self::NotDirty => self.to_string(),
Self::InvalidState => self.to_string(),
Expand Down Expand Up @@ -139,3 +157,17 @@ impl From<UploadFileReaderError> for SaveError {
}
}
}

#[derive(Error, Debug, Clone, PartialEq)]
pub enum SetContentError {
#[error("{0}")]
RepoLocked(#[from] RepoLockedError),
}

impl UserError for SetContentError {
fn user_error(&self) -> String {
match self {
Self::RepoLocked(err) => err.user_error(),
}
}
}
76 changes: 59 additions & 17 deletions vault-core/src/repo_files_details/mutations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ use crate::{
state::{RepoFile, RepoFilesUploadResult},
},
repo_files_read::errors::GetFilesReaderError,
repos, store,
repos::{self, errors::RepoLockedError},
store,
transfers::errors::TransferError,
types::{EncryptedName, EncryptedPath, RepoId},
utils::repo_encrypted_path_utils,
};

use super::{
errors::{LoadContentError, SaveError},
errors::{LoadContentError, SaveError, SetContentError},
selectors,
state::{
RepoFilesDetails, RepoFilesDetailsContent, RepoFilesDetailsContentData,
RepoFilesDetailsContentLoading, RepoFilesDetailsLocation, RepoFilesDetailsOptions,
SaveInitiator,
RepoFilesDetailsContentDataBytes, RepoFilesDetailsContentLoading, RepoFilesDetailsLocation,
RepoFilesDetailsOptions, SaveInitiator,
},
};

Expand Down Expand Up @@ -226,18 +227,21 @@ pub fn update_details(state: &mut store::State, notify: &store::Notify, details_
_ => return,
};

let (repo_status, is_locked, cipher) = match &details.location {
let (repo_status, is_locked, cipher, repo_exists) = match &details.location {
Some(loc) => {
let (repo, status) = repos::selectors::select_repo_status(state, &loc.repo_id);
let cipher = repos::selectors::select_cipher_owned(state, &loc.repo_id).ok();

(
status,
repo.map(|repo| repo.state.is_locked()).unwrap_or(false),
repo.as_ref()
.map(|repo| repo.state.is_locked())
.unwrap_or(false),
cipher,
repo.is_ok(),
)
}
None => (Status::Initial, false, None),
None => (Status::Initial, false, None, false),
};

let details = match state.repo_files_details.details.get_mut(&details_id) {
Expand All @@ -250,7 +254,7 @@ pub fn update_details(state: &mut store::State, notify: &store::Notify, details_
if let Some(location) = &mut details.location {
let decrypted_name_is_some = location.decrypted_name.is_some();

match (decrypted_name_is_some, cipher) {
match (decrypted_name_is_some, &cipher) {
(false, Some(cipher)) => {
location.decrypted_name = Some(cipher.decrypt_filename(&location.name));

Expand All @@ -265,6 +269,35 @@ pub fn update_details(state: &mut store::State, notify: &store::Notify, details_
}
}

if let Some(location) = &mut details.location {
if let Some(data) = &mut location.content.data {
match (&mut data.bytes, &cipher) {
(RepoFilesDetailsContentDataBytes::Encrypted(bytes), Some(cipher)) => {
if let Ok(decrypted) = cipher.decrypt_vec(&bytes) {
data.bytes =
RepoFilesDetailsContentDataBytes::Decrypted(decrypted, cipher.clone());

location.content.version += 1;

dirty = true;
}
}
(RepoFilesDetailsContentDataBytes::Decrypted(bytes, cipher), None)
if repo_exists =>
{
if let Ok(encrypted) = cipher.encrypt_vec(&bytes) {
data.bytes = RepoFilesDetailsContentDataBytes::Encrypted(encrypted);

location.content.version += 1;

dirty = true;
}
}
_ => {}
}
}
}

if details.repo_status != repo_status {
details.repo_status = repo_status;

Expand Down Expand Up @@ -504,27 +537,36 @@ pub fn set_content(
notify: &store::Notify,
details_id: u32,
content: Vec<u8>,
) {
) -> Result<(), SetContentError> {
let location = match selectors::select_details_location_mut(state, details_id) {
Some(location) => location,
_ => return,
_ => return Ok(()),
};

if let Some(data) = &mut location.content.data {
if data.bytes != content {
data.bytes = content;
match &mut data.bytes {
RepoFilesDetailsContentDataBytes::Encrypted(_) => {
return Err(SetContentError::RepoLocked(RepoLockedError))
}
RepoFilesDetailsContentDataBytes::Decrypted(bytes, _) => {
if bytes != &content {
*bytes = content;

location.content.version += 1;
location.content.version += 1;

notify(store::Event::RepoFilesDetailsContentData);
notify(store::Event::RepoFilesDetailsContentData);

if !location.is_dirty {
location.is_dirty = true;
if !location.is_dirty {
location.is_dirty = true;

notify(store::Event::RepoFilesDetails);
notify(store::Event::RepoFilesDetails);
}
}
}
}
}

Ok(())
}

pub fn saving(
Expand Down
10 changes: 8 additions & 2 deletions vault-core/src/repo_files_details/selectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ use super::{
errors::SaveError,
state::{
RepoFilesDetails, RepoFilesDetailsContent, RepoFilesDetailsContentData,
RepoFilesDetailsContentLoading, RepoFilesDetailsInfo, RepoFilesDetailsLocation,
RepoFilesDetailsContentDataBytes, RepoFilesDetailsContentLoading, RepoFilesDetailsInfo,
RepoFilesDetailsLocation,
},
};

Expand Down Expand Up @@ -393,7 +394,12 @@ pub fn select_content_bytes_version<'a>(
.content
.data
.as_ref()
.map(|data| data.bytes.as_ref()),
.and_then(|data| match &data.bytes {
RepoFilesDetailsContentDataBytes::Encrypted(_) => None,
RepoFilesDetailsContentDataBytes::Decrypted(bytes, _) => {
Some(bytes.as_ref())
}
}),
location.content.version,
)
})
Expand Down
Loading

0 comments on commit c50e767

Please sign in to comment.