//! 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, 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; } #[derive(Clone, Eq, PartialEq)] pub struct DetachedSignatureBytes(Vec); impl DetachedSignatureBytes { pub fn new(bytes: Vec) -> Self { Self(bytes) } pub fn as_bytes(&self) -> &[u8] { &self.0 } pub fn into_bytes(self) -> Vec { 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, } /// Imported and verified OpenPGP certificates, indexed by primary fingerprint. #[derive(Default)] pub struct KeyStore { keys: BTreeMap, } 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) -> Result { 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, 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::::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 + '_ { 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 { 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, 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 { 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 { 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::, 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::>(); 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 { 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::>(); 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 { 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 { 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 { 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> { 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> { 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( key: &K, password: &Password, data: &[u8], ) -> Result { 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(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 { 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, 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::, _>>() .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, length: u64, ) -> Result, 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(), } }