#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
#![warn(rustdoc::bare_urls)]
use std::path::PathBuf;
pub use crate::api::{
Decryptor, Encryptor, KeyPairGenerator, PassphraseDecryptor, PrivateKeyDecryptor,
default_encrypted_filename, generate_key_pair, probe_recipient_mode,
probe_recipient_mode_with_limits, validate_private_key_file, validate_public_key_file,
};
pub use crate::archive::{ArchiveLimits, IncompleteOutputPolicy};
pub use crate::container::HeaderReadLimits;
pub use crate::crypto::kdf::{KdfLimit, KdfParams};
pub use crate::error::{CryptoError, FormatDefect, InvalidKdfParams, UnsupportedVersion};
pub use crate::format::{ENCRYPTED_EXTENSION, FCR_FILE_VERSION, MAGIC};
pub use crate::key::files::{PRIVATE_KEY_FILENAME, PUBLIC_KEY_FILENAME};
pub use crate::key::private::{PRIVATE_KEY_V1_VERSION, PRIVATE_KEY_VERSION};
pub use crate::key::public::{PUBLIC_KEY_V1_VERSION, PUBLIC_KEY_VERSION};
pub use crate::recipient::policy::MixingPolicy;
pub use secrecy;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnauthenticatedRecipientMode {
Passphrase,
PublicKey,
}
impl std::fmt::Display for UnauthenticatedRecipientMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
Self::Passphrase => "passphrase",
Self::PublicKey => "public-key",
};
f.write_str(label)
}
}
impl UnauthenticatedRecipientMode {
pub(crate) const fn credential_name(self) -> &'static str {
match self {
Self::Passphrase => "a passphrase",
Self::PublicKey => "a private key",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AuthenticatedRecipientMode {
kind: AuthenticatedRecipientModeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AuthenticatedRecipientModeKind {
Passphrase,
PublicKey,
}
impl AuthenticatedRecipientMode {
pub(crate) const fn passphrase() -> Self {
Self {
kind: AuthenticatedRecipientModeKind::Passphrase,
}
}
pub(crate) const fn public_key() -> Self {
Self {
kind: AuthenticatedRecipientModeKind::PublicKey,
}
}
pub const fn kind(&self) -> AuthenticatedRecipientModeKind {
self.kind
}
pub const fn is_passphrase(&self) -> bool {
matches!(self.kind, AuthenticatedRecipientModeKind::Passphrase)
}
pub const fn is_public_key(&self) -> bool {
matches!(self.kind, AuthenticatedRecipientModeKind::PublicKey)
}
}
impl std::fmt::Display for AuthenticatedRecipientModeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
Self::Passphrase => "passphrase",
Self::PublicKey => "public-key",
};
f.write_str(label)
}
}
impl std::fmt::Display for AuthenticatedRecipientMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.kind.fmt(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProgressEvent {
DerivingPassphraseWrapKey,
UnlockingPrivateKey,
Encrypting,
Decrypting,
GeneratingKeyPair,
}
impl std::fmt::Display for ProgressEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Self::DerivingPassphraseWrapKey => "Deriving passphrase key\u{2026}",
Self::UnlockingPrivateKey => "Unlocking private key\u{2026}",
Self::Encrypting => "Encrypting\u{2026}",
Self::Decrypting => "Decrypting\u{2026}",
Self::GeneratingKeyPair => "Generating key pair\u{2026}",
};
f.write_str(msg)
}
}
pub use crate::key::private::PrivateKey;
pub use crate::key::public::PublicKey;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct EncryptOutcome {
pub output_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DecryptOutcome {
pub output_path: PathBuf,
pub recipient_mode: AuthenticatedRecipientMode,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeyGenOutcome {
pub private_key_path: PathBuf,
pub public_key_path: PathBuf,
pub fingerprint: String,
}
mod api;
mod archive;
mod container;
mod crypto;
mod error;
mod format;
mod fs;
mod key;
mod protocol;
mod recipient;
#[cfg(feature = "fuzzing")]
pub mod fuzz_exports;
pub fn decode_recipient_string(recipient_string: &str) -> Result<[u8; 32], CryptoError> {
key::public::decode_x25519_recipient(recipient_string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_recipient_mode_routes_argon2id_recipient_as_passphrase() {
let header_key =
crypto::keys::HeaderKey::from_bytes_for_tests([0x42u8; crypto::mac::HMAC_KEY_SIZE]);
let payload_key = crypto::keys::PayloadKey::from_bytes_for_tests(
[0u8; crypto::keys::ENCRYPTION_KEY_SIZE],
);
let stream_nonce = [0x07u8; format::STREAM_NONCE_SIZE];
let entry = recipient::RecipientEntry::native(
recipient::policy::NativeRecipientType::Argon2id,
vec![0u8; recipient::argon2id::BODY_LENGTH],
)
.unwrap();
let built = container::build_encrypted_header(
&[entry],
b"",
stream_nonce,
payload_key,
&header_key,
)
.unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&built.prefix_bytes);
bytes.extend_from_slice(&built.header_bytes);
bytes.extend_from_slice(&built.header_mac);
std::fs::write(tmp.path(), &bytes).unwrap();
assert_eq!(
probe_recipient_mode(tmp.path()).unwrap(),
Some(UnauthenticatedRecipientMode::Passphrase)
);
}
#[test]
fn probe_recipient_mode_routes_x25519_recipient_as_public_key() {
let header_key =
crypto::keys::HeaderKey::from_bytes_for_tests([0x42u8; crypto::mac::HMAC_KEY_SIZE]);
let payload_key = crypto::keys::PayloadKey::from_bytes_for_tests(
[0u8; crypto::keys::ENCRYPTION_KEY_SIZE],
);
let stream_nonce = [0x07u8; format::STREAM_NONCE_SIZE];
let entry = recipient::RecipientEntry::native(
recipient::policy::NativeRecipientType::X25519,
vec![0u8; recipient::x25519::BODY_LENGTH],
)
.unwrap();
let built = container::build_encrypted_header(
&[entry],
b"",
stream_nonce,
payload_key,
&header_key,
)
.unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&built.prefix_bytes);
bytes.extend_from_slice(&built.header_bytes);
bytes.extend_from_slice(&built.header_mac);
std::fs::write(tmp.path(), &bytes).unwrap();
assert_eq!(
probe_recipient_mode(tmp.path()).unwrap(),
Some(UnauthenticatedRecipientMode::PublicKey)
);
}
#[test]
fn probe_recipient_mode_returns_none_for_non_fcr_file() {
let plaintext = b"this is just a regular text file with no magic at all";
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), plaintext).unwrap();
assert_eq!(probe_recipient_mode(tmp.path()).unwrap(), None);
}
#[test]
fn probe_recipient_mode_returns_none_for_empty_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"").unwrap();
assert_eq!(probe_recipient_mode(tmp.path()).unwrap(), None);
}
#[test]
fn progress_events_display_exact_strings() {
assert_eq!(
ProgressEvent::DerivingPassphraseWrapKey.to_string(),
"Deriving passphrase key\u{2026}"
);
assert_eq!(
ProgressEvent::UnlockingPrivateKey.to_string(),
"Unlocking private key\u{2026}"
);
assert_eq!(ProgressEvent::Encrypting.to_string(), "Encrypting\u{2026}");
assert_eq!(ProgressEvent::Decrypting.to_string(), "Decrypting\u{2026}");
assert_eq!(
ProgressEvent::GeneratingKeyPair.to_string(),
"Generating key pair\u{2026}"
);
}
#[test]
fn decode_recipient_doc_cap_matches_constant() {
assert_eq!(
key::public::RECIPIENT_STRING_LEN_LOCAL_CAP_DEFAULT,
1_024,
"decode_recipient_string docstring inlines the cap value; \
update both lib.rs:decode_recipient_string and this test in \
the same commit if the cap changes"
);
}
#[test]
fn decode_recipient_rejects_all_zero_public_key() {
let s =
key::public::encode_recipient_string(recipient::x25519::TYPE_NAME, &[0u8; 32]).unwrap();
match decode_recipient_string(&s) {
Err(CryptoError::InvalidFormat(FormatDefect::MalformedPublicKey)) => {}
other => panic!("expected MalformedPublicKey, got {other:?}"),
}
}
#[test]
fn unauthenticated_recipient_mode_display_pinned_strings() {
assert_eq!(
UnauthenticatedRecipientMode::Passphrase.to_string(),
"passphrase"
);
assert_eq!(
UnauthenticatedRecipientMode::PublicKey.to_string(),
"public-key"
);
match UnauthenticatedRecipientMode::Passphrase {
UnauthenticatedRecipientMode::Passphrase | UnauthenticatedRecipientMode::PublicKey => {}
}
}
#[test]
fn authenticated_recipient_mode_display_pinned_strings() {
assert_eq!(
AuthenticatedRecipientModeKind::Passphrase.to_string(),
"passphrase"
);
assert_eq!(
AuthenticatedRecipientModeKind::PublicKey.to_string(),
"public-key"
);
assert_eq!(
AuthenticatedRecipientMode::passphrase().to_string(),
"passphrase"
);
assert_eq!(
AuthenticatedRecipientMode::public_key().to_string(),
"public-key"
);
}
#[test]
fn authenticated_recipient_mode_kind_round_trips() {
assert_eq!(
AuthenticatedRecipientMode::passphrase().kind(),
AuthenticatedRecipientModeKind::Passphrase
);
assert_eq!(
AuthenticatedRecipientMode::public_key().kind(),
AuthenticatedRecipientModeKind::PublicKey
);
assert!(AuthenticatedRecipientMode::passphrase().is_passphrase());
assert!(!AuthenticatedRecipientMode::passphrase().is_public_key());
assert!(AuthenticatedRecipientMode::public_key().is_public_key());
assert!(!AuthenticatedRecipientMode::public_key().is_passphrase());
}
}