keyfork-shard: refactor key discovery mechanisms
This commit is contained in:
parent
9f9ad54445
commit
41fc804764
|
@ -7,7 +7,7 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::{Format, openpgp::OpenPGP};
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ fn run() -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let openpgp = OpenPGP;
|
||||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?;
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
|
||||||
print!("{}", smex::encode(&bytes));
|
print!("{}", smex::encode(&bytes));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -33,7 +33,7 @@ fn run() -> Result<()> {
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let openpgp = OpenPGP;
|
||||||
|
|
||||||
openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?;
|
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ fn run() -> Result<()> {
|
||||||
|
|
||||||
let openpgp = OpenPGP;
|
let openpgp = OpenPGP;
|
||||||
|
|
||||||
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery, std::io::stdout())?;
|
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::{
|
use std::io::{stdin, stdout, Read, Write};
|
||||||
io::{stdin, stdout, Read, Write},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{consts::U12, Aead, AeadCore, OsRng},
|
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||||
|
@ -25,6 +22,27 @@ const ENC_LEN: u8 = 4 * 16;
|
||||||
#[cfg(feature = "openpgp")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod openpgp;
|
pub mod openpgp;
|
||||||
|
|
||||||
|
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
|
||||||
|
/// the filesystem that keys may be read from.
|
||||||
|
pub trait KeyDiscovery<F: Format + ?Sized> {
|
||||||
|
/// Discover public keys for the associated format.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if public keys could not be loaded from the given discovery
|
||||||
|
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
|
||||||
|
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
|
||||||
|
|
||||||
|
/// Discover private keys for the associated format.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if private keys could not be loaded from the given
|
||||||
|
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
|
||||||
|
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
|
||||||
|
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
|
||||||
|
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
|
||||||
|
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
/// A format to use for splitting and combining secrets.
|
/// A format to use for splitting and combining secrets.
|
||||||
pub trait Format {
|
pub trait Format {
|
||||||
/// The error type returned from any failed operations.
|
/// The error type returned from any failed operations.
|
||||||
|
@ -42,17 +60,6 @@ pub trait Format {
|
||||||
/// A type representing the parsed, but encrypted, Shard data.
|
/// A type representing the parsed, but encrypted, Shard data.
|
||||||
type EncryptedData;
|
type EncryptedData;
|
||||||
|
|
||||||
/// Parse the public key data from a readable type.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The method may return an error if private key data could not be properly parsed from the
|
|
||||||
/// path.
|
|
||||||
/// occurred while parsing the public key data.
|
|
||||||
fn parse_public_key_data(
|
|
||||||
&self,
|
|
||||||
key_data_path: impl AsRef<Path>,
|
|
||||||
) -> Result<Vec<Self::PublicKey>, Self::Error>;
|
|
||||||
|
|
||||||
/// Derive a signer
|
/// Derive a signer
|
||||||
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
||||||
|
|
||||||
|
@ -83,17 +90,6 @@ pub trait Format {
|
||||||
signing_key: &mut Self::SigningKey,
|
signing_key: &mut Self::SigningKey,
|
||||||
) -> Result<Self::EncryptedData, Self::Error>;
|
) -> Result<Self::EncryptedData, Self::Error>;
|
||||||
|
|
||||||
/// Parse the private key data from a readable type. The private key may not be accessible (it
|
|
||||||
/// may be hardware only, such as a smartcard), for which this method may return None.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The method may return an error if private key data could not be properly parsed from the
|
|
||||||
/// path.
|
|
||||||
fn parse_private_key_data(
|
|
||||||
&self,
|
|
||||||
key_data_path: impl AsRef<Path>,
|
|
||||||
) -> Result<Self::PrivateKeyData, Self::Error>;
|
|
||||||
|
|
||||||
/// Parse the Shard file into a processable type.
|
/// Parse the Shard file into a processable type.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
@ -148,11 +144,11 @@ pub trait Format {
|
||||||
/// be combined into a secret.
|
/// be combined into a secret.
|
||||||
fn decrypt_all_shards_to_secret(
|
fn decrypt_all_shards_to_secret(
|
||||||
&self,
|
&self,
|
||||||
private_key_data_path: Option<impl AsRef<Path>>,
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
reader: impl Read + Send + Sync,
|
reader: impl Read + Send + Sync,
|
||||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
let private_keys = private_key_data_path
|
let private_keys = private_key_discovery
|
||||||
.map(|p| self.parse_private_key_data(p))
|
.map(|p| p.discover_private_keys())
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?;
|
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?;
|
||||||
|
@ -173,15 +169,15 @@ pub trait Format {
|
||||||
/// QR code; instead, a mnemonic prompt will be used.
|
/// QR code; instead, a mnemonic prompt will be used.
|
||||||
fn decrypt_one_shard_for_transport(
|
fn decrypt_one_shard_for_transport(
|
||||||
&self,
|
&self,
|
||||||
private_key_data_path: Option<impl AsRef<Path>>,
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
reader: impl Read + Send + Sync,
|
reader: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut pm = Terminal::new(stdin(), stdout())?;
|
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||||
let wordlist = Wordlist::default();
|
let wordlist = Wordlist::default();
|
||||||
|
|
||||||
// parse input
|
// parse input
|
||||||
let private_keys = private_key_data_path
|
let private_keys = private_key_discovery
|
||||||
.map(|p| self.parse_private_key_data(p))
|
.map(|p| p.discover_private_keys())
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let encrypted_messages = self.parse_shard_file(reader)?;
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
|
|
||||||
|
@ -321,7 +317,7 @@ pub trait Format {
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
public_key_data_path: impl AsRef<Path>,
|
public_key_discovery: impl KeyDiscovery<Self>,
|
||||||
writer: impl Write + Send + Sync,
|
writer: impl Write + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut signing_key = self.derive_signing_key(secret);
|
let mut signing_key = self.derive_signing_key(secret);
|
||||||
|
@ -329,7 +325,7 @@ pub trait Format {
|
||||||
let sharks = Sharks(threshold);
|
let sharks = Sharks(threshold);
|
||||||
let dealer = sharks.dealer(secret);
|
let dealer = sharks.dealer(secret);
|
||||||
|
|
||||||
let public_keys = self.parse_public_key_data(public_key_data_path)?;
|
let public_keys = public_key_discovery.discover_public_keys()?;
|
||||||
assert!(
|
assert!(
|
||||||
public_keys.len() < u8::MAX as usize,
|
public_keys.len() < u8::MAX as usize,
|
||||||
"must have less than u8::MAX public keys"
|
"must have less than u8::MAX public keys"
|
||||||
|
|
|
@ -58,7 +58,7 @@ const SHARD_METADATA_OFFSET: usize = 2;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR,
|
Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR,
|
||||||
QRCODE_PROMPT, QRCODE_TIMEOUT,
|
QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery
|
||||||
};
|
};
|
||||||
|
|
||||||
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
||||||
|
@ -282,13 +282,6 @@ impl Format for OpenPGP {
|
||||||
type SigningKey = Cert;
|
type SigningKey = Cert;
|
||||||
type EncryptedData = EncryptedMessage;
|
type EncryptedData = EncryptedMessage;
|
||||||
|
|
||||||
fn parse_public_key_data(
|
|
||||||
&self,
|
|
||||||
key_data_path: impl AsRef<Path>,
|
|
||||||
) -> std::result::Result<Vec<Self::PublicKey>, Self::Error> {
|
|
||||||
Self::discover_certs(key_data_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive an OpenPGP Shard certificate from the given seed.
|
/// Derive an OpenPGP Shard certificate from the given seed.
|
||||||
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
|
||||||
let seed = VariableLengthSeed::new(seed);
|
let seed = VariableLengthSeed::new(seed);
|
||||||
|
@ -449,13 +442,6 @@ impl Format for OpenPGP {
|
||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_private_key_data(
|
|
||||||
&self,
|
|
||||||
key_data_path: impl AsRef<Path>,
|
|
||||||
) -> std::result::Result<Self::PrivateKeyData, Self::Error> {
|
|
||||||
Self::discover_certs(key_data_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_shard_file(
|
fn parse_shard_file(
|
||||||
&self,
|
&self,
|
||||||
shard_file: impl Read + Send + Sync,
|
shard_file: impl Read + Send + Sync,
|
||||||
|
@ -579,6 +565,26 @@ impl Format for OpenPGP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeyDiscovery<OpenPGP> for &Path {
|
||||||
|
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
||||||
|
OpenPGP::discover_certs(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyDiscovery<OpenPGP> for &[Cert] {
|
||||||
|
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
||||||
|
Ok(self.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
||||||
|
Ok(self.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
||||||
/// from a file, or from files one level deep in a directory.
|
/// from a file, or from files one level deep in a directory.
|
||||||
///
|
///
|
||||||
|
|
|
@ -111,12 +111,10 @@ impl DecryptionHelper for &mut Keyring {
|
||||||
pkesk.recipient().is_wildcard()
|
pkesk.recipient().is_wildcard()
|
||||||
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
||||||
}) {
|
}) {
|
||||||
#[allow(deprecated, clippy::map_flatten)]
|
|
||||||
let name = cert
|
let name = cert
|
||||||
.userids()
|
.userids()
|
||||||
.next()
|
.next()
|
||||||
.map(|userid| userid.userid().name().transpose())
|
.and_then(|userid| userid.userid().name2().transpose())
|
||||||
.flatten()
|
|
||||||
.transpose()
|
.transpose()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
|
@ -37,7 +37,7 @@ impl RecoverSubcommands {
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
// TODO: remove .clone() by making handle() consume self
|
// TODO: remove .clone() by making handle() consume self
|
||||||
let seed = openpgp
|
let seed = openpgp
|
||||||
.decrypt_all_shards_to_secret(key_discovery.clone(), content.as_bytes())?;
|
.decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?;
|
||||||
Ok(seed)
|
Ok(seed)
|
||||||
} else {
|
} else {
|
||||||
panic!("unknown format of shard file");
|
panic!("unknown format of shard file");
|
||||||
|
|
|
@ -32,27 +32,23 @@ trait ShardExec {
|
||||||
&self,
|
&self,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
key_discovery: impl AsRef<Path>,
|
key_discovery: &Path,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
output: &mut (impl Write + Send + Sync),
|
output: &mut (impl Write + Send + Sync),
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
fn combine<T>(
|
fn combine(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
where
|
|
||||||
T: AsRef<Path>;
|
|
||||||
|
|
||||||
fn decrypt<T>(
|
fn decrypt(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
where
|
|
||||||
T: AsRef<Path>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -63,7 +59,7 @@ impl ShardExec for OpenPGP {
|
||||||
&self,
|
&self,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
key_discovery: impl AsRef<Path>,
|
key_discovery: &Path,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
output: &mut (impl Write + Send + Sync),
|
output: &mut (impl Write + Send + Sync),
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
@ -71,14 +67,12 @@ impl ShardExec for OpenPGP {
|
||||||
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn combine<T>(
|
fn combine(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
where
|
|
||||||
T: AsRef<Path>,
|
|
||||||
{
|
{
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
|
||||||
|
@ -87,13 +81,11 @@ impl ShardExec for OpenPGP {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt<T>(
|
fn decrypt(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
where
|
|
||||||
T: AsRef<Path>,
|
|
||||||
{
|
{
|
||||||
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
|
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
|
||||||
|
@ -189,7 +181,7 @@ impl ShardSubcommands {
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
Some(Format::OpenPGP(o)) => {
|
Some(Format::OpenPGP(o)) => {
|
||||||
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes())
|
o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
|
||||||
}
|
}
|
||||||
Some(Format::P256(_p)) => todo!(),
|
Some(Format::P256(_p)) => todo!(),
|
||||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||||
|
@ -206,7 +198,7 @@ impl ShardSubcommands {
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
Some(Format::OpenPGP(o)) => o.combine(
|
Some(Format::OpenPGP(o)) => o.combine(
|
||||||
key_discovery.as_ref(),
|
key_discovery.as_deref(),
|
||||||
shard_content.as_bytes(),
|
shard_content.as_bytes(),
|
||||||
&mut stdout,
|
&mut stdout,
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,6 +15,8 @@ use keyfork_prompt::{
|
||||||
Message, PromptHandler, Terminal,
|
Message, PromptHandler, Terminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
#[error("Invalid PIN length: {0}")]
|
#[error("Invalid PIN length: {0}")]
|
||||||
pub struct PinLength(usize);
|
pub struct PinLength(usize);
|
||||||
|
@ -163,13 +165,13 @@ fn generate_shard_secret(
|
||||||
certs.push(cert);
|
certs.push(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let opgp = OpenPGP;
|
||||||
|
|
||||||
if let Some(output_file) = output_file {
|
if let Some(output_file) = output_file {
|
||||||
let output = File::create(output_file)?;
|
let output = File::create(output_file)?;
|
||||||
#[allow(deprecated)]
|
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
|
||||||
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
|
|
||||||
} else {
|
} else {
|
||||||
#[allow(deprecated)]
|
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
|
||||||
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue