Prepare Apple App Store distribution (#59)

This commit is contained in:
hugo committed 2026-08-16 17:55:59 +02:00
1 parent b50e47ab73
commit f894d43eb6
46 files changed
+20694 -166

No files matched your search

+95 -20
View File
@@ -181,7 +181,7 @@ impl MobileKeyTransferService {
}
pub struct MobileKeyTransferImport {
config: Config,
destination: MobileKeyTransferDestination,
digest: Option<String>,
total: Option<usize>,
chunks: BTreeMap<usize, Vec<u8>>,
@@ -189,10 +189,26 @@ pub struct MobileKeyTransferImport {
key: Option<MobileKeyTransferKey>,
}
enum MobileKeyTransferDestination {
Configured(Box<Config>),
LocalSetup,
}
impl MobileKeyTransferImport {
fn new(config: Config) -> Self {
Self {
config,
destination: MobileKeyTransferDestination::Configured(Box::new(config)),
digest: None,
total: None,
chunks: BTreeMap::new(),
complete: None,
key: None,
}
}
pub fn local_setup() -> Self {
Self {
destination: MobileKeyTransferDestination::LocalSetup,
digest: None,
total: None,
chunks: BTreeMap::new(),
@@ -280,6 +296,7 @@ impl MobileKeyTransferImport {
&mut self,
passphrase: Option<SecretBytes>,
make_default: bool,
overwrite: bool,
) -> Result<MobileKeyTransferOutcome, MobileKeyTransferError> {
let armor = self
.complete
@@ -298,20 +315,46 @@ impl MobileKeyTransferImport {
return Err(MobileKeyTransferError::PublicDefault);
}
let existing = KeyStore::load(self.config.key_material())?;
if matches!(self.destination, MobileKeyTransferDestination::LocalSetup) {
if key.kind != MobileKeyTransferKind::Private || !make_default {
return Err(MobileKeyTransferError::PublicDefault);
}
let outcome = crate::mobile_onboarding::MobileOnboardingOperation::default()
.setup_local_imported(
SecretBytes::new(armor.expose().to_vec()),
passphrase,
overwrite,
)
.map_err(MobileKeyTransferError::LocalSetup)?;
return Ok(MobileKeyTransferOutcome {
title: outcome.title().to_owned(),
detail: outcome.detail().to_owned(),
});
}
let MobileKeyTransferDestination::Configured(config) = &self.destination else {
unreachable!("local setup returned above")
};
let existing = KeyStore::load(config.key_material())?;
if let Some(found) = existing
.infos()
.find(|info| info.fingerprint().as_str() == key.fingerprint)
&& (found.has_secret() || key.kind == MobileKeyTransferKind::Public)
&& !overwrite
{
return Err(MobileKeyTransferError::DuplicateKey);
return Err(MobileKeyTransferError::ExistingKey);
}
persist_key_material(self.config.key_material(), key, armor.expose())?;
persist_armored_key(
config.key_material(),
&key.fingerprint,
key.kind == MobileKeyTransferKind::Private,
armor.expose(),
overwrite,
)?;
if make_default {
let mut settings = self.config.settings();
let mut settings = config.settings();
settings.set_default_key(key.fingerprint.clone());
self.config.with_settings(settings)?.persist()?;
config.with_settings(settings)?.persist()?;
}
let kind = if key.kind == MobileKeyTransferKind::Private {
"private"
@@ -357,12 +400,13 @@ pub enum MobileKeyTransferError {
AlreadyComplete,
ChecksumMismatch,
MismatchedKeys,
DuplicateKey,
ExistingKey,
PublicDefault,
InvalidKeyMaterial,
IncorrectPassphrase,
QrPayload,
Write,
LocalSetup(crate::mobile_onboarding::MobileOnboardingError),
}
impl fmt::Display for MobileKeyTransferError {
@@ -381,12 +425,15 @@ impl fmt::Display for MobileKeyTransferError {
"the reconstructed key transfer did not pass its integrity check"
}
Self::MismatchedKeys => "the transfer contains multiple or mismatched GPG keys",
Self::DuplicateKey => "this GPG key is already installed",
Self::ExistingKey => {
"this GPG key is already installed; confirm replacement to continue"
}
Self::PublicDefault => "a public-only key cannot become the default GPG key",
Self::InvalidKeyMaterial => "the GPG key material is invalid",
Self::IncorrectPassphrase => "the GPG key passphrase is incorrect",
Self::QrPayload => "the GPG key could not be represented as QR codes",
Self::Write => "the GPG key could not be stored safely",
Self::LocalSetup(error) => return error.fmt(formatter),
})
}
}
@@ -499,7 +546,7 @@ fn parse_positive(value: Option<&str>) -> Result<usize, MobileKeyTransferError>
.ok_or(MobileKeyTransferError::InvalidFrame)
}
fn validate_transfer_armor(bytes: &[u8]) -> Result<(), MobileKeyTransferError> {
pub(crate) fn validate_transfer_armor(bytes: &[u8]) -> Result<(), MobileKeyTransferError> {
let text =
std::str::from_utf8(bytes).map_err(|_| MobileKeyTransferError::InvalidKeyMaterial)?;
let end = if text.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----") {
@@ -522,18 +569,19 @@ fn validate_transfer_armor(bytes: &[u8]) -> Result<(), MobileKeyTransferError> {
Ok(())
}
fn persist_key_material(
pub(crate) fn persist_armored_key(
configured: &Path,
key: &MobileKeyTransferKey,
fingerprint: &str,
secret: bool,
armor: &[u8],
overwrite: bool,
) -> Result<(), MobileKeyTransferError> {
if configured.is_dir() {
let suffix = if key.kind == MobileKeyTransferKind::Private {
"secret"
} else {
"public"
};
let name = format!("{}-{suffix}.asc", key.fingerprint.to_ascii_lowercase());
let suffix = if secret { "secret" } else { "public" };
let name = format!("{}-{suffix}.asc", fingerprint.to_ascii_lowercase());
if configured.join(&name).exists() && !overwrite {
return Err(MobileKeyTransferError::ExistingKey);
}
atomic_replace(configured, Path::new(&name), armor)
} else {
let existing = fs::read(configured).map_err(|_| MobileKeyTransferError::Write)?;
@@ -689,7 +737,7 @@ mod tests {
);
assert!(progress.received() < progress.total());
assert_eq!(
importer.import(None, false),
importer.import(None, false, false),
Err(MobileKeyTransferError::IncompleteTransfer)
);
assert_eq!(
@@ -706,13 +754,14 @@ mod tests {
let mut importer = MobileKeyTransferImport::new(fixture.config.clone());
importer.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
assert_eq!(
importer.import(Some(SecretBytes::new(b"wrong".to_vec())), true),
importer.import(Some(SecretBytes::new(b"wrong".to_vec())), true, false),
Err(MobileKeyTransferError::IncorrectPassphrase)
);
assert_eq!(std::fs::read_dir(&fixture.key_path)?.count(), 1);
importer.import(
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
true,
false,
)?;
let keys = KeyStore::load(&fixture.key_path)?;
assert!(keys.infos().any(|key| key.has_secret()));
@@ -722,6 +771,32 @@ mod tests {
.as_str(),
ALICE_FINGERPRINT
);
let before = std::fs::read(fixture.key_path.join(format!(
"{}-secret.asc",
ALICE_FINGERPRINT.to_ascii_lowercase()
)))?;
let mut replacement = MobileKeyTransferImport::new(fixture.config);
replacement.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
assert_eq!(
replacement.import(
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
true,
false,
),
Err(MobileKeyTransferError::ExistingKey)
);
assert_eq!(
std::fs::read(fixture.key_path.join(format!(
"{}-secret.asc",
ALICE_FINGERPRINT.to_ascii_lowercase()
)))?,
before
);
replacement.import(
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
true,
true,
)?;
Ok(())
}