use std::path::{Path, PathBuf};
use crate::passphrase::Passphrase;
use crate::archive::{self, ArchiveLimits, IncompleteOutputPolicy};
use crate::container::HeaderReadLimits;
use crate::crypto::kdf::{KdfLimit, KdfParams};
use crate::error::sanitize_path_for_display;
use crate::format;
use crate::fs::paths;
use crate::key::limits::KeyReadLimits;
use crate::key::private::PrivateKey;
use crate::key::public::PublicKey;
use crate::protocol;
use crate::recipient;
use crate::recipient::policy::NativeRecipientType;
use crate::{
AuthenticatedRecipientMode, CryptoError, DecryptOutcome, ENCRYPTED_EXTENSION, EncryptOutcome,
KeyGenOutcome, ProgressEvent, UnauthenticatedRecipientMode,
};
#[derive(Debug)]
#[non_exhaustive]
pub struct Encryptor {
state: EncryptorState,
save_as: Option<PathBuf>,
archive_limits: Option<ArchiveLimits>,
kdf_params: Option<KdfParams>,
header_read_limits: Option<HeaderReadLimits>,
kdf_limit: Option<KdfLimit>,
}
#[derive(Debug)]
enum EncryptorState {
Passphrase(Passphrase),
Recipients(Vec<PublicKey>),
}
fn collect_recipients_within_structural_max(
public_keys: impl IntoIterator<Item = PublicKey>,
) -> Result<Vec<PublicKey>, CryptoError> {
let ceiling = HeaderReadLimits::RECIPIENT_COUNT_STRUCTURAL_MAX;
let iter = public_keys.into_iter();
let mut collected = Vec::with_capacity(iter.size_hint().0.min(usize::from(ceiling)));
for public_key in iter {
if collected.len() == usize::from(ceiling) {
return Err(CryptoError::RecipientCountCapExceeded {
count: ceiling.saturating_add(1),
local_cap: ceiling,
});
}
collected.push(public_key);
}
Ok(collected)
}
impl Encryptor {
pub fn with_passphrase(passphrase: Passphrase) -> Self {
Self {
state: EncryptorState::Passphrase(passphrase),
save_as: None,
archive_limits: None,
kdf_params: None,
header_read_limits: None,
kdf_limit: None,
}
}
pub fn with_public_key(public_key: PublicKey) -> Self {
Self {
state: EncryptorState::Recipients(vec![public_key]),
save_as: None,
archive_limits: None,
kdf_params: None,
header_read_limits: None,
kdf_limit: None,
}
}
pub fn with_public_keys(
public_keys: impl IntoIterator<Item = PublicKey>,
) -> Result<Self, CryptoError> {
let public_keys = collect_recipients_within_structural_max(public_keys)?;
if public_keys.is_empty() {
return Err(CryptoError::EmptyRecipientList);
}
Ok(Self {
state: EncryptorState::Recipients(public_keys),
save_as: None,
archive_limits: None,
kdf_params: None,
header_read_limits: None,
kdf_limit: None,
})
}
pub fn save_as(mut self, path: impl AsRef<Path>) -> Self {
self.save_as = Some(path.as_ref().to_path_buf());
self
}
pub fn archive_limits(mut self, limits: ArchiveLimits) -> Self {
self.archive_limits = Some(limits);
self
}
pub fn kdf_params(mut self, params: KdfParams) -> Self {
self.kdf_params = Some(params);
self
}
pub fn header_read_limits(mut self, limits: HeaderReadLimits) -> Self {
self.header_read_limits = Some(limits);
self
}
pub fn kdf_limit(mut self, limit: KdfLimit) -> Self {
self.kdf_limit = Some(limit);
self
}
pub fn write(
self,
input: impl AsRef<Path>,
output_dir: impl AsRef<Path>,
on_event: impl Fn(&ProgressEvent),
) -> Result<EncryptOutcome, CryptoError> {
let input = input.as_ref();
let output_dir = output_dir.as_ref();
let archive_limits = self.archive_limits.unwrap_or_default();
let header_read_limits = self.header_read_limits.unwrap_or_default();
let kdf_limit = self.kdf_limit.unwrap_or_default();
let kdf_params = self.kdf_params.unwrap_or_default();
let save_as = self.save_as.as_deref();
if let EncryptorState::Passphrase(p) = &self.state {
validate_passphrase(p)?;
}
match &self.state {
EncryptorState::Recipients(rs) => {
protocol::preflight_header_write_limits(
header_read_limits,
rs.len(),
NativeRecipientType::X25519,
protocol::WRITE_EXT_BYTES,
)?;
}
EncryptorState::Passphrase(_) => {
protocol::preflight_header_write_limits(
header_read_limits,
1,
NativeRecipientType::Argon2id,
protocol::WRITE_EXT_BYTES,
)?;
kdf_params.validate_for_write(Some(&kdf_limit))?;
}
}
archive::validate_encrypt_input(input)?;
let output_path = match self.state {
EncryptorState::Passphrase(passphrase) => {
let recipient = recipient::argon2id::PassphraseRecipient {
passphrase,
kdf_params,
kdf_limit,
};
protocol::encrypt(
vec![recipient],
archive_limits,
header_read_limits,
input,
output_dir,
save_as,
&on_event,
)?
}
EncryptorState::Recipients(public_keys) => {
let public_key_bytes_vec: Vec<[u8; 32]> = public_keys
.iter()
.map(|pk| pk.to_x25519_bytes())
.collect::<Result<_, _>>()?;
let recipients: Vec<recipient::x25519::X25519Recipient> = public_key_bytes_vec
.iter()
.map(|bytes| recipient::x25519::X25519Recipient {
recipient_public_key_bytes: bytes,
})
.collect();
protocol::encrypt(
recipients,
archive_limits,
header_read_limits,
input,
output_dir,
save_as,
&on_event,
)?
}
};
Ok(EncryptOutcome { output_path })
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Decryptor {
Passphrase(PassphraseDecryptor),
PrivateKey(PrivateKeyDecryptor),
}
impl Decryptor {
pub fn open(input: impl AsRef<Path>) -> Result<Self, CryptoError> {
Self::open_inner(input.as_ref(), None)
}
pub fn open_with_limits(
input: impl AsRef<Path>,
header_read_limits: HeaderReadLimits,
) -> Result<Self, CryptoError> {
Self::open_inner(input.as_ref(), Some(header_read_limits))
}
fn open_inner(
input: &Path,
header_read_limits: Option<HeaderReadLimits>,
) -> Result<Self, CryptoError> {
let Some(file) = open_header_input(input)? else {
return Err(CryptoError::InvalidInput(format!(
"Cannot decrypt a directory: {}",
sanitize_path_for_display(input)
)));
};
let mode =
protocol::DecryptSession::from_file(file, header_read_limits.unwrap_or_default())?
.mode();
let input = input.to_path_buf();
match mode {
UnauthenticatedRecipientMode::Passphrase => Ok(Self::Passphrase(PassphraseDecryptor {
input,
kdf_limit: None,
archive_limits: None,
header_read_limits,
incomplete_output_policy: None,
})),
UnauthenticatedRecipientMode::PublicKey => Ok(Self::PrivateKey(PrivateKeyDecryptor {
input,
kdf_limit: None,
key_read_limits: None,
archive_limits: None,
header_read_limits,
incomplete_output_policy: None,
})),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct PassphraseDecryptor {
input: PathBuf,
kdf_limit: Option<KdfLimit>,
archive_limits: Option<ArchiveLimits>,
header_read_limits: Option<HeaderReadLimits>,
incomplete_output_policy: Option<IncompleteOutputPolicy>,
}
impl PassphraseDecryptor {
pub fn kdf_limit(mut self, limit: KdfLimit) -> Self {
self.kdf_limit = Some(limit);
self
}
pub fn archive_limits(mut self, limits: ArchiveLimits) -> Self {
self.archive_limits = Some(limits);
self
}
pub fn header_read_limits(mut self, limits: HeaderReadLimits) -> Self {
self.header_read_limits = Some(limits);
self
}
pub fn incomplete_output_policy(mut self, policy: IncompleteOutputPolicy) -> Self {
self.incomplete_output_policy = Some(policy);
self
}
pub fn decrypt(
self,
passphrase: Passphrase,
output_dir: impl AsRef<Path>,
on_event: impl Fn(&ProgressEvent),
) -> Result<DecryptOutcome, CryptoError> {
validate_passphrase(&passphrase)?;
let credential = recipient::argon2id::PassphraseCredential {
passphrase,
kdf_limit: self.kdf_limit.as_ref(),
};
let archive_limits = self.archive_limits.unwrap_or_default();
let header_read_limits = self.header_read_limits.unwrap_or_default();
let incomplete_output_policy = self.incomplete_output_policy.unwrap_or_default();
let output_path = protocol::decrypt(
credential,
&self.input,
output_dir.as_ref(),
archive_limits,
header_read_limits,
incomplete_output_policy,
&on_event,
)?;
Ok(DecryptOutcome {
output_path,
recipient_mode: AuthenticatedRecipientMode::passphrase(),
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct PrivateKeyDecryptor {
input: PathBuf,
kdf_limit: Option<KdfLimit>,
key_read_limits: Option<KeyReadLimits>,
archive_limits: Option<ArchiveLimits>,
header_read_limits: Option<HeaderReadLimits>,
incomplete_output_policy: Option<IncompleteOutputPolicy>,
}
impl PrivateKeyDecryptor {
pub fn kdf_limit(mut self, limit: KdfLimit) -> Self {
self.kdf_limit = Some(limit);
self
}
pub fn key_read_limits(mut self, limits: KeyReadLimits) -> Self {
self.key_read_limits = Some(limits);
self
}
pub fn archive_limits(mut self, limits: ArchiveLimits) -> Self {
self.archive_limits = Some(limits);
self
}
pub fn header_read_limits(mut self, limits: HeaderReadLimits) -> Self {
self.header_read_limits = Some(limits);
self
}
pub fn incomplete_output_policy(mut self, policy: IncompleteOutputPolicy) -> Self {
self.incomplete_output_policy = Some(policy);
self
}
pub fn decrypt(
self,
private_key: PrivateKey,
output_dir: impl AsRef<Path>,
on_event: impl Fn(&ProgressEvent),
) -> Result<DecryptOutcome, CryptoError> {
let (key_file_path, private_key_passphrase) = private_key.into_key_file_parts();
validate_passphrase(&private_key_passphrase)?;
let archive_limits = self.archive_limits.unwrap_or_default();
let header_read_limits = self.header_read_limits.unwrap_or_default();
let incomplete_output_policy = self.incomplete_output_policy.unwrap_or_default();
let session = protocol::DecryptSession::open(&self.input, header_read_limits)?;
if session.mode() != UnauthenticatedRecipientMode::PublicKey {
return Err(CryptoError::DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode::PublicKey,
found: session.mode(),
});
}
let private_key_bytes = recipient::native::x25519::open_x25519_private_key(
&key_file_path,
&private_key_passphrase,
self.kdf_limit.as_ref(),
self.key_read_limits.unwrap_or_default(),
&on_event,
)?;
drop(private_key_passphrase);
let decryption_credential = recipient::x25519::X25519Credential { private_key_bytes };
let output_path = protocol::decrypt_session(
decryption_credential,
session,
output_dir.as_ref(),
archive_limits,
incomplete_output_policy,
&on_event,
)?;
Ok(DecryptOutcome {
output_path,
recipient_mode: AuthenticatedRecipientMode::public_key(),
})
}
}
impl PrivateKey {
pub fn into_public_key(
self,
on_event: impl Fn(&ProgressEvent),
) -> Result<PublicKey, CryptoError> {
self.into_public_key_with_limits(KdfLimit::default(), KeyReadLimits::default(), on_event)
}
pub fn into_public_key_with_limits(
self,
kdf_limit: KdfLimit,
key_read_limits: KeyReadLimits,
on_event: impl Fn(&ProgressEvent),
) -> Result<PublicKey, CryptoError> {
const _: () = match crate::format::WRITER_KEYPAIR_SUITE {
crate::format::KeypairSuite::V1 => (),
};
let (key_file_path, passphrase) = self.into_key_file_parts();
validate_passphrase(&passphrase)?;
let opened = recipient::native::x25519::open_x25519_key_file(
&key_file_path,
&passphrase,
Some(&kdf_limit),
key_read_limits,
&on_event,
)?;
drop(passphrase);
drop(opened.secret);
PublicKey::from_x25519_bytes(opened.public)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct KeyPairGenerator {
passphrase: Passphrase,
kdf_params: Option<KdfParams>,
kdf_limit: Option<KdfLimit>,
}
impl KeyPairGenerator {
pub fn with_passphrase(passphrase: Passphrase) -> Self {
Self {
passphrase,
kdf_params: None,
kdf_limit: None,
}
}
pub fn kdf_params(mut self, params: KdfParams) -> Self {
self.kdf_params = Some(params);
self
}
pub fn kdf_limit(mut self, limit: KdfLimit) -> Self {
self.kdf_limit = Some(limit);
self
}
pub fn write(
self,
output_dir: impl AsRef<Path>,
on_event: impl Fn(&ProgressEvent),
) -> Result<KeyGenOutcome, CryptoError> {
validate_passphrase(&self.passphrase)?;
let kdf_params = self.kdf_params.unwrap_or_default();
let kdf_limit = self.kdf_limit.unwrap_or_default();
let (private_key_path, public_key_path, recipient_string, fingerprint) =
protocol::generate_key_pair(
self.passphrase,
&kdf_params,
Some(&kdf_limit),
output_dir.as_ref(),
&on_event,
)?;
Ok(KeyGenOutcome {
private_key_path,
public_key_path,
recipient_string,
fingerprint,
})
}
}
pub fn generate_key_pair(
output_dir: impl AsRef<Path>,
passphrase: Passphrase,
on_event: impl Fn(&ProgressEvent),
) -> Result<KeyGenOutcome, CryptoError> {
KeyPairGenerator::with_passphrase(passphrase).write(output_dir, on_event)
}
pub fn probe_recipient_mode(
file_path: impl AsRef<Path>,
) -> Result<Option<UnauthenticatedRecipientMode>, CryptoError> {
probe_recipient_mode_with_limits(file_path, HeaderReadLimits::default())
}
pub fn probe_recipient_mode_with_limits(
file_path: impl AsRef<Path>,
limits: HeaderReadLimits,
) -> Result<Option<UnauthenticatedRecipientMode>, CryptoError> {
use std::io::Read;
let path = file_path.as_ref();
let Some(mut file) = open_header_input(path)? else {
return Ok(None);
};
let mut magic_buf = [0u8; format::MAGIC_SIZE];
let mut filled = 0;
while filled < magic_buf.len() {
match file.read(&mut magic_buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(CryptoError::Io(e)),
}
}
if filled < magic_buf.len() || magic_buf != format::MAGIC {
return Ok(None);
}
let session = protocol::DecryptSession::from_file(file, limits)?;
Ok(Some(session.mode()))
}
fn open_header_input(path: &Path) -> Result<Option<std::fs::File>, CryptoError> {
let file = match paths::open_input_file(path) {
Ok(file) => file,
Err(CryptoError::Io(error))
if error.kind() == std::io::ErrorKind::PermissionDenied && path.is_dir() =>
{
return Ok(None);
}
Err(error) => return Err(error),
};
if file.metadata().map_err(CryptoError::Io)?.is_dir() {
return Ok(None);
}
Ok(Some(file))
}
pub fn default_encrypted_filename(input_path: impl AsRef<Path>) -> Result<String, CryptoError> {
let base_name = paths::encryption_base_name(input_path)?;
Ok(format!("{}.{}", base_name, ENCRYPTED_EXTENSION))
}
pub fn validate_private_key_file(key_file: impl AsRef<Path>) -> Result<(), CryptoError> {
let data = crate::key::files::read_private_key_bytes(
key_file.as_ref(),
KeyReadLimits::PRIVATE_KEY_WRAPPED_SECRET_LEN_STRUCTURAL_MAX,
)?;
recipient::native::x25519::validate_private_key_shape(&data)
}
pub fn validate_public_key_file(key_file: impl AsRef<Path>) -> Result<(), CryptoError> {
PublicKey::from_key_file_with_limits(key_file, KeyReadLimits::structural_max()).map(|_| ())
}
pub(crate) fn validate_passphrase(passphrase: &Passphrase) -> Result<(), CryptoError> {
crate::crypto::kdf::check_passphrase_len(passphrase.expose())
}