968 lines
32 KiB
Rust
968 lines
32 KiB
Rust
//! Embedded OpenPGP operations for existing exported key material.
|
|
|
|
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
error::Error,
|
|
fmt, fs,
|
|
io::{self, Cursor, Read},
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use cap_std::{ambient_authority, fs::Dir};
|
|
use pgp::{
|
|
composed::{
|
|
Deserializable, DetachedSignature, Esk, Message, MessageBuilder, PublicOrSecret,
|
|
SignedPublicKey, SignedPublicSubKey, SignedSecretKey, SubpacketConfig,
|
|
},
|
|
crypto::{hash::HashAlgorithm, sym::SymmetricKeyAlgorithm},
|
|
packet::{SignatureType, Subpacket, SubpacketData},
|
|
ser::Serialize as _,
|
|
types::{KeyDetails as _, Password, SigningKey, Timestamp, VerifyingKey},
|
|
};
|
|
use rand::rngs::OsRng;
|
|
|
|
use crate::repository::{EncryptedEntry, SecretBytes};
|
|
|
|
const MAX_KEY_FILE_BYTES: u64 = 16 * 1024 * 1024;
|
|
const MAX_IMPORTED_KEYS: usize = 1024;
|
|
|
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
pub struct KeyFingerprint(String);
|
|
|
|
impl KeyFingerprint {
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for KeyFingerprint {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str(&self.0)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
pub struct KeyHandle(KeyFingerprint);
|
|
|
|
impl KeyHandle {
|
|
pub fn fingerprint(&self) -> &KeyFingerprint {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct KeyInfo {
|
|
fingerprint: KeyFingerprint,
|
|
key_id: String,
|
|
user_ids: Vec<String>,
|
|
has_secret: bool,
|
|
can_encrypt: bool,
|
|
can_sign: bool,
|
|
}
|
|
|
|
impl KeyInfo {
|
|
pub fn fingerprint(&self) -> &KeyFingerprint {
|
|
&self.fingerprint
|
|
}
|
|
|
|
pub fn key_id(&self) -> &str {
|
|
&self.key_id
|
|
}
|
|
|
|
pub fn user_ids(&self) -> &[String] {
|
|
&self.user_ids
|
|
}
|
|
|
|
pub fn has_secret(&self) -> bool {
|
|
self.has_secret
|
|
}
|
|
|
|
pub fn can_encrypt(&self) -> bool {
|
|
self.can_encrypt
|
|
}
|
|
|
|
pub fn can_sign(&self) -> bool {
|
|
self.can_sign
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum SecretProviderError {
|
|
Unavailable,
|
|
Cancelled,
|
|
}
|
|
|
|
/// Supplies an unlock secret without exposing it to configuration or the key store.
|
|
pub trait SecretProvider {
|
|
fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError>;
|
|
}
|
|
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub struct DetachedSignatureBytes(Vec<u8>);
|
|
|
|
impl DetachedSignatureBytes {
|
|
pub fn new(bytes: Vec<u8>) -> Self {
|
|
Self(bytes)
|
|
}
|
|
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
|
|
pub fn into_bytes(self) -> Vec<u8> {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for DetachedSignatureBytes {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("DetachedSignatureBytes")
|
|
.field("length", &self.0.len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
struct KeyMaterial {
|
|
public: SignedPublicKey,
|
|
secret: Option<SignedSecretKey>,
|
|
}
|
|
|
|
/// Imported and verified OpenPGP certificates, indexed by primary fingerprint.
|
|
#[derive(Default)]
|
|
pub struct KeyStore {
|
|
keys: BTreeMap<KeyFingerprint, KeyMaterial>,
|
|
}
|
|
|
|
impl fmt::Debug for KeyStore {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter
|
|
.debug_struct("KeyStore")
|
|
.field("key_count", &self.keys.len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl KeyStore {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Load a regular exported-key file or a directory tree made only of exported-key files.
|
|
pub fn load(path: impl AsRef<Path>) -> Result<Self, CryptoError> {
|
|
let path = path.as_ref();
|
|
let metadata = fs::symlink_metadata(path)
|
|
.map_err(|error| crypto_io("inspect key material", path, error))?;
|
|
if metadata.file_type().is_symlink() || (!metadata.is_file() && !metadata.is_dir()) {
|
|
return Err(CryptoError::UnsupportedKeyFileType {
|
|
path: path.to_owned(),
|
|
});
|
|
}
|
|
let mut store = Self::new();
|
|
if metadata.is_file() {
|
|
let bytes = read_ambient_key_file(path, metadata.len())?;
|
|
store.import(&bytes)?;
|
|
} else {
|
|
let canonical = fs::canonicalize(path)
|
|
.map_err(|error| crypto_io("canonicalize key directory", path, error))?;
|
|
let directory = Dir::open_ambient_dir(&canonical, ambient_authority())
|
|
.map_err(|error| crypto_io("open key directory", &canonical, error))?;
|
|
import_key_directory(&mut store, &directory, Path::new(""))?;
|
|
}
|
|
if store.keys.is_empty() {
|
|
return Err(CryptoError::NoKeyMaterial);
|
|
}
|
|
Ok(store)
|
|
}
|
|
|
|
/// Transactionally import one armored or binary stream containing public and/or secret keys.
|
|
pub fn import(&mut self, bytes: &[u8]) -> Result<Vec<KeyInfo>, CryptoError> {
|
|
if bytes.len() as u64 > MAX_KEY_FILE_BYTES {
|
|
return Err(CryptoError::KeyMaterialTooLarge);
|
|
}
|
|
let (parsed, _) = PublicOrSecret::from_reader_many(Cursor::new(bytes))
|
|
.map_err(|_| CryptoError::CorruptKeyMaterial)?;
|
|
let mut pending = BTreeMap::<KeyFingerprint, KeyMaterial>::new();
|
|
for parsed_key in parsed {
|
|
let parsed_key = parsed_key.map_err(|_| CryptoError::CorruptKeyMaterial)?;
|
|
parsed_key
|
|
.verify_bindings()
|
|
.map_err(|_| CryptoError::InvalidKeyBindings)?;
|
|
let (public, secret) = match parsed_key {
|
|
PublicOrSecret::Public(public) => (public, None),
|
|
PublicOrSecret::Secret(secret) => (secret.to_public_key(), Some(secret)),
|
|
};
|
|
let fingerprint = fingerprint_of(&public);
|
|
if let Some(existing) = pending.get_mut(&fingerprint) {
|
|
if existing.public.primary_key != public.primary_key {
|
|
return Err(CryptoError::DuplicateFingerprint { fingerprint });
|
|
}
|
|
if existing.secret.is_none() {
|
|
existing.secret = secret;
|
|
}
|
|
} else {
|
|
pending.insert(fingerprint, KeyMaterial { public, secret });
|
|
}
|
|
if self.keys.len() + pending.len() > MAX_IMPORTED_KEYS {
|
|
return Err(CryptoError::TooManyKeys);
|
|
}
|
|
}
|
|
if pending.is_empty() {
|
|
return Err(CryptoError::NoKeyMaterial);
|
|
}
|
|
for (fingerprint, material) in &pending {
|
|
if self
|
|
.keys
|
|
.get(fingerprint)
|
|
.is_some_and(|existing| existing.public.primary_key != material.public.primary_key)
|
|
{
|
|
return Err(CryptoError::DuplicateFingerprint {
|
|
fingerprint: fingerprint.clone(),
|
|
});
|
|
}
|
|
}
|
|
for (fingerprint, material) in pending {
|
|
if let Some(existing) = self.keys.get_mut(&fingerprint) {
|
|
if existing.secret.is_none() {
|
|
existing.secret = material.secret;
|
|
}
|
|
} else {
|
|
self.keys.insert(fingerprint, material);
|
|
}
|
|
}
|
|
Ok(self.infos().collect())
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.keys.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.keys.is_empty()
|
|
}
|
|
|
|
pub fn infos(&self) -> impl Iterator<Item = KeyInfo> + '_ {
|
|
self.keys.values().map(key_info)
|
|
}
|
|
|
|
/// Resolve a full primary/subkey fingerprint, 8/16-digit key ID, or exact UTF-8 user ID.
|
|
pub fn resolve(&self, identity: &str) -> Result<KeyHandle, CryptoError> {
|
|
let hex_identity = normalize_hex_identity(identity);
|
|
let mut matches = BTreeSet::new();
|
|
if let Some(hex_identity) = hex_identity {
|
|
for (fingerprint, key) in &self.keys {
|
|
if key_matches_hex(key, &hex_identity) {
|
|
matches.insert(fingerprint.clone());
|
|
}
|
|
}
|
|
}
|
|
if matches.is_empty() {
|
|
for (fingerprint, key) in &self.keys {
|
|
if key
|
|
.public
|
|
.details
|
|
.users
|
|
.iter()
|
|
.any(|user| user.id.as_str() == Some(identity))
|
|
{
|
|
matches.insert(fingerprint.clone());
|
|
}
|
|
}
|
|
}
|
|
match matches.len() {
|
|
0 => Err(CryptoError::MissingIdentity {
|
|
identity: identity.to_owned(),
|
|
}),
|
|
1 => Ok(KeyHandle(matches.pop_first().expect("one match"))),
|
|
_ => Err(CryptoError::AmbiguousIdentity {
|
|
identity: identity.to_owned(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Apply upstream `.gpg-id` comment and whitespace rules, then resolve every recipient.
|
|
pub fn resolve_recipients(&self, contents: &[u8]) -> Result<Vec<KeyHandle>, CryptoError> {
|
|
let contents =
|
|
std::str::from_utf8(contents).map_err(|_| CryptoError::InvalidRecipientFile)?;
|
|
let mut recipients = Vec::new();
|
|
let mut seen = BTreeSet::new();
|
|
for line in contents.lines() {
|
|
let identity = line.split('#').next().unwrap_or_default().trim();
|
|
if identity.is_empty() {
|
|
continue;
|
|
}
|
|
let recipient = self.resolve(identity)?;
|
|
if seen.insert(recipient.clone()) {
|
|
recipients.push(recipient);
|
|
}
|
|
}
|
|
if recipients.is_empty() {
|
|
return Err(CryptoError::MissingRecipients);
|
|
}
|
|
Ok(recipients)
|
|
}
|
|
|
|
/// Encrypt a plaintext for every resolved recipient using pass-compatible uncompressed
|
|
/// SEIPD v1 with AES-256.
|
|
pub fn encrypt(
|
|
&self,
|
|
plaintext: SecretBytes,
|
|
recipients: &[KeyHandle],
|
|
) -> Result<EncryptedEntry, CryptoError> {
|
|
if recipients.is_empty() {
|
|
return Err(CryptoError::MissingRecipients);
|
|
}
|
|
let mut unique = BTreeSet::new();
|
|
let mut targets = Vec::new();
|
|
for recipient in recipients {
|
|
if !unique.insert(recipient) {
|
|
continue;
|
|
}
|
|
let material = self.material(recipient)?;
|
|
let target = encryption_target(&material.public).ok_or_else(|| {
|
|
CryptoError::MissingEncryptionKey {
|
|
fingerprint: recipient.0.clone(),
|
|
}
|
|
})?;
|
|
targets.push(target);
|
|
}
|
|
let reader = SecretReader::new(plaintext);
|
|
let mut rng = OsRng;
|
|
let mut message = MessageBuilder::from_reader("", reader)
|
|
.seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256);
|
|
for target in targets {
|
|
match target {
|
|
EncryptionTarget::Primary(key) => message
|
|
.encrypt_to_key(&mut rng, key)
|
|
.map_err(|_| CryptoError::EncryptionFailed)?,
|
|
EncryptionTarget::Subkey(key) => message
|
|
.encrypt_to_key(&mut rng, key)
|
|
.map_err(|_| CryptoError::EncryptionFailed)?,
|
|
};
|
|
}
|
|
message
|
|
.to_vec(rng)
|
|
.map(EncryptedEntry::new)
|
|
.map_err(|_| CryptoError::EncryptionFailed)
|
|
}
|
|
|
|
/// Report whether every public-key session packet names exactly the requested primary
|
|
/// certificates. Unknown, anonymous, symmetric, missing, or extra recipients do not match.
|
|
pub fn is_encrypted_for(
|
|
&self,
|
|
ciphertext: &EncryptedEntry,
|
|
recipients: &[KeyHandle],
|
|
) -> Result<bool, CryptoError> {
|
|
let message = Message::from_bytes(Cursor::new(ciphertext.as_bytes()))
|
|
.map_err(|_| CryptoError::CorruptMessage)?;
|
|
let Message::Encrypted { esk, .. } = message else {
|
|
return Err(CryptoError::CorruptMessage);
|
|
};
|
|
let expected = recipients
|
|
.iter()
|
|
.map(|recipient| {
|
|
self.material(recipient)?;
|
|
Ok(recipient.0.clone())
|
|
})
|
|
.collect::<Result<BTreeSet<_>, CryptoError>>()?;
|
|
if expected.is_empty() {
|
|
return Ok(false);
|
|
}
|
|
let mut actual = BTreeSet::new();
|
|
for packet in esk {
|
|
let Esk::PublicKeyEncryptedSessionKey(packet) = packet else {
|
|
return Ok(false);
|
|
};
|
|
let matches = self
|
|
.keys
|
|
.iter()
|
|
.filter(|(_, material)| packet_matches_public(&packet, &material.public))
|
|
.map(|(fingerprint, _)| fingerprint)
|
|
.collect::<Vec<_>>();
|
|
if matches.len() != 1 {
|
|
return Ok(false);
|
|
}
|
|
actual.insert((*matches[0]).clone());
|
|
}
|
|
Ok(actual == expected)
|
|
}
|
|
|
|
/// Decrypt a pass entry with only the secret keys named by its PKESK packets.
|
|
pub fn decrypt(
|
|
&self,
|
|
ciphertext: &EncryptedEntry,
|
|
provider: &mut impl SecretProvider,
|
|
) -> Result<SecretBytes, CryptoError> {
|
|
let message = Message::from_bytes(Cursor::new(ciphertext.as_bytes()))
|
|
.map_err(|_| CryptoError::CorruptMessage)?;
|
|
if !message.is_encrypted() {
|
|
return Err(CryptoError::CorruptMessage);
|
|
}
|
|
let candidates = self
|
|
.keys
|
|
.iter()
|
|
.filter(|(_, material)| {
|
|
material
|
|
.secret
|
|
.as_ref()
|
|
.is_some_and(|secret| message_matches_secret(&message, secret))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if candidates.is_empty() {
|
|
return Err(CryptoError::MissingSecretKey);
|
|
}
|
|
|
|
let mut unavailable = None;
|
|
let mut attempted = false;
|
|
for (fingerprint, material) in &candidates {
|
|
let secret = material.secret.as_ref().expect("filtered secret key");
|
|
let supplied = if secret_requires_password(secret) {
|
|
let info = key_info(material);
|
|
match provider.secret_for(&info) {
|
|
Ok(passphrase) => passphrase,
|
|
Err(SecretProviderError::Unavailable) => {
|
|
unavailable.get_or_insert_with(|| CryptoError::SecretProvider {
|
|
fingerprint: (*fingerprint).clone(),
|
|
reason: SecretProviderError::Unavailable,
|
|
});
|
|
continue;
|
|
}
|
|
Err(reason @ SecretProviderError::Cancelled) => {
|
|
return Err(CryptoError::SecretProvider {
|
|
fingerprint: (*fingerprint).clone(),
|
|
reason,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
SecretBytes::new(Vec::new())
|
|
};
|
|
attempted = true;
|
|
let password = Password::from(supplied.expose());
|
|
let message = Message::from_bytes(Cursor::new(ciphertext.as_bytes()))
|
|
.map_err(|_| CryptoError::CorruptMessage)?;
|
|
if let Ok(mut decrypted) = message.decrypt(&password, secret)
|
|
&& let Ok(plaintext) = decrypted.as_data_vec()
|
|
{
|
|
return Ok(SecretBytes::new(plaintext));
|
|
}
|
|
}
|
|
if attempted {
|
|
Err(CryptoError::DecryptionFailed)
|
|
} else {
|
|
Err(unavailable.expect("every candidate was unavailable"))
|
|
}
|
|
}
|
|
|
|
pub fn sign(
|
|
&self,
|
|
data: &[u8],
|
|
signer: &KeyHandle,
|
|
provider: &mut impl SecretProvider,
|
|
) -> Result<DetachedSignatureBytes, CryptoError> {
|
|
let material = self.material(signer)?;
|
|
let secret = material
|
|
.secret
|
|
.as_ref()
|
|
.ok_or_else(|| CryptoError::MissingSigningKey {
|
|
fingerprint: signer.0.clone(),
|
|
})?;
|
|
let target = signing_target(secret).ok_or_else(|| CryptoError::MissingSigningKey {
|
|
fingerprint: signer.0.clone(),
|
|
})?;
|
|
let supplied = if signing_target_requires_password(target) {
|
|
provider.secret_for(&key_info(material)).map_err(|reason| {
|
|
CryptoError::SecretProvider {
|
|
fingerprint: signer.0.clone(),
|
|
reason,
|
|
}
|
|
})?
|
|
} else {
|
|
SecretBytes::new(Vec::new())
|
|
};
|
|
let password = Password::from(supplied.expose());
|
|
let signature = match target {
|
|
SigningTarget::Primary(key) => sign_data(key, &password, data),
|
|
SigningTarget::Subkey(key) => sign_data(key, &password, data),
|
|
}?;
|
|
let mut bytes = Vec::new();
|
|
signature
|
|
.to_writer(&mut bytes)
|
|
.map_err(|_| CryptoError::SigningFailed)?;
|
|
Ok(DetachedSignatureBytes(bytes))
|
|
}
|
|
|
|
/// Verify a detached `.gpg-id.sig` against an explicit set of allowed primary identities.
|
|
pub fn verify(
|
|
&self,
|
|
data: &[u8],
|
|
signature: &DetachedSignatureBytes,
|
|
allowed_signers: &[KeyHandle],
|
|
) -> Result<KeyHandle, CryptoError> {
|
|
if allowed_signers.is_empty() {
|
|
return Err(CryptoError::InvalidSignature);
|
|
}
|
|
let signature = DetachedSignature::from_bytes(Cursor::new(signature.as_bytes()))
|
|
.map_err(|_| CryptoError::CorruptSignature)?;
|
|
for signer in allowed_signers {
|
|
let material = self.material(signer)?;
|
|
if verify_with_public(&signature, &material.public, data) {
|
|
return Ok(signer.clone());
|
|
}
|
|
}
|
|
Err(CryptoError::InvalidSignature)
|
|
}
|
|
|
|
fn material(&self, handle: &KeyHandle) -> Result<&KeyMaterial, CryptoError> {
|
|
self.keys
|
|
.get(&handle.0)
|
|
.ok_or_else(|| CryptoError::MissingIdentity {
|
|
identity: handle.0.0.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum CryptoError {
|
|
UnsupportedKeyFileType {
|
|
path: PathBuf,
|
|
},
|
|
KeyMaterialTooLarge,
|
|
TooManyKeys,
|
|
NoKeyMaterial,
|
|
CorruptKeyMaterial,
|
|
InvalidKeyBindings,
|
|
DuplicateFingerprint {
|
|
fingerprint: KeyFingerprint,
|
|
},
|
|
MissingIdentity {
|
|
identity: String,
|
|
},
|
|
AmbiguousIdentity {
|
|
identity: String,
|
|
},
|
|
InvalidRecipientFile,
|
|
MissingRecipients,
|
|
MissingEncryptionKey {
|
|
fingerprint: KeyFingerprint,
|
|
},
|
|
CorruptMessage,
|
|
MissingSecretKey,
|
|
SecretProvider {
|
|
fingerprint: KeyFingerprint,
|
|
reason: SecretProviderError,
|
|
},
|
|
DecryptionFailed,
|
|
MissingSigningKey {
|
|
fingerprint: KeyFingerprint,
|
|
},
|
|
EncryptionFailed,
|
|
SigningFailed,
|
|
CorruptSignature,
|
|
InvalidSignature,
|
|
Io {
|
|
operation: &'static str,
|
|
path: PathBuf,
|
|
source: io::ErrorKind,
|
|
},
|
|
}
|
|
|
|
impl fmt::Display for CryptoError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::UnsupportedKeyFileType { path } => {
|
|
write!(
|
|
formatter,
|
|
"unsupported exported-key file type: {}",
|
|
path.display()
|
|
)
|
|
}
|
|
Self::KeyMaterialTooLarge => formatter.write_str("exported key material is too large"),
|
|
Self::TooManyKeys => formatter.write_str("too many exported keys"),
|
|
Self::NoKeyMaterial => formatter.write_str("no OpenPGP key material was found"),
|
|
Self::CorruptKeyMaterial => formatter.write_str("OpenPGP key material is malformed"),
|
|
Self::InvalidKeyBindings => formatter.write_str("OpenPGP key bindings are invalid"),
|
|
Self::DuplicateFingerprint { fingerprint } => {
|
|
write!(
|
|
formatter,
|
|
"conflicting OpenPGP key fingerprint: {fingerprint}"
|
|
)
|
|
}
|
|
Self::MissingIdentity { identity } => {
|
|
write!(formatter, "OpenPGP identity was not found: {identity}")
|
|
}
|
|
Self::AmbiguousIdentity { identity } => {
|
|
write!(formatter, "OpenPGP identity is ambiguous: {identity}")
|
|
}
|
|
Self::InvalidRecipientFile => formatter.write_str(".gpg-id is not valid UTF-8"),
|
|
Self::MissingRecipients => formatter.write_str("no OpenPGP recipients were provided"),
|
|
Self::MissingEncryptionKey { fingerprint } => {
|
|
write!(formatter, "OpenPGP key cannot encrypt: {fingerprint}")
|
|
}
|
|
Self::CorruptMessage => formatter.write_str("encrypted OpenPGP message is malformed"),
|
|
Self::MissingSecretKey => {
|
|
formatter.write_str("no imported secret key can decrypt the message")
|
|
}
|
|
Self::SecretProvider {
|
|
fingerprint,
|
|
reason,
|
|
} => write!(
|
|
formatter,
|
|
"secret provider could not unlock OpenPGP key {fingerprint}: {reason:?}"
|
|
),
|
|
Self::DecryptionFailed => formatter.write_str("OpenPGP decryption failed"),
|
|
Self::MissingSigningKey { fingerprint } => {
|
|
write!(formatter, "OpenPGP key cannot sign: {fingerprint}")
|
|
}
|
|
Self::EncryptionFailed => formatter.write_str("OpenPGP encryption failed"),
|
|
Self::SigningFailed => formatter.write_str("OpenPGP signing failed"),
|
|
Self::CorruptSignature => {
|
|
formatter.write_str("detached OpenPGP signature is malformed")
|
|
}
|
|
Self::InvalidSignature => formatter.write_str("detached OpenPGP signature is invalid"),
|
|
Self::Io {
|
|
operation,
|
|
path,
|
|
source,
|
|
} => write!(formatter, "cannot {operation} {}: {source}", path.display()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for CryptoError {}
|
|
|
|
fn fingerprint_of(key: &SignedPublicKey) -> KeyFingerprint {
|
|
KeyFingerprint(format!("{:X}", key.fingerprint()))
|
|
}
|
|
|
|
fn key_info(material: &KeyMaterial) -> KeyInfo {
|
|
KeyInfo {
|
|
fingerprint: fingerprint_of(&material.public),
|
|
key_id: material
|
|
.public
|
|
.legacy_key_id()
|
|
.to_string()
|
|
.to_ascii_uppercase(),
|
|
user_ids: material
|
|
.public
|
|
.details
|
|
.users
|
|
.iter()
|
|
.filter_map(|user| user.id.as_str().map(str::to_owned))
|
|
.collect(),
|
|
has_secret: material.secret.is_some(),
|
|
can_encrypt: encryption_target(&material.public).is_some(),
|
|
can_sign: can_sign(&material.public),
|
|
}
|
|
}
|
|
|
|
fn normalize_hex_identity(identity: &str) -> Option<String> {
|
|
let identity = identity
|
|
.strip_prefix("0x")
|
|
.or_else(|| identity.strip_prefix("0X"))
|
|
.unwrap_or(identity);
|
|
if matches!(identity.len(), 8 | 16 | 40 | 64)
|
|
&& identity.bytes().all(|byte| byte.is_ascii_hexdigit())
|
|
{
|
|
Some(identity.to_ascii_uppercase())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn key_matches_hex(key: &KeyMaterial, identity: &str) -> bool {
|
|
let mut identities = vec![
|
|
format!("{:X}", key.public.fingerprint()),
|
|
key.public.legacy_key_id().to_string().to_ascii_uppercase(),
|
|
];
|
|
for subkey in &key.public.public_subkeys {
|
|
identities.push(format!("{:X}", subkey.fingerprint()));
|
|
identities.push(subkey.legacy_key_id().to_string().to_ascii_uppercase());
|
|
}
|
|
identities
|
|
.iter()
|
|
.any(|candidate| candidate == identity || candidate.ends_with(identity))
|
|
}
|
|
|
|
enum EncryptionTarget<'a> {
|
|
Primary(&'a SignedPublicKey),
|
|
Subkey(&'a SignedPublicSubKey),
|
|
}
|
|
|
|
fn encryption_target(key: &SignedPublicKey) -> Option<EncryptionTarget<'_>> {
|
|
for subkey in &key.public_subkeys {
|
|
let revoked = subkey
|
|
.signatures
|
|
.iter()
|
|
.any(|signature| signature.typ() == Some(SignatureType::SubkeyRevocation));
|
|
let encrypts = subkey.signatures.iter().any(|signature| {
|
|
let flags = signature.key_flags();
|
|
flags.encrypt_comms() || flags.encrypt_storage()
|
|
});
|
|
if encrypts && !revoked {
|
|
return Some(EncryptionTarget::Subkey(subkey));
|
|
}
|
|
}
|
|
if primary_flags(key, |flags| {
|
|
flags.encrypt_comms() || flags.encrypt_storage()
|
|
}) {
|
|
Some(EncryptionTarget::Primary(key))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum SigningTarget<'a> {
|
|
Primary(&'a pgp::packet::SecretKey),
|
|
Subkey(&'a pgp::packet::SecretSubkey),
|
|
}
|
|
|
|
fn signing_target(key: &SignedSecretKey) -> Option<SigningTarget<'_>> {
|
|
for subkey in &key.secret_subkeys {
|
|
let revoked = subkey
|
|
.signatures
|
|
.iter()
|
|
.any(|signature| signature.typ() == Some(SignatureType::SubkeyRevocation));
|
|
let signs = subkey
|
|
.signatures
|
|
.iter()
|
|
.any(|signature| signature.key_flags().sign());
|
|
if signs && !revoked {
|
|
return Some(SigningTarget::Subkey(&subkey.key));
|
|
}
|
|
}
|
|
let public = key.to_public_key();
|
|
if primary_flags(&public, |flags| flags.sign()) {
|
|
Some(SigningTarget::Primary(&key.primary_key))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn can_sign(key: &SignedPublicKey) -> bool {
|
|
key.public_subkeys.iter().any(|subkey| {
|
|
let revoked = subkey
|
|
.signatures
|
|
.iter()
|
|
.any(|signature| signature.typ() == Some(SignatureType::SubkeyRevocation));
|
|
!revoked
|
|
&& subkey
|
|
.signatures
|
|
.iter()
|
|
.any(|signature| signature.key_flags().sign())
|
|
}) || primary_flags(key, |flags| flags.sign())
|
|
}
|
|
|
|
fn primary_flags(
|
|
key: &SignedPublicKey,
|
|
predicate: impl Fn(&pgp::packet::KeyFlags) -> bool,
|
|
) -> bool {
|
|
key.details
|
|
.direct_signatures
|
|
.iter()
|
|
.chain(
|
|
key.details
|
|
.users
|
|
.iter()
|
|
.flat_map(|user| user.signatures.iter()),
|
|
)
|
|
.any(|signature| predicate(&signature.key_flags()))
|
|
}
|
|
|
|
fn secret_requires_password(key: &SignedSecretKey) -> bool {
|
|
key.primary_key.secret_params().is_encrypted()
|
|
|| key
|
|
.secret_subkeys
|
|
.iter()
|
|
.any(|subkey| subkey.secret_params().is_encrypted())
|
|
}
|
|
|
|
fn signing_target_requires_password(target: SigningTarget<'_>) -> bool {
|
|
match target {
|
|
SigningTarget::Primary(key) => key.secret_params().is_encrypted(),
|
|
SigningTarget::Subkey(key) => key.secret_params().is_encrypted(),
|
|
}
|
|
}
|
|
|
|
fn message_matches_secret(message: &Message<'_>, key: &SignedSecretKey) -> bool {
|
|
let Message::Encrypted { esk, .. } = message else {
|
|
return false;
|
|
};
|
|
esk.iter().any(|esk| {
|
|
let Esk::PublicKeyEncryptedSessionKey(esk) = esk else {
|
|
return false;
|
|
};
|
|
esk.match_identity(key.primary_key.public_key())
|
|
|| key
|
|
.secret_subkeys
|
|
.iter()
|
|
.any(|subkey| esk.match_identity(subkey.public_key()))
|
|
})
|
|
}
|
|
|
|
fn packet_matches_public(
|
|
packet: &pgp::packet::PublicKeyEncryptedSessionKey,
|
|
key: &SignedPublicKey,
|
|
) -> bool {
|
|
packet.match_identity(&key.primary_key)
|
|
|| key
|
|
.public_subkeys
|
|
.iter()
|
|
.any(|subkey| packet.match_identity(&subkey.key))
|
|
}
|
|
|
|
fn sign_data<K: SigningKey>(
|
|
key: &K,
|
|
password: &Password,
|
|
data: &[u8],
|
|
) -> Result<DetachedSignature, CryptoError> {
|
|
let hashed = vec![
|
|
Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint()))
|
|
.map_err(|_| CryptoError::SigningFailed)?,
|
|
Subpacket::regular(SubpacketData::SignatureCreationTime(Timestamp::now()))
|
|
.map_err(|_| CryptoError::SigningFailed)?,
|
|
];
|
|
let unhashed = vec![
|
|
Subpacket::regular(SubpacketData::IssuerKeyId(key.legacy_key_id()))
|
|
.map_err(|_| CryptoError::SigningFailed)?,
|
|
];
|
|
DetachedSignature::sign_binary_data_with_subpackets(
|
|
OsRng,
|
|
key,
|
|
password,
|
|
HashAlgorithm::Sha256,
|
|
data,
|
|
SubpacketConfig::UserDefined { hashed, unhashed },
|
|
)
|
|
.map_err(|_| CryptoError::SigningFailed)
|
|
}
|
|
|
|
fn verify_with_public(signature: &DetachedSignature, key: &SignedPublicKey, data: &[u8]) -> bool {
|
|
if verify_data(signature, &key.primary_key, data) {
|
|
return true;
|
|
}
|
|
key.public_subkeys
|
|
.iter()
|
|
.any(|subkey| verify_data(signature, &subkey.key, data))
|
|
}
|
|
|
|
fn verify_data<K: VerifyingKey>(signature: &DetachedSignature, key: &K, data: &[u8]) -> bool {
|
|
signature.verify(key, data).is_ok()
|
|
}
|
|
|
|
struct SecretReader {
|
|
secret: SecretBytes,
|
|
position: usize,
|
|
}
|
|
|
|
impl SecretReader {
|
|
fn new(secret: SecretBytes) -> Self {
|
|
Self {
|
|
secret,
|
|
position: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Read for SecretReader {
|
|
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
|
let remaining = &self.secret.expose()[self.position..];
|
|
let length = remaining.len().min(buffer.len());
|
|
buffer[..length].copy_from_slice(&remaining[..length]);
|
|
self.position += length;
|
|
Ok(length)
|
|
}
|
|
}
|
|
|
|
fn read_ambient_key_file(path: &Path, length: u64) -> Result<Vec<u8>, CryptoError> {
|
|
if length > MAX_KEY_FILE_BYTES {
|
|
return Err(CryptoError::KeyMaterialTooLarge);
|
|
}
|
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
|
let name = path
|
|
.file_name()
|
|
.ok_or_else(|| CryptoError::UnsupportedKeyFileType {
|
|
path: path.to_owned(),
|
|
})?;
|
|
let parent = fs::canonicalize(parent)
|
|
.map_err(|error| crypto_io("canonicalize key parent", parent, error))?;
|
|
let directory = Dir::open_ambient_dir(&parent, ambient_authority())
|
|
.map_err(|error| crypto_io("open key parent", &parent, error))?;
|
|
let metadata = directory
|
|
.symlink_metadata(name)
|
|
.map_err(|error| crypto_io("inspect key file", path, error))?;
|
|
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
|
return Err(CryptoError::UnsupportedKeyFileType {
|
|
path: path.to_owned(),
|
|
});
|
|
}
|
|
read_cap_file(&directory, name, path, metadata.len())
|
|
}
|
|
|
|
fn import_key_directory(
|
|
store: &mut KeyStore,
|
|
directory: &Dir,
|
|
relative: &Path,
|
|
) -> Result<(), CryptoError> {
|
|
let mut entries = directory
|
|
.read_dir(".")
|
|
.map_err(|error| crypto_io("read key directory", relative, error))?
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|error| crypto_io("read key directory entry", relative, error))?;
|
|
entries.sort_by_key(cap_std::fs::DirEntry::file_name);
|
|
for entry in entries {
|
|
let name = entry.file_name();
|
|
let path = relative.join(&name);
|
|
let file_type = entry
|
|
.file_type()
|
|
.map_err(|error| crypto_io("inspect key directory entry", &path, error))?;
|
|
if file_type.is_symlink() || (!file_type.is_file() && !file_type.is_dir()) {
|
|
return Err(CryptoError::UnsupportedKeyFileType { path });
|
|
}
|
|
if file_type.is_dir() {
|
|
let child = entry
|
|
.open_dir()
|
|
.map_err(|error| crypto_io("open key directory", &path, error))?;
|
|
import_key_directory(store, &child, &path)?;
|
|
} else {
|
|
let length = entry
|
|
.metadata()
|
|
.map_err(|error| crypto_io("inspect key file", &path, error))?
|
|
.len();
|
|
let bytes = read_cap_file(directory, &name, &path, length)?;
|
|
store.import(&bytes)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn read_cap_file(
|
|
directory: &Dir,
|
|
name: impl AsRef<Path>,
|
|
path: &Path,
|
|
length: u64,
|
|
) -> Result<Vec<u8>, CryptoError> {
|
|
if length > MAX_KEY_FILE_BYTES {
|
|
return Err(CryptoError::KeyMaterialTooLarge);
|
|
}
|
|
let mut file = directory
|
|
.open(name)
|
|
.map_err(|error| crypto_io("open key file", path, error))?;
|
|
let mut bytes = Vec::with_capacity(length as usize);
|
|
file.read_to_end(&mut bytes)
|
|
.map_err(|error| crypto_io("read key file", path, error))?;
|
|
if bytes.len() as u64 > MAX_KEY_FILE_BYTES {
|
|
return Err(CryptoError::KeyMaterialTooLarge);
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn crypto_io(operation: &'static str, path: &Path, error: io::Error) -> CryptoError {
|
|
CryptoError::Io {
|
|
operation,
|
|
path: path.to_owned(),
|
|
source: error.kind(),
|
|
}
|
|
}
|