use std::fs;
use std::path::{Path, PathBuf};
use secrecy::{ExposeSecret as _, SecretString};
use crate::archive::{self, ArchiveLimits, IncompleteOutputPolicy};
use crate::container::{self, HeaderReadLimits};
use crate::crypto::kdf::{KdfLimit, KdfParams};
use crate::error::FormatDefect;
use crate::format;
use crate::fs::paths;
use crate::key::files::KeyFileKind;
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(SecretString),
Recipients(Vec<PublicKey>),
}
impl Encryptor {
pub fn with_passphrase(passphrase: SecretString) -> 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: Vec<PublicKey> = public_keys.into_iter().collect();
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 save_as = self.save_as.as_deref();
if let EncryptorState::Passphrase(p) = &self.state {
validate_passphrase(p)?;
}
match &self.state {
EncryptorState::Recipients(rs) => {
preflight_header_write_limits(
header_read_limits,
rs.len(),
NativeRecipientType::X25519,
)?;
}
EncryptorState::Passphrase(_) => {
preflight_header_write_limits(
header_read_limits,
1,
NativeRecipientType::Argon2id,
)?;
let kdf_params = self.kdf_params.unwrap_or_default();
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: &passphrase,
kdf_params: self.kdf_params.unwrap_or_default(),
};
protocol::encrypt(
std::slice::from_ref(&recipient),
archive_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_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,
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 input = input.to_path_buf();
validate_input_path(&input)?;
if input.is_dir() {
return Err(CryptoError::InvalidInput(format!(
"Cannot decrypt a directory: {}",
input.display()
)));
}
let mode =
probe_recipient_mode_with_limits(&input, header_read_limits.unwrap_or_default())?
.ok_or(CryptoError::InvalidFormat(FormatDefect::BadMagic))?;
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,
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: SecretString,
output_dir: impl AsRef<Path>,
on_event: impl Fn(&ProgressEvent),
) -> Result<DecryptOutcome, CryptoError> {
validate_passphrase(&passphrase)?;
let credential = recipient::argon2id::PassphraseCredential {
passphrase: &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>,
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 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,
private_key_passphrase: SecretString,
output_dir: impl AsRef<Path>,
on_event: impl Fn(&ProgressEvent),
) -> Result<DecryptOutcome, CryptoError> {
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 probe = probe_recipient_mode_with_limits(&self.input, header_read_limits)?
.ok_or(CryptoError::InvalidFormat(FormatDefect::BadMagic))?;
if probe != UnauthenticatedRecipientMode::PublicKey {
return Err(CryptoError::DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode::PublicKey,
found: probe,
});
}
let private_key_bytes = recipient::native::x25519::open_x25519_private_key(
private_key.key_file_path(),
&private_key_passphrase,
self.kdf_limit.as_ref(),
&on_event,
)?;
let decryption_credential = recipient::x25519::X25519Credential { private_key_bytes };
let output_path = protocol::decrypt(
&decryption_credential,
&self.input,
output_dir.as_ref(),
archive_limits,
header_read_limits,
incomplete_output_policy,
&on_event,
)?;
Ok(DecryptOutcome {
output_path,
recipient_mode: AuthenticatedRecipientMode::public_key(),
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct KeyPairGenerator {
passphrase: SecretString,
kdf_params: Option<KdfParams>,
kdf_limit: Option<KdfLimit>,
}
impl KeyPairGenerator {
pub fn with_passphrase(passphrase: SecretString) -> 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();
kdf_params.validate_for_write(Some(&kdf_limit))?;
let (private_key_path, public_key_path, fingerprint) = protocol::generate_key_pair(
&self.passphrase,
&kdf_params,
output_dir.as_ref(),
&on_event,
)?;
Ok(KeyGenOutcome {
private_key_path,
public_key_path,
fingerprint,
})
}
}
pub fn generate_key_pair(
output_dir: impl AsRef<Path>,
passphrase: SecretString,
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, Seek, SeekFrom};
let path = file_path.as_ref();
if path.is_dir() {
return Ok(None);
}
let mut file = fs::File::open(path)?;
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) if e.kind() == std::io::ErrorKind::IsADirectory => return Ok(None),
Err(e) => return Err(CryptoError::Io(e)),
}
}
if filled < magic_buf.len() || magic_buf != format::MAGIC {
return Ok(None);
}
file.seek(SeekFrom::Start(0))?;
let parsed = container::read_encrypted_header(&mut file, limits)?;
let mode = recipient::classify_recipient_mode(&parsed.recipient_entries)?;
Ok(Some(mode))
}
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 = paths::read_file_capped(
key_file.as_ref(),
crate::key::private::PRIVATE_KEY_FILE_READ_CAP_BYTES,
|| CryptoError::InvalidFormat(FormatDefect::MalformedPrivateKey),
)?;
if matches!(KeyFileKind::classify(&data), KeyFileKind::Public) {
return Err(CryptoError::InvalidFormat(FormatDefect::WrongKeyFileType));
}
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(key_file).validate()
}
fn preflight_header_write_limits(
limits: HeaderReadLimits,
recipient_count: usize,
native: NativeRecipientType,
) -> Result<(), CryptoError> {
let type_name = native.type_name();
let body_len = native.body_len();
let count_u16: u16 = u16::try_from(recipient_count).unwrap_or(u16::MAX);
limits.enforce_recipient_count(count_u16)?;
let body_len_u32: u32 = u32::try_from(body_len).unwrap_or(u32::MAX);
limits.enforce_recipient_body_len(body_len_u32)?;
let overflow_err = || CryptoError::HeaderLenCapExceeded {
header_len: u32::MAX,
local_cap: limits.max_header_len,
};
let per_entry = (recipient::entry::ENTRY_HEADER_SIZE as u64)
.checked_add(type_name.len() as u64)
.and_then(|v| v.checked_add(body_len as u64))
.ok_or_else(overflow_err)?;
let total_entries = (recipient_count as u64)
.checked_mul(per_entry)
.ok_or_else(overflow_err)?;
let header_len_u64 = (format::HEADER_FIXED_SIZE as u64)
.checked_add(total_entries)
.ok_or_else(overflow_err)?;
let header_len = u32::try_from(header_len_u64).unwrap_or(u32::MAX);
limits.enforce_header_len(header_len)?;
Ok(())
}
pub(crate) fn validate_passphrase(passphrase: &SecretString) -> Result<(), CryptoError> {
if passphrase.expose_secret().is_empty() {
return Err(CryptoError::InvalidInput(
"Passphrase must not be empty".to_string(),
));
}
Ok(())
}
pub(crate) fn validate_input_path(input_path: &Path) -> Result<(), CryptoError> {
if !input_path.exists() {
return Err(CryptoError::InputPath);
}
Ok(())
}