Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/crypto/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ pub(crate) fn iv_len() -> usize {
DecAlg::iv_size()
}

pub(crate) fn key_len() -> usize {
DecAlg::key_size()
}

pub(crate) fn generate_private_key() -> Result<Zeroizing<Vec<u8>>, super::Error> {
let mut key = vec![0u8; EncAlg::key_size()];
getrandom::fill(&mut key)?;
Expand Down
4 changes: 4 additions & 0 deletions client/src/crypto/openssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub(crate) fn iv_len() -> usize {
cipher.iv_len().unwrap()
}

pub(crate) fn key_len() -> usize {
Cipher::from_nid(ENC_ALG).unwrap().key_len()
}

pub(crate) fn generate_private_key() -> Result<Zeroizing<Vec<u8>>, super::Error> {
let cipher = Cipher::from_nid(ENC_ALG).unwrap();
let mut buf = Zeroizing::new(vec![0; cipher.key_len()]);
Expand Down
26 changes: 26 additions & 0 deletions client/src/file/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,32 @@ impl Keyring {
Ok(self.items.iter().any(|item| item.is_valid(Some(&key))))
}

pub(super) fn validate_key(&self, key: &Key) -> bool {
self.items.is_empty() || self.items.iter().any(|item| item.is_valid(Some(key)))
}

pub(super) fn validate_items(&self, key: &Key) -> Result<(), Error> {
let (valid_items, broken_items) =
self.items.iter().fold((0, 0), |(valid, broken), item| {
if item.is_valid(Some(key)) {
(valid + 1, broken)
} else {
(valid, broken + 1)
}
});

if valid_items == 0 && broken_items != 0 {
Err(Error::IncorrectSecret)
} else if broken_items > valid_items {
Err(Error::PartiallyCorruptedKeyring {
valid_items,
broken_items,
})
} else {
Ok(())
}
}

pub fn validate_unencrypted(&self) -> bool {
self.items.iter().all(|item| item.is_valid(None))
}
Expand Down
6 changes: 6 additions & 0 deletions client/src/file/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum Error {
SaltSizeMismatch(usize, u32),
/// Key for some reason too weak to trust it for writing
WeakKey(WeakKeyError),
/// A file encryption key has an unexpected length.
InvalidKeyLength { expected: usize, actual: usize },
/// Input/Output.
Io(std::io::Error),
/// Unexpected MAC digest value.
Expand Down Expand Up @@ -114,6 +116,10 @@ impl std::fmt::Display for Error {
"Salt size is not as expected. Array: {arr}, Explicit: {explicit}"
),
Self::WeakKey(err) => write!(f, "{err}"),
Self::InvalidKeyLength { expected, actual } => write!(
f,
"Invalid file key length: expected {expected} bytes, got {actual}",
),
Self::Io(e) => write!(f, "IO error {e}"),
Self::MacError => write!(f, "Mac digest is not equal to the expected value"),
Self::ChecksumMismatch => write!(f, "Incorrect secret or corrupted keyring data"),
Expand Down
114 changes: 75 additions & 39 deletions client/src/file/locked_keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use tokio::{
};

use super::{Error, LockedItem, UnlockedKeyring, api};
use crate::Secret;
use crate::{Key, Secret};

/// A locked keyring that requires a secret to unlock.
#[derive(Debug)]
Expand All @@ -44,6 +44,21 @@ impl LockedKeyring {
Ok(keyring.validate_secret(secret)?)
}

/// Validate that an already-derived key can decrypt at least one item in
/// this keyring.
///
/// Empty keyrings return `true` because they contain no item with which to
/// authenticate the key. Callers that persist keys for empty keyrings must
/// bind them to the exact keyring file separately.
///
/// A partially corrupted keyring may return `true` here but still fail
/// [`Self::unlock_with_key`] when broken items outnumber valid items.
pub async fn validate_key(&self, key: &Key) -> Result<bool, Error> {
key.validate_file_key()?;
let keyring = self.keyring.read().await;
Ok(keyring.validate_key(key))
}

pub async fn validate_unencrypted(&self) -> Result<bool, Error> {
let keyring = self.keyring.read().await;
Ok(keyring.validate_unencrypted())
Expand Down Expand Up @@ -78,6 +93,27 @@ impl LockedKeyring {
self.unlock_inner(secret, true).await
}

/// Unlocks a keyring with an already-derived key and validates it.
///
/// An exact-length [`Key::new`] value is treated as direct key material and
/// may be used for subsequent writes. The caller is responsible for
/// supplying a key with sufficient entropy.
///
/// Empty keyrings cannot authenticate the key and therefore accept any key
/// of the required length, matching [`Self::validate_key`].
pub async fn unlock_with_key(self, key: Key) -> Result<UnlockedKeyring, Error> {
let key = key.into_file_key()?;
let validation = {
let inner_keyring = self.keyring.read().await;
inner_keyring.validate_items(&key)
};
#[cfg(feature = "tracing")]
Self::log_validation_error(&validation, false);
validation?;

Ok(self.into_unlocked(Some(Arc::new(key)), None))
}

/// Unlocks a keyring without validating it
///
/// # Safety
Expand All @@ -100,52 +136,58 @@ impl LockedKeyring {
let inner_keyring = self.keyring.read().await;

let key = inner_keyring.derive_key(&secret)?;
let validation = inner_keyring.validate_items(&key);
#[cfg(feature = "tracing")]
Self::log_validation_error(&validation, true);
validation?;

let mut n_broken_items = 0;
let mut n_valid_items = 0;
for encrypted_item in &inner_keyring.items {
if encrypted_item.is_valid(Some(&key)) {
n_valid_items += 1;
} else {
n_broken_items += 1;
}
}
Some(Arc::new(key))
} else {
None
};

drop(inner_keyring);
Ok(self.into_unlocked(key, Some(Arc::new(secret))))
}

if n_valid_items == 0 && n_broken_items != 0 {
#[cfg(feature = "tracing")]
#[cfg(feature = "tracing")]
fn log_validation_error(validation: &Result<(), Error>, source_secret: bool) {
match validation {
Err(Error::IncorrectSecret) if source_secret => {
tracing::error!("Keyring cannot be decrypted. Invalid secret.");
return Err(Error::IncorrectSecret);
} else if n_broken_items > n_valid_items {
#[cfg(feature = "tracing")]
{
tracing::warn!(
"The file contains {n_broken_items} broken items and {n_valid_items} valid ones."
);
}
Err(Error::IncorrectSecret) => {
tracing::error!("Keyring cannot be decrypted. Invalid key material.");
}
Err(Error::PartiallyCorruptedKeyring {
valid_items,
broken_items,
}) => {
tracing::warn!(
"The file contains {broken_items} broken items and {valid_items} valid ones."
);
if source_secret {
tracing::info!(
"Please switch to `UnlockedKeyring::load_unchecked` to load the keyring without the secret validation.
`Keyring::delete_broken_items` can be used to remove them or alternatively with `oo7-cli --repair`."
);
} else {
tracing::info!(
"Recover the keyring with its source secret; key-based unlock does not bypass validation."
);
}
return Err(Error::PartiallyCorruptedKeyring {
valid_items: n_valid_items,
broken_items: n_broken_items,
});
}
_ => {}
}
}

Some(Arc::new(key))
} else {
None
};

Ok(UnlockedKeyring {
fn into_unlocked(self, key: Option<Arc<Key>>, secret: Option<Arc<Secret>>) -> UnlockedKeyring {
UnlockedKeyring {
keyring: self.keyring,
path: self.path,
mtime: self.mtime,
key: Mutex::new(key),
secret: Mutex::new(Some(Arc::new(secret))),
})
secret: Mutex::new(secret),
}
}

/// Unlocks a keyring without a secret, for unencrypted keyrings.
Expand All @@ -162,13 +204,7 @@ impl LockedKeyring {
}
drop(inner_keyring);

Ok(UnlockedKeyring {
keyring: self.keyring,
path: self.path,
mtime: self.mtime,
key: Mutex::new(None),
secret: Mutex::new(None),
})
Ok(self.into_unlocked(None, None))
}

/// Load a keyring from a file path.
Expand Down
32 changes: 32 additions & 0 deletions client/src/file/unlocked_keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ impl UnlockedKeyring {
Self::load_inner(path, secret, true).await
}

/// Load and unlock a keyring with an already-derived key.
#[cfg_attr(feature = "tracing", tracing::instrument(skip(key), fields(path = ?path.as_ref())))]
pub async fn load_with_key(path: impl AsRef<Path>, key: Key) -> Result<Self, Error> {
LockedKeyring::load(path).await?.unlock_with_key(key).await
}

/// Load from a keyring file without validating the secret.
///
/// # Arguments
Expand Down Expand Up @@ -243,6 +249,13 @@ impl UnlockedKeyring {
Self::open_with_paths(v1_path, v0_path, secret).await
}

/// Open a named current-format keyring with an already-derived key.
#[cfg_attr(feature = "tracing", tracing::instrument(skip(key)))]
pub async fn open_with_key(name: &str, key: Key) -> Result<Self, Error> {
let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?;
Self::load_with_key(v1_path, key).await
}

/// Open or create a keyring at a specific data directory.
///
/// This is useful for tests and cases where you want explicit control over
Expand Down Expand Up @@ -289,6 +302,18 @@ impl UnlockedKeyring {
Self::open_with_paths(v1_path, v0_path, secret).await
}

/// Open a named current-format keyring at a specific data directory with
/// an already-derived key.
#[cfg_attr(feature = "tracing", tracing::instrument(skip(key), fields(data_dir = ?data_dir.as_ref())))]
pub async fn open_at_with_key(
Comment thread
caniko marked this conversation as resolved.
data_dir: impl AsRef<Path>,
name: &str,
key: Key,
) -> Result<Self, Error> {
let v1_path = api::Keyring::path_at(&data_dir, name, api::MAJOR_VERSION);
Self::load_with_key(v1_path, key).await
}

/// Lock the keyring.
pub fn lock(self) -> LockedKeyring {
LockedKeyring {
Expand Down Expand Up @@ -586,6 +611,13 @@ impl UnlockedKeyring {
/// Returns `None` when no secret is set (unencrypted keyring).
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
async fn derive_key(&self) -> Result<Option<Arc<Key>>, crate::crypto::Error> {
{
let key_lock = self.key.lock().await;
if key_lock.is_some() {
return Ok(key_lock.clone());
}
}

let keyring = Arc::clone(&self.keyring);
let secret_lock = self.secret.lock().await;
let secret = match secret_lock.as_ref() {
Expand Down
31 changes: 30 additions & 1 deletion client/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::{crypto, file};

/// A key.
/// Cryptographic key material.
///
/// File-keyring APIs accept already-derived values constructed with
/// [`Self::new`]. Key bytes are redacted from [`Debug`](std::fmt::Debug)
/// output.
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Key {
key: Vec<u8>,
Expand Down Expand Up @@ -34,6 +38,11 @@ impl AsMut<[u8]> for Key {
}

impl Key {
/// Construct a key from bytes.
///
/// The key's source strength is unknown. File-keyring APIs accept an
/// exact-length value as direct key material, so callers are responsible
/// for supplying sufficient entropy.
pub const fn new(key: Vec<u8>) -> Self {
Self::new_with_strength(key, Err(file::WeakKeyError::StrengthUnknown))
}
Expand All @@ -49,6 +58,26 @@ impl Key {
Self { key, strength }
}

pub(crate) fn validate_file_key(&self) -> Result<(), file::Error> {
let expected = crypto::key_len();
if self.key.len() == expected {
Ok(())
} else {
Err(file::Error::InvalidKeyLength {
expected,
actual: self.key.len(),
})
}
}

pub(crate) fn into_file_key(mut self) -> Result<Self, file::Error> {
self.validate_file_key()?;
if matches!(self.strength, Err(file::WeakKeyError::StrengthUnknown)) {
self.strength = Ok(());
}
Ok(self)
}

pub fn generate_private_key() -> Result<Self, crypto::Error> {
Ok(Self::new(crypto::generate_private_key()?.to_vec()))
}
Expand Down
4 changes: 0 additions & 4 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ mod key;
mod mac;
mod migration;

#[cfg(feature = "unstable")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
pub use key::Key;
#[cfg(not(feature = "unstable"))]
pub(crate) use key::Key;
pub use mac::Mac;

#[cfg(not(feature = "unstable"))]
Expand Down
Loading
Loading