Implement embedded OpenPGP compatibility (#4)

This commit is contained in:
Hermes Agent
2026-08-09 21:59:29 +00:00
parent 8e4c9390a0
commit 08bc244a4a
9 changed files with 1392 additions and 8 deletions

View File

@@ -0,0 +1,915 @@
//! 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)
}
/// 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 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(),
}
}

View File

@@ -7,6 +7,7 @@
pub mod command;
pub mod config;
pub mod crypto;
pub mod repository;
/// Product name shared by the presentation adapters.