Skip to content

Commit

Permalink
refactor!: Add more control over used keys
Browse files Browse the repository at this point in the history
  • Loading branch information
aawsome committed Dec 10, 2024
1 parent 6a50c12 commit 20c7054
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 79 deletions.
15 changes: 13 additions & 2 deletions crates/core/src/backend/decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ pub trait DecryptReadBackend: ReadBackend + Clone + 'static {
///
/// * If the file could not be read.
fn get_file<F: RepoFile>(&self, id: &Id) -> RusticResult<F> {
let data = self.read_encrypted_full(F::TYPE, id)?;
let data = if F::ENCRYPTED {

Check warning on line 137 in crates/core/src/backend/decrypt.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/decrypt.rs#L137

Added line #L137 was not covered by tests
self.read_encrypted_full(F::TYPE, id)?
} else {
self.read_full(F::TYPE, id)?
};
let deserialized = serde_json::from_slice(&data).map_err(|err| {
RusticError::with_source(
ErrorKind::Internal,
Expand Down Expand Up @@ -262,7 +266,14 @@ pub trait DecryptWriteBackend: WriteBackend + Clone + 'static {
.ask_report()
})?;

self.hash_write_full(F::TYPE, &data)
if F::ENCRYPTED {

Check warning on line 269 in crates/core/src/backend/decrypt.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/decrypt.rs#L269

Added line #L269 was not covered by tests
self.hash_write_full(F::TYPE, &data)
} else {
let id = hash(&data);

Check warning on line 272 in crates/core/src/backend/decrypt.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/decrypt.rs#L272

Added line #L272 was not covered by tests

self.write_bytes(F::TYPE, &id, false, data.into())?;
Ok(id)

Check warning on line 275 in crates/core/src/backend/decrypt.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/backend/decrypt.rs#L274-L275

Added lines #L274 - L275 were not covered by tests
}
}

/// Saves the given file uncompressed.
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ pub(crate) fn check_repository<P: ProgressBars, S: Open>(
trees: Vec<TreeId>,
) -> RusticResult<()> {
let be = repo.dbe();
let cache = repo.cache();
let cache = &repo.open_status().cache;

Check warning on line 241 in crates/core/src/commands/check.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/commands/check.rs#L241

Added line #L241 was not covered by tests
let hot_be = &repo.be_hot;
let raw_be = repo.dbe();
let pb = &repo.pb;
Expand Down
12 changes: 6 additions & 6 deletions crates/core/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
crypto::aespoly1305::Key,
error::RusticResult,
id::Id,
repofile::{configfile::RepositoryId, ConfigFile},
repofile::{configfile::RepositoryId, ConfigFile, KeyId},
repository::Repository,
};

Expand Down Expand Up @@ -42,7 +42,7 @@ pub(crate) fn init<P, S>(
pass: &str,
key_opts: &KeyOptions,
config_opts: &ConfigOptions,
) -> RusticResult<(Key, ConfigFile)> {
) -> RusticResult<(Key, KeyId, ConfigFile)> {
// Create config first to allow catching errors from here without writing anything
let repo_id = RepositoryId::from(Id::random());
let chunker_poly = random_poly()?;
Expand All @@ -54,10 +54,10 @@ pub(crate) fn init<P, S>(
}
config_opts.apply(&mut config)?;

let key = init_with_config(repo, pass, key_opts, &config)?;
let (key, key_id) = init_with_config(repo, pass, key_opts, &config)?;
info!("repository {} successfully created.", repo_id);

Ok((key, config))
Ok((key, key_id, config))
}

/// Initialize a new repository with a given config.
Expand All @@ -82,11 +82,11 @@ pub(crate) fn init_with_config<P, S>(
pass: &str,
key_opts: &KeyOptions,
config: &ConfigFile,
) -> RusticResult<Key> {
) -> RusticResult<(Key, KeyId)> {
repo.be.create()?;
let (key, id) = init_key(repo, key_opts, pass)?;
info!("key {id} successfully added.");
save_config(repo, config.clone(), key)?;

Ok(key)
Ok((key, id))
}
4 changes: 3 additions & 1 deletion crates/core/src/repofile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ pub(crate) mod keyfile;
pub(crate) mod packfile;
pub(crate) mod snapshotfile;

/// Marker trait for repository files which are stored as encrypted JSON
/// Marker trait for repository files which are stored as JSON
pub trait RepoFile: Serialize + DeserializeOwned + Sized + Send + Sync + 'static {
/// The [`FileType`] associated with the repository file
const TYPE: FileType;
/// Indicate whether the files are stored encrypted
const ENCRYPTED: bool = true;
/// The Id type associated with the repository file
type Id: From<Id> + Send;
}
Expand Down
33 changes: 20 additions & 13 deletions crates/core/src/repofile/keyfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
crypto::{aespoly1305::Key, CryptoKey},
error::{ErrorKind, RusticError, RusticResult},
impl_repoid,
repofile::RepoFile,
};

/// [`KeyFileErrorKind`] describes the errors that can be returned for `KeyFile`s
Expand Down Expand Up @@ -45,34 +46,40 @@ impl_repoid!(KeyId, FileType::Key);
#[derive(Serialize, Deserialize, Debug)]
pub struct KeyFile {
/// Hostname where the key was created
hostname: Option<String>,
pub hostname: Option<String>,

/// User which created the key
username: Option<String>,
pub username: Option<String>,

/// Creation time of the key
created: Option<DateTime<Local>>,
pub created: Option<DateTime<Local>>,

/// The used key derivation function (currently only `scrypt`)
kdf: String,
pub kdf: String,

/// Parameter N for `scrypt`
#[serde(rename = "N")]
n: u32,
pub n: u32,

/// Parameter r for `scrypt`
r: u32,
pub r: u32,

/// Parameter p for `scrypt`
p: u32,
pub p: u32,

/// The key data encrypted by `scrypt`
#[serde_as(as = "Base64")]
data: Vec<u8>,
pub data: Vec<u8>,

/// The salt used with `scrypt`
#[serde_as(as = "Base64")]
salt: Vec<u8>,
pub salt: Vec<u8>,
}

impl RepoFile for KeyFile {
const TYPE: FileType = FileType::Key;
const ENCRYPTED: bool = false;
type Id = KeyId;
}

impl KeyFile {
Expand Down Expand Up @@ -386,15 +393,15 @@ pub(crate) fn find_key_in_backend<B: ReadBackend>(
be: &B,
passwd: &impl AsRef<[u8]>,
hint: Option<&KeyId>,
) -> RusticResult<Key> {
) -> RusticResult<(Key, KeyId)> {
if let Some(id) = hint {
key_from_backend(be, id, passwd)
Ok((key_from_backend(be, id, passwd)?, *id))

Check warning on line 398 in crates/core/src/repofile/keyfile.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repofile/keyfile.rs#L398

Added line #L398 was not covered by tests
} else {
for id in be.list(FileType::Key)? {
match key_from_backend(be, &id.into(), passwd) {
Ok(key) => return Ok(key),
Ok(key) => return Ok((key, KeyId(id))),
Err(err) if err.is_code("C001") => continue,
err => return err,
Err(err) => return Err(err),
}
}

Expand Down
109 changes: 53 additions & 56 deletions crates/core/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
local_destination::LocalDestination,
node::Node,
warm_up::WarmUpAccessBackend,
FileType, ReadBackend, WriteBackend,
FileType, FindInBackend, ReadBackend, WriteBackend,
},
blob::{
tree::{FindMatches, FindNode, NodeStreamer, TreeId, TreeStreamerOptions as LsOptions},
Expand Down Expand Up @@ -520,13 +520,13 @@ impl<P, S> Repository<P, S> {
}
}

let key = find_key_in_backend(&self.be, &password, None)?;
let (key, key_id) = find_key_in_backend(&self.be, &password, None)?;

info!("repository {}: password is correct.", self.name);

let dbe = DecryptBackend::new(self.be.clone(), key);
let config: ConfigFile = dbe.get_file(&config_id)?;
self.open_raw(key, config)
self.open_raw(key, key_id, config)
}

/// Initialize a new repository with given options using the password defined in `RepositoryOptions`
Expand Down Expand Up @@ -604,9 +604,9 @@ impl<P, S> Repository<P, S> {
.attach_context("name", self.name));
}

let (key, config) = commands::init::init(&self, pass, key_opts, config_opts)?;
let (key, key_id, config) = commands::init::init(&self, pass, key_opts, config_opts)?;

self.open_raw(key, config)
self.open_raw(key, key_id, config)
}

/// Initialize a new repository with given password and a ready [`ConfigFile`].
Expand All @@ -632,9 +632,9 @@ impl<P, S> Repository<P, S> {
key_opts: &KeyOptions,
config: ConfigFile,
) -> RusticResult<Repository<P, OpenStatus>> {
let key = commands::init::init_with_config(&self, password, key_opts, &config)?;
let (key, key_id) = commands::init::init_with_config(&self, password, key_opts, &config)?;

Check warning on line 635 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L635

Added line #L635 was not covered by tests
info!("repository {} successfully created.", config.id);
self.open_raw(key, config)
self.open_raw(key, key_id, config)

Check warning on line 637 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L637

Added line #L637 was not covered by tests
}

/// Open the repository with given [`Key`] and [`ConfigFile`].
Expand All @@ -652,7 +652,12 @@ impl<P, S> Repository<P, S> {
///
/// * If the config file has `is_hot` set to `true` but the repository is not hot
/// * If the config file has `is_hot` set to `false` but the repository is hot
fn open_raw(mut self, key: Key, config: ConfigFile) -> RusticResult<Repository<P, OpenStatus>> {
fn open_raw(
mut self,
key: Key,
key_id: KeyId,
config: ConfigFile,
) -> RusticResult<Repository<P, OpenStatus>> {
match (config.is_hot == Some(true), self.be_hot.is_some()) {
(true, false) => return Err(
RusticError::new(
Expand Down Expand Up @@ -684,7 +689,12 @@ impl<P, S> Repository<P, S> {
dbe.set_zstd(config.zstd()?);
dbe.set_extra_verify(config.extra_verify());

let open = OpenStatus { cache, dbe, config };
let open = OpenStatus {
cache,
dbe,
config,
key_id,
};

Ok(Repository {
name: self.name,
Expand Down Expand Up @@ -755,58 +765,32 @@ impl<P: ProgressBars, S> Repository<P, S> {

/// A repository which is open, i.e. the password has been checked and the decryption key is available.
pub trait Open {
/// Get the cache
fn cache(&self) -> Option<&Cache>;

/// Get the [`DecryptBackend`]
fn dbe(&self) -> &DecryptBackend<Key>;

/// Get the [`ConfigFile`]
fn config(&self) -> &ConfigFile;
/// Get the open status
fn open_status(&self) -> &OpenStatus;
}

impl<P, S: Open> Open for Repository<P, S> {
/// Get the cache
fn cache(&self) -> Option<&Cache> {
self.status.cache()
}

/// Get the [`DecryptBackend`]
fn dbe(&self) -> &DecryptBackend<Key> {
self.status.dbe()
}

/// Get the [`ConfigFile`]
fn config(&self) -> &ConfigFile {
self.status.config()
fn open_status(&self) -> &OpenStatus {

Check warning on line 773 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L773

Added line #L773 was not covered by tests
self.status.open_status()
}
}

/// Open Status: This repository is open, i.e. the password has been checked and the decryption key is available.
#[derive(Debug)]
pub struct OpenStatus {
/// The cache
cache: Option<Cache>,
pub(crate) cache: Option<Cache>,
/// The [`DecryptBackend`]
dbe: DecryptBackend<Key>,
/// The [`ConfigFile`]
config: ConfigFile,
/// The [`KeyId`] of the used key
key_id: KeyId,
}

impl Open for OpenStatus {
/// Get the cache
fn cache(&self) -> Option<&Cache> {
self.cache.as_ref()
}

/// Get the [`DecryptBackend`]
fn dbe(&self) -> &DecryptBackend<Key> {
&self.dbe
}

/// Get the [`ConfigFile`]
fn config(&self) -> &ConfigFile {
&self.config
fn open_status(&self) -> &OpenStatus {
self
}
}

Expand Down Expand Up @@ -863,12 +847,33 @@ impl<P, S: Open> Repository<P, S> {

/// Get the repository configuration
pub fn config(&self) -> &ConfigFile {
self.status.config()
&self.open_status().config

Check warning on line 850 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L850

Added line #L850 was not covered by tests
}

// TODO: add documentation!
pub(crate) fn dbe(&self) -> &DecryptBackend<Key> {
self.status.dbe()
&self.open_status().dbe
}

/// Get the [`KeyId`] of the key used to open the repository
pub fn key_id(&self) -> &KeyId {

Check warning on line 859 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L859

Added line #L859 was not covered by tests
&self.open_status().key_id
}

/// Delete the key with id starting with the given string from the repository.
///
/// # Errors
///
/// * If the key could not be removed.
pub fn delete_key(&self, id: &str) -> RusticResult<()> {
let id = self.dbe().find_id(FileType::Key, id)?;
if self.key_id() == &KeyId::from(id) {
return Err(RusticError::new(
ErrorKind::Repository,
"Cannot remove the currently used key",

Check warning on line 873 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L872-L873

Added lines #L872 - L873 were not covered by tests
));
}
self.dbe().remove(FileType::Key, &id, false)

Check warning on line 876 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L876

Added line #L876 was not covered by tests
}
}

Expand Down Expand Up @@ -1557,16 +1562,8 @@ impl<P, S: IndexedFull> IndexedFull for Repository<P, S> {
}

impl<T, S: Open> Open for IndexedStatus<T, S> {
fn cache(&self) -> Option<&Cache> {
self.open.cache()
}

fn dbe(&self) -> &DecryptBackend<Key> {
self.open.dbe()
}

fn config(&self) -> &ConfigFile {
self.open.config()
fn open_status(&self) -> &OpenStatus {

Check warning on line 1565 in crates/core/src/repository.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository.rs#L1565

Added line #L1565 was not covered by tests
self.open.open_status()
}
}

Expand Down
4 changes: 4 additions & 0 deletions crates/core/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
//! The fixtures are defined as functions with the `#[fixture]` attribute.
//! The tests that use the fixtures are defined as functions with the `#[rstest]` attribute.
//! The fixtures are passed as arguments to the test functions.
mod integration {
use super::*;
mod key;
}

use anyhow::Result;
use bytes::Bytes;
Expand Down
Loading

0 comments on commit 20c7054

Please sign in to comment.