Implement complete Android settings and recovery controls (#104)
This commit is contained in:
1 parent
06575363a2
commit
43f660ace3
34 files changed
+2720
-64
No files matched your search
@@ -217,6 +217,15 @@ impl MobileKeyTransferImport {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_image(
|
||||
&mut self,
|
||||
image: SecretBytes,
|
||||
) -> Result<MobileKeyTransferProgress, MobileKeyTransferError> {
|
||||
let payload =
|
||||
QrMatrix::decode_image(&image).map_err(|_| MobileKeyTransferError::InvalidFrame)?;
|
||||
self.add_frame(payload)
|
||||
}
|
||||
|
||||
pub fn add_frame(
|
||||
&mut self,
|
||||
payload: SecretBytes,
|
||||
@@ -352,6 +361,32 @@ impl MobileKeyTransferImport {
|
||||
return Err(MobileKeyTransferError::ExistingKey);
|
||||
}
|
||||
|
||||
let current = Config::load(Some(config.source()))?;
|
||||
let candidate = if make_default {
|
||||
let mut settings = current.settings();
|
||||
settings.set_default_key(key.fingerprint.clone());
|
||||
Some(current.with_settings(settings)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let target = if config.key_material().is_dir() {
|
||||
let suffix = if key.kind == MobileKeyTransferKind::Private {
|
||||
"secret"
|
||||
} else {
|
||||
"public"
|
||||
};
|
||||
config.key_material().join(format!(
|
||||
"{}-{suffix}.asc",
|
||||
key.fingerprint.to_ascii_lowercase()
|
||||
))
|
||||
} else {
|
||||
config.key_material().to_owned()
|
||||
};
|
||||
let backup = match fs::read(&target) {
|
||||
Ok(value) => Some(SecretBytes::new(value)),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(_) => return Err(MobileKeyTransferError::Write),
|
||||
};
|
||||
persist_armored_key(
|
||||
config.key_material(),
|
||||
&key.fingerprint,
|
||||
@@ -359,10 +394,21 @@ impl MobileKeyTransferImport {
|
||||
armor.expose(),
|
||||
overwrite,
|
||||
)?;
|
||||
if make_default {
|
||||
let mut settings = config.settings();
|
||||
settings.set_default_key(key.fingerprint.clone());
|
||||
config.with_settings(settings)?.persist()?;
|
||||
if let Some(candidate) = candidate
|
||||
&& let Err(error) = candidate.persist()
|
||||
{
|
||||
if !matches!(error, ConfigError::DurabilityUncertain { .. }) {
|
||||
let parent = target.parent().ok_or(MobileKeyTransferError::Write)?;
|
||||
let name = Path::new(target.file_name().ok_or(MobileKeyTransferError::Write)?);
|
||||
let restored = match backup {
|
||||
Some(ref value) => atomic_replace(parent, name, value.expose()),
|
||||
None => fs::remove_file(&target)
|
||||
.and_then(|()| fs::File::open(parent)?.sync_all())
|
||||
.map_err(|_| MobileKeyTransferError::Write),
|
||||
};
|
||||
restored.map_err(|_| MobileKeyTransferError::RecoveryRequired)?;
|
||||
}
|
||||
return Err(error.into());
|
||||
}
|
||||
let kind = if key.kind == MobileKeyTransferKind::Private {
|
||||
"private"
|
||||
@@ -421,12 +467,14 @@ pub enum MobileKeyTransferError {
|
||||
IncorrectPassphrase,
|
||||
QrPayload,
|
||||
Write,
|
||||
RecoveryRequired,
|
||||
LocalSetup(crate::mobile_onboarding::MobileOnboardingError),
|
||||
}
|
||||
|
||||
impl fmt::Display for MobileKeyTransferError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(match self {
|
||||
Self::RecoveryRequired => "key import could not be rolled back; retain the key material and recover configuration before retrying",
|
||||
Self::Configuration => "the IronStorage configuration is unavailable",
|
||||
Self::KeyUnavailable => "the selected GPG key is unavailable",
|
||||
Self::InvalidFrame => "this is not a valid IronStorage key-transfer QR code",
|
||||
@@ -666,6 +714,35 @@ mod tests {
|
||||
include_bytes!("../tests/fixtures/compatibility/keys/alice-secret.asc");
|
||||
const ALICE_FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
|
||||
|
||||
#[test]
|
||||
fn qr_images_reconstruct_the_same_key_and_reject_invalid_images()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let fixture = configured_public_key()?;
|
||||
let mut importer = MobileKeyTransferImport::new(fixture.config);
|
||||
assert_eq!(
|
||||
importer.add_image(SecretBytes::new(b"not an image".to_vec())),
|
||||
Err(MobileKeyTransferError::InvalidFrame)
|
||||
);
|
||||
for frame in encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)? {
|
||||
let matrix = frame.matrix();
|
||||
let size = u32::try_from(matrix.width() + 8)? * 4;
|
||||
let image = image::GrayImage::from_fn(size, size, |x, y| {
|
||||
let x = (x / 4) as usize;
|
||||
let y = (y / 4) as usize;
|
||||
let dark = x >= 4 && y >= 4 && matrix.is_dark(x - 4, y - 4).unwrap_or(false);
|
||||
image::Luma([if dark { 0 } else { 255 }])
|
||||
});
|
||||
let mut png = std::io::Cursor::new(Vec::new());
|
||||
image.write_to(&mut png, image::ImageFormat::Png)?;
|
||||
importer.add_image(SecretBytes::new(png.into_inner()))?;
|
||||
}
|
||||
assert_eq!(
|
||||
importer.complete.as_ref().map(SecretBytes::expose),
|
||||
Some(ALICE_SECRET)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multipart_frames_reconstruct_exact_armor_out_of_order()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -773,11 +850,23 @@ mod tests {
|
||||
Err(MobileKeyTransferError::IncorrectPassphrase)
|
||||
);
|
||||
assert_eq!(std::fs::read_dir(&fixture.key_path)?.count(), 1);
|
||||
fixture
|
||||
.config
|
||||
.update_git_identity(&crate::git::GitIdentity::new(
|
||||
"Changed since QR opened",
|
||||
"changed@example.test",
|
||||
)?)?;
|
||||
importer.import(
|
||||
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||
true,
|
||||
false,
|
||||
)?;
|
||||
assert_eq!(
|
||||
Config::load(Some(fixture.config.source()))?
|
||||
.git_identity()
|
||||
.name(),
|
||||
"Changed since QR opened"
|
||||
);
|
||||
let keys = KeyStore::load(&fixture.key_path)?;
|
||||
assert!(keys.infos().any(|key| key.has_secret()));
|
||||
assert_eq!(
|
||||
@@ -815,6 +904,49 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn default_key_import_rolls_back_when_config_cannot_be_replaced()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let fixture = configured_public_key()?;
|
||||
let mut importer = MobileKeyTransferImport::new(fixture.config.clone());
|
||||
importer.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
|
||||
let before = std::fs::read(fixture.config.source())?;
|
||||
let parent = fixture.config.source().parent().unwrap();
|
||||
let permissions = std::fs::metadata(parent)?.permissions();
|
||||
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o500))?;
|
||||
let result = importer.import(
|
||||
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
std::fs::set_permissions(parent, permissions)?;
|
||||
assert_eq!(result, Err(MobileKeyTransferError::Configuration));
|
||||
assert_eq!(std::fs::read(fixture.config.source())?, before);
|
||||
assert_eq!(std::fs::read_dir(fixture.key_path)?.count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_configuration_does_not_install_an_imported_key()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let fixture = configured_public_key()?;
|
||||
let mut importer = MobileKeyTransferImport::new(fixture.config.clone());
|
||||
importer.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
|
||||
std::fs::write(fixture.config.source(), b"not a TOML document [")?;
|
||||
assert_eq!(
|
||||
importer.import(
|
||||
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||
true,
|
||||
false
|
||||
),
|
||||
Err(MobileKeyTransferError::Configuration)
|
||||
);
|
||||
assert_eq!(std::fs::read_dir(fixture.key_path)?.count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_store_with_empty_key_directory_can_import_its_private_key()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Reference in new issue
Block a user