Allow existing GPG keys with cloned stores
This commit is contained in:
@@ -1927,7 +1927,7 @@ private final class KeyImportScannerViewController: UIViewController,
|
|||||||
: "Import this public key?"
|
: "Import this public key?"
|
||||||
let alert = UIAlertController(
|
let alert = UIAlertController(
|
||||||
title: localSetup
|
title: localSetup
|
||||||
? "Create Local Store with This Key?"
|
? "Use This GPG Key?"
|
||||||
: (privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?"),
|
: (privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?"),
|
||||||
message: "\(warning)\n\n\(key.title)\n\(key.detail)",
|
message: "\(warning)\n\n\(key.title)\n\(key.detail)",
|
||||||
preferredStyle: .alert
|
preferredStyle: .alert
|
||||||
@@ -1952,6 +1952,15 @@ private final class KeyImportScannerViewController: UIViewController,
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if localSetup && privateKey {
|
||||||
|
alert.addAction(UIAlertAction(title: "Use with Existing Store", style: .default) {
|
||||||
|
[weak self, weak alert] _ in
|
||||||
|
self?.finishImport(
|
||||||
|
passphrase: alert?.textFields?.first?.text,
|
||||||
|
makeDefault: false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
if privateKey {
|
if privateKey {
|
||||||
alert.addAction(UIAlertAction(
|
alert.addAction(UIAlertAction(
|
||||||
title: localSetup ? "Create Local Store" : "Import as Default",
|
title: localSetup ? "Create Local Store" : "Import as Default",
|
||||||
@@ -6290,7 +6299,7 @@ private final class SetupChoiceViewController: UITableViewController {
|
|||||||
case (0, 1):
|
case (0, 1):
|
||||||
content.image = UIImage(systemName: "qrcode.viewfinder")
|
content.image = UIImage(systemName: "qrcode.viewfinder")
|
||||||
content.text = "Import Local GPG Key"
|
content.text = "Import Local GPG Key"
|
||||||
content.secondaryText = "Scan a private IronStorage key transfer"
|
content.secondaryText = "Use a private key with a new or existing store"
|
||||||
default:
|
default:
|
||||||
content.image = UIImage(systemName: "network")
|
content.image = UIImage(systemName: "network")
|
||||||
content.text = "Connect Existing Store"
|
content.text = "Connect Existing Store"
|
||||||
@@ -6796,15 +6805,11 @@ private final class OnboardingViewController: UITableViewController {
|
|||||||
case let .success(.completed(outcome)):
|
case let .success(.completed(outcome)):
|
||||||
tokenField.text = nil
|
tokenField.text = nil
|
||||||
let alert = UIAlertController(title: outcome.title, message: outcome.detail, preferredStyle: .alert)
|
let alert = UIAlertController(title: outcome.title, message: outcome.detail, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
|
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
|
||||||
if self?.attachLocalStore == true {
|
NotificationCenter.default.post(
|
||||||
NotificationCenter.default.post(
|
name: .ironStorageConfigurationDidChange,
|
||||||
name: .ironStorageConfigurationDidChange,
|
object: nil
|
||||||
object: nil
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self?.navigationController?.popViewController(animated: true)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
present(alert, animated: true)
|
present(alert, animated: true)
|
||||||
case let .failure(failure):
|
case let .failure(failure):
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ pub struct MobileKeyTransferService {
|
|||||||
impl MobileKeyTransferService {
|
impl MobileKeyTransferService {
|
||||||
pub fn load() -> Result<Self, MobileKeyTransferError> {
|
pub fn load() -> Result<Self, MobileKeyTransferError> {
|
||||||
let config = Config::load(None)?;
|
let config = Config::load(None)?;
|
||||||
let keys = KeyStore::load(config.key_material())?;
|
let keys = load_keys_or_empty(config.key_material())?;
|
||||||
Ok(Self { config, keys })
|
Ok(Self { config, keys })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,16 +316,24 @@ impl MobileKeyTransferImport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matches!(self.destination, MobileKeyTransferDestination::LocalSetup) {
|
if matches!(self.destination, MobileKeyTransferDestination::LocalSetup) {
|
||||||
if key.kind != MobileKeyTransferKind::Private || !make_default {
|
if key.kind != MobileKeyTransferKind::Private {
|
||||||
return Err(MobileKeyTransferError::PublicDefault);
|
return Err(MobileKeyTransferError::PublicDefault);
|
||||||
}
|
}
|
||||||
let outcome = crate::mobile_onboarding::MobileOnboardingOperation::default()
|
let operation = crate::mobile_onboarding::MobileOnboardingOperation::default();
|
||||||
.setup_local_imported(
|
let outcome = if make_default {
|
||||||
|
operation.setup_local_imported(
|
||||||
SecretBytes::new(armor.expose().to_vec()),
|
SecretBytes::new(armor.expose().to_vec()),
|
||||||
passphrase,
|
passphrase,
|
||||||
overwrite,
|
overwrite,
|
||||||
)
|
)
|
||||||
.map_err(MobileKeyTransferError::LocalSetup)?;
|
} else {
|
||||||
|
operation.stage_local_imported(
|
||||||
|
SecretBytes::new(armor.expose().to_vec()),
|
||||||
|
passphrase,
|
||||||
|
overwrite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map_err(MobileKeyTransferError::LocalSetup)?;
|
||||||
return Ok(MobileKeyTransferOutcome {
|
return Ok(MobileKeyTransferOutcome {
|
||||||
title: outcome.title().to_owned(),
|
title: outcome.title().to_owned(),
|
||||||
detail: outcome.detail().to_owned(),
|
detail: outcome.detail().to_owned(),
|
||||||
@@ -334,7 +342,7 @@ impl MobileKeyTransferImport {
|
|||||||
let MobileKeyTransferDestination::Configured(config) = &self.destination else {
|
let MobileKeyTransferDestination::Configured(config) = &self.destination else {
|
||||||
unreachable!("local setup returned above")
|
unreachable!("local setup returned above")
|
||||||
};
|
};
|
||||||
let existing = KeyStore::load(config.key_material())?;
|
let existing = load_keys_or_empty(config.key_material())?;
|
||||||
if let Some(found) = existing
|
if let Some(found) = existing
|
||||||
.infos()
|
.infos()
|
||||||
.find(|info| info.fingerprint().as_str() == key.fingerprint)
|
.find(|info| info.fingerprint().as_str() == key.fingerprint)
|
||||||
@@ -389,6 +397,13 @@ impl MobileKeyTransferImport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_keys_or_empty(path: &Path) -> Result<KeyStore, CryptoError> {
|
||||||
|
match KeyStore::load(path) {
|
||||||
|
Err(CryptoError::NoKeyMaterial) => Ok(KeyStore::new()),
|
||||||
|
result => result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum MobileKeyTransferError {
|
pub enum MobileKeyTransferError {
|
||||||
Configuration,
|
Configuration,
|
||||||
@@ -640,7 +655,7 @@ fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), MobileKeyTra
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
MobileKeyTransferError, MobileKeyTransferImport, MobileKeyTransferKind,
|
MobileKeyTransferError, MobileKeyTransferImport, MobileKeyTransferKind,
|
||||||
MobileKeyTransferService, encode_multipart,
|
MobileKeyTransferService, encode_multipart, load_keys_or_empty,
|
||||||
};
|
};
|
||||||
use crate::{config::Config, crypto::KeyStore, repository::SecretBytes};
|
use crate::{config::Config, crypto::KeyStore, repository::SecretBytes};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@@ -800,6 +815,36 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cloned_store_with_empty_key_directory_can_import_its_private_key()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixture = configured_key("placeholder", b"")?;
|
||||||
|
std::fs::remove_file(fixture.key_path.join("placeholder"))?;
|
||||||
|
let service = MobileKeyTransferService {
|
||||||
|
keys: load_keys_or_empty(&fixture.key_path)?,
|
||||||
|
config: fixture.config.clone(),
|
||||||
|
};
|
||||||
|
assert!(service.keys().is_empty());
|
||||||
|
|
||||||
|
let mut importer = service.importer();
|
||||||
|
importer.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
|
||||||
|
importer.import(
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let keys = KeyStore::load(&fixture.key_path)?;
|
||||||
|
assert!(keys.resolve(ALICE_FINGERPRINT).is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
Config::load(Some(fixture.config.source()))?
|
||||||
|
.default_key()
|
||||||
|
.as_str(),
|
||||||
|
ALICE_FINGERPRINT
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
struct Fixture {
|
struct Fixture {
|
||||||
_root: tempfile::TempDir,
|
_root: tempfile::TempDir,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ impl MobileOnboardingOperation {
|
|||||||
.map_err(MobileOnboardingError::from_git)?;
|
.map_err(MobileOnboardingError::from_git)?;
|
||||||
fs::create_dir_all(&paths.keys).map_err(|_| MobileOnboardingError::configuration())?;
|
fs::create_dir_all(&paths.keys).map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
set_private_directory(&paths.keys).map_err(|_| MobileOnboardingError::configuration())?;
|
set_private_directory(&paths.keys).map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
let default_key = default_key(&repository)?;
|
let default_key = default_key(&repository, &paths.keys)?;
|
||||||
store_application_token(request)?;
|
store_application_token(request)?;
|
||||||
Config::create_mobile_clone(
|
Config::create_mobile_clone(
|
||||||
paths.config,
|
paths.config,
|
||||||
@@ -262,21 +262,23 @@ impl MobileOnboardingOperation {
|
|||||||
passphrase: Option<SecretBytes>,
|
passphrase: Option<SecretBytes>,
|
||||||
replace_existing_key: bool,
|
replace_existing_key: bool,
|
||||||
) -> Result<MobileOnboardingOutcome, MobileOnboardingError> {
|
) -> Result<MobileOnboardingOutcome, MobileOnboardingError> {
|
||||||
validate_transfer_armor(armor.expose())
|
let fingerprint = validate_imported_local_key(&armor, passphrase.as_ref())?;
|
||||||
.map_err(|_| MobileOnboardingError::invalid_gpg_key())?;
|
self.setup_local_key(armor, passphrase, &fingerprint, replace_existing_key)
|
||||||
let mut keys = KeyStore::new();
|
}
|
||||||
let infos = keys
|
|
||||||
.import(armor.expose())
|
pub fn stage_local_imported(
|
||||||
.map_err(MobileOnboardingError::from_crypto)?;
|
&self,
|
||||||
let key = infos
|
armor: SecretBytes,
|
||||||
.first()
|
passphrase: Option<SecretBytes>,
|
||||||
.filter(|_| infos.len() == 1)
|
replace_existing_key: bool,
|
||||||
.ok_or_else(MobileOnboardingError::invalid_gpg_key)?;
|
) -> Result<MobileOnboardingOutcome, MobileOnboardingError> {
|
||||||
validate_local_key(&keys, key, passphrase.as_ref())?;
|
let fingerprint = validate_imported_local_key(&armor, passphrase.as_ref())?;
|
||||||
self.setup_local_key(
|
let paths = MobileOnboardingPaths::system()?;
|
||||||
|
self.stage_local_key_at(
|
||||||
|
paths,
|
||||||
armor,
|
armor,
|
||||||
passphrase,
|
passphrase.as_ref(),
|
||||||
key.fingerprint().as_str(),
|
&fingerprint,
|
||||||
replace_existing_key,
|
replace_existing_key,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -368,6 +370,50 @@ impl MobileOnboardingOperation {
|
|||||||
.to_owned(),
|
.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stage_local_key_at(
|
||||||
|
&self,
|
||||||
|
paths: MobileOnboardingPaths,
|
||||||
|
armor: SecretBytes,
|
||||||
|
passphrase: Option<&SecretBytes>,
|
||||||
|
fingerprint: &str,
|
||||||
|
replace_existing_key: bool,
|
||||||
|
) -> Result<MobileOnboardingOutcome, MobileOnboardingError> {
|
||||||
|
if Config::load(Some(&paths.config)).is_ok() || paths.config.exists() {
|
||||||
|
return Err(MobileOnboardingError::already_configured());
|
||||||
|
}
|
||||||
|
paths.prepare_root()?;
|
||||||
|
require_empty_directory(&paths.keys, replace_existing_key)?;
|
||||||
|
|
||||||
|
let staging = paths.staging_path();
|
||||||
|
fs::create_dir(&staging).map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
|
set_private_directory(&staging).map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
|
let result = (|| {
|
||||||
|
let staged_keys = staging.join("keys");
|
||||||
|
fs::create_dir(&staged_keys).map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
|
set_private_directory(&staged_keys)
|
||||||
|
.map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
|
persist_armored_key(&staged_keys, fingerprint, true, armor.expose(), false)
|
||||||
|
.map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
|
let keys = KeyStore::load(&staged_keys).map_err(MobileOnboardingError::from_crypto)?;
|
||||||
|
let key = keys
|
||||||
|
.infos()
|
||||||
|
.find(|key| key.fingerprint().as_str() == fingerprint)
|
||||||
|
.ok_or_else(MobileOnboardingError::invalid_gpg_key)?;
|
||||||
|
validate_local_key(&keys, &key, passphrase)?;
|
||||||
|
paths.install_staged_keys(&staging)
|
||||||
|
})();
|
||||||
|
if staging.exists() {
|
||||||
|
let _ = fs::remove_dir_all(&staging);
|
||||||
|
}
|
||||||
|
result?;
|
||||||
|
Ok(MobileOnboardingOutcome {
|
||||||
|
title: "GPG Key Ready".to_owned(),
|
||||||
|
detail:
|
||||||
|
"The private key is ready. Connect Existing Store to clone its password repository."
|
||||||
|
.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MobileOnboardingRequest {
|
pub struct MobileOnboardingRequest {
|
||||||
@@ -528,7 +574,7 @@ impl MobileOnboardingError {
|
|||||||
Self {
|
Self {
|
||||||
kind: MobileOnboardingErrorKind::ExistingKey,
|
kind: MobileOnboardingErrorKind::ExistingKey,
|
||||||
title: "GPG Key Already Exists",
|
title: "GPG Key Already Exists",
|
||||||
detail: "Creating this local store would replace existing local key material. Confirm replacement to continue."
|
detail: "This operation would replace existing local key material. Confirm replacement to continue."
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -738,6 +784,22 @@ impl MobileOnboardingPaths {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn install_staged_keys(&self, staging: &Path) -> Result<(), MobileOnboardingError> {
|
||||||
|
let previous = staging.join("previous-keys");
|
||||||
|
let had_keys = self.keys.exists();
|
||||||
|
if had_keys {
|
||||||
|
fs::rename(&self.keys, &previous)
|
||||||
|
.map_err(|_| MobileOnboardingError::configuration())?;
|
||||||
|
}
|
||||||
|
if fs::rename(staging.join("keys"), &self.keys).is_err() {
|
||||||
|
if had_keys {
|
||||||
|
let _ = fs::rename(previous, &self.keys);
|
||||||
|
}
|
||||||
|
return Err(MobileOnboardingError::configuration());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn restore_staged(&self, staging: &Path) -> std::io::Result<()> {
|
fn restore_staged(&self, staging: &Path) -> std::io::Result<()> {
|
||||||
let failed_vault = staging.join("failed-vault");
|
let failed_vault = staging.join("failed-vault");
|
||||||
let failed_keys = staging.join("failed-keys");
|
let failed_keys = staging.join("failed-keys");
|
||||||
@@ -800,6 +862,24 @@ fn validate_local_key(
|
|||||||
.map_err(|_| MobileOnboardingError::invalid_gpg_key())
|
.map_err(|_| MobileOnboardingError::invalid_gpg_key())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_imported_local_key(
|
||||||
|
armor: &SecretBytes,
|
||||||
|
passphrase: Option<&SecretBytes>,
|
||||||
|
) -> Result<String, MobileOnboardingError> {
|
||||||
|
validate_transfer_armor(armor.expose())
|
||||||
|
.map_err(|_| MobileOnboardingError::invalid_gpg_key())?;
|
||||||
|
let mut keys = KeyStore::new();
|
||||||
|
let infos = keys
|
||||||
|
.import(armor.expose())
|
||||||
|
.map_err(MobileOnboardingError::from_crypto)?;
|
||||||
|
let key = infos
|
||||||
|
.first()
|
||||||
|
.filter(|_| infos.len() == 1)
|
||||||
|
.ok_or_else(MobileOnboardingError::invalid_gpg_key)?;
|
||||||
|
validate_local_key(&keys, key, passphrase)?;
|
||||||
|
Ok(key.fingerprint().as_str().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
struct NoSetupSecrets;
|
struct NoSetupSecrets;
|
||||||
|
|
||||||
impl SecretProvider for NoSetupSecrets {
|
impl SecretProvider for NoSetupSecrets {
|
||||||
@@ -868,7 +948,10 @@ fn hex_prefix(bytes: &[u8], length: usize) -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_key(repository: &Repository) -> Result<String, MobileOnboardingError> {
|
fn default_key(
|
||||||
|
repository: &Repository,
|
||||||
|
key_material: &Path,
|
||||||
|
) -> Result<String, MobileOnboardingError> {
|
||||||
let root =
|
let root =
|
||||||
DirectoryPath::parse("").map_err(|_| MobileOnboardingError::inaccessible_repository())?;
|
DirectoryPath::parse("").map_err(|_| MobileOnboardingError::inaccessible_repository())?;
|
||||||
let contents = repository
|
let contents = repository
|
||||||
@@ -877,11 +960,27 @@ fn default_key(repository: &Repository) -> Result<String, MobileOnboardingError>
|
|||||||
.ok_or_else(MobileOnboardingError::inaccessible_repository)?;
|
.ok_or_else(MobileOnboardingError::inaccessible_repository)?;
|
||||||
let text = std::str::from_utf8(&contents)
|
let text = std::str::from_utf8(&contents)
|
||||||
.map_err(|_| MobileOnboardingError::inaccessible_repository())?;
|
.map_err(|_| MobileOnboardingError::inaccessible_repository())?;
|
||||||
text.lines()
|
let identities = text
|
||||||
|
.lines()
|
||||||
.map(|line| line.split('#').next().unwrap_or_default().trim())
|
.map(|line| line.split('#').next().unwrap_or_default().trim())
|
||||||
.find(|identity| !identity.is_empty())
|
.filter(|identity| {
|
||||||
.filter(|identity| identity.len() <= 512 && !identity.chars().any(char::is_control))
|
!identity.is_empty() && identity.len() <= 512 && !identity.chars().any(char::is_control)
|
||||||
.map(str::to_owned)
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if let Ok(keys) = KeyStore::load(key_material) {
|
||||||
|
for identity in &identities {
|
||||||
|
if let Ok(handle) = keys.resolve(identity)
|
||||||
|
&& keys
|
||||||
|
.infos()
|
||||||
|
.any(|key| key.fingerprint() == handle.fingerprint() && key.has_secret())
|
||||||
|
{
|
||||||
|
return Ok(handle.fingerprint().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
identities
|
||||||
|
.first()
|
||||||
|
.map(|identity| (*identity).to_owned())
|
||||||
.ok_or_else(MobileOnboardingError::inaccessible_repository)
|
.ok_or_else(MobileOnboardingError::inaccessible_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,7 +1056,7 @@ mod tests {
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
MobileOnboardingErrorKind, MobileOnboardingOperation, MobileOnboardingPaths,
|
MobileOnboardingErrorKind, MobileOnboardingOperation, MobileOnboardingPaths,
|
||||||
MobileOnboardingPhase, MobileOnboardingRequest,
|
MobileOnboardingPhase, MobileOnboardingRequest, default_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1058,6 +1157,69 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn imported_key_can_be_staged_before_cloning_without_creating_a_repository() {
|
||||||
|
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||||
|
let root = temporary.path().join("ironstorage");
|
||||||
|
let paths = MobileOnboardingPaths {
|
||||||
|
config: root.join("config.toml"),
|
||||||
|
vault: root.join("vault"),
|
||||||
|
keys: root.join("keys"),
|
||||||
|
root,
|
||||||
|
};
|
||||||
|
let passphrase = SecretBytes::new(b"existing key passphrase".to_vec());
|
||||||
|
let generated =
|
||||||
|
KeyStore::generate("Existing Store Key <owner@vault.example.com>", &passphrase)
|
||||||
|
.expect("generate key");
|
||||||
|
let fingerprint = generated.info().fingerprint().as_str().to_owned();
|
||||||
|
|
||||||
|
MobileOnboardingOperation::default()
|
||||||
|
.stage_local_key_at(
|
||||||
|
paths,
|
||||||
|
generated.into_armor(),
|
||||||
|
Some(&passphrase),
|
||||||
|
&fingerprint,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("stage imported key");
|
||||||
|
|
||||||
|
assert!(!temporary.path().join("ironstorage/config.toml").exists());
|
||||||
|
assert!(!temporary.path().join("ironstorage/vault").exists());
|
||||||
|
let keys =
|
||||||
|
KeyStore::load(temporary.path().join("ironstorage/keys")).expect("staged key material");
|
||||||
|
assert!(keys.resolve(&fingerprint).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_store_selects_the_staged_private_recipient_as_default() {
|
||||||
|
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||||
|
let vault = temporary.path().join("vault");
|
||||||
|
let keys_path = temporary.path().join("keys");
|
||||||
|
fs::create_dir(&vault).expect("vault");
|
||||||
|
fs::create_dir(&keys_path).expect("keys");
|
||||||
|
let passphrase = SecretBytes::new(b"existing key passphrase".to_vec());
|
||||||
|
let generated =
|
||||||
|
KeyStore::generate("Existing Store Key <owner@vault.example.com>", &passphrase)
|
||||||
|
.expect("generate key");
|
||||||
|
let fingerprint = generated.info().fingerprint().as_str().to_owned();
|
||||||
|
fs::write(
|
||||||
|
vault.join(".gpg-id"),
|
||||||
|
format!("UNAVAILABLE-RECIPIENT\n{fingerprint}\n"),
|
||||||
|
)
|
||||||
|
.expect("recipient policy");
|
||||||
|
fs::write(
|
||||||
|
keys_path.join("imported-secret.asc"),
|
||||||
|
generated.into_armor().expose(),
|
||||||
|
)
|
||||||
|
.expect("key material");
|
||||||
|
let repository = crate::repository::Repository::open(&vault).expect("repository");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
default_key(&repository, &keys_path).expect("default key"),
|
||||||
|
fingerprint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_setup_requires_confirmation_before_replacing_key_material() {
|
fn local_setup_requires_confirmation_before_replacing_key_material() {
|
||||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||||
|
|||||||
Reference in New Issue
Block a user