2024-01-20 06:17:32 +00:00
|
|
|
#![doc = include_str!("../README.md")]
|
2024-02-21 01:39:28 +00:00
|
|
|
#![allow(clippy::expect_fun_call)]
|
2024-01-16 02:44:48 +00:00
|
|
|
|
2024-02-20 23:33:54 +00:00
|
|
|
use std::{
|
|
|
|
io::{stdin, stdout, Read, Write},
|
|
|
|
sync::{Arc, Mutex},
|
|
|
|
};
|
2024-01-05 04:05:30 +00:00
|
|
|
|
|
|
|
use aes_gcm::{
|
2024-04-09 23:46:37 +00:00
|
|
|
aead::{consts::U12, Aead},
|
2024-01-20 06:20:04 +00:00
|
|
|
Aes256Gcm, KeyInit, Nonce,
|
2024-01-05 04:05:30 +00:00
|
|
|
};
|
2024-04-15 03:15:35 +00:00
|
|
|
use base64::prelude::{Engine, BASE64_STANDARD};
|
2024-01-08 19:00:31 +00:00
|
|
|
use hkdf::Hkdf;
|
2024-02-21 01:39:28 +00:00
|
|
|
use keyfork_bug::{bug, POISONED_MUTEX};
|
2024-08-01 13:50:30 +00:00
|
|
|
use keyfork_mnemonic::{English, Mnemonic};
|
2024-01-10 20:34:29 +00:00
|
|
|
use keyfork_prompt::{
|
2024-04-09 23:46:37 +00:00
|
|
|
validators::{
|
|
|
|
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
|
|
|
Validator,
|
|
|
|
},
|
2024-01-12 00:49:56 +00:00
|
|
|
Message as PromptMessage, PromptHandler, Terminal,
|
2024-01-10 20:34:29 +00:00
|
|
|
};
|
|
|
|
use sha2::Sha256;
|
2024-11-21 22:24:06 +00:00
|
|
|
use blahaj::{Share, Sharks};
|
2024-01-05 04:05:30 +00:00
|
|
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
|
|
|
|
2024-05-04 02:41:38 +00:00
|
|
|
const PLAINTEXT_LENGTH: u8 = 32 // shard
|
|
|
|
+ 1 // index
|
|
|
|
+ 1 // threshold
|
|
|
|
+ 1 // version
|
|
|
|
+ 1; // length;
|
2024-04-15 00:27:00 +00:00
|
|
|
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
2024-01-20 06:20:04 +00:00
|
|
|
|
2023-10-19 22:06:34 +00:00
|
|
|
#[cfg(feature = "openpgp")]
|
|
|
|
pub mod openpgp;
|
2024-01-05 04:05:30 +00:00
|
|
|
|
2024-02-19 01:19:29 +00:00
|
|
|
/// 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>;
|
|
|
|
}
|
|
|
|
|
2024-01-20 06:20:04 +00:00
|
|
|
/// A format to use for splitting and combining secrets.
|
|
|
|
pub trait Format {
|
|
|
|
/// The error type returned from any failed operations.
|
|
|
|
type Error: std::error::Error + 'static;
|
|
|
|
|
2024-02-12 16:51:49 +00:00
|
|
|
/// A type encapsulating a single public key recipient.
|
|
|
|
type PublicKey;
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
/// A type encapsulating the private key recipients of shards.
|
|
|
|
type PrivateKeyData;
|
|
|
|
|
2024-02-12 16:51:49 +00:00
|
|
|
/// A type representing a Signer derived from the secret.
|
|
|
|
type SigningKey;
|
|
|
|
|
2024-01-20 06:20:04 +00:00
|
|
|
/// A type representing the parsed, but encrypted, Shard data.
|
2024-02-15 08:01:23 +00:00
|
|
|
type EncryptedData;
|
2024-01-20 06:20:04 +00:00
|
|
|
|
2024-02-12 16:51:49 +00:00
|
|
|
/// Derive a signer
|
|
|
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
|
|
|
|
|
|
|
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
|
|
|
|
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
|
|
|
|
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
|
|
|
|
/// will use the same key_data for both, ensuring an iteration of this method will match with
|
|
|
|
/// iterations in methods called later.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if encryption to any of the public keys fails.
|
|
|
|
fn format_encrypted_header(
|
|
|
|
&self,
|
|
|
|
signing_key: &Self::SigningKey,
|
2024-02-15 08:01:23 +00:00
|
|
|
key_data: &[Self::PublicKey],
|
2024-02-12 16:51:49 +00:00
|
|
|
threshold: u8,
|
2024-02-15 08:01:23 +00:00
|
|
|
) -> Result<Self::EncryptedData, Self::Error>;
|
2024-02-12 16:51:49 +00:00
|
|
|
|
|
|
|
/// Format a shard encrypted to the given public key, signing with the private key.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the public key used to encrypt the shard is unsuitable
|
|
|
|
/// for encryption, or if an error occurs while encrypting.
|
|
|
|
fn encrypt_shard(
|
|
|
|
&self,
|
|
|
|
shard: &[u8],
|
|
|
|
public_key: &Self::PublicKey,
|
|
|
|
signing_key: &mut Self::SigningKey,
|
2024-02-15 08:01:23 +00:00
|
|
|
) -> Result<Self::EncryptedData, Self::Error>;
|
2024-02-12 16:51:49 +00:00
|
|
|
|
2024-01-20 06:20:04 +00:00
|
|
|
/// Parse the Shard file into a processable type.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the Shard file could not be read from or if the Shard
|
|
|
|
/// file could not be properly parsed.
|
|
|
|
fn parse_shard_file(
|
|
|
|
&self,
|
|
|
|
shard_file: impl Read + Send + Sync,
|
2024-02-15 08:01:23 +00:00
|
|
|
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
/// Write the Shard data to a Shard file.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the Shard data could not be properly serialized or if the
|
|
|
|
/// Shard file could not be written to.
|
|
|
|
fn format_shard_file(
|
|
|
|
&self,
|
2024-02-15 08:01:23 +00:00
|
|
|
encrypted_data: &[Self::EncryptedData],
|
|
|
|
shard_file: impl Write + Send + Sync,
|
2024-01-20 06:20:04 +00:00
|
|
|
) -> Result<(), Self::Error>;
|
|
|
|
|
|
|
|
/// Decrypt shares and associated metadata from a readable input. For the current version of
|
|
|
|
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
|
|
|
|
/// secrets.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the shardfile couldn't be read from, if all shards
|
|
|
|
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
|
|
|
|
fn decrypt_all_shards(
|
|
|
|
&self,
|
|
|
|
private_keys: Option<Self::PrivateKeyData>,
|
2024-02-15 08:01:23 +00:00
|
|
|
encrypted_messages: &[Self::EncryptedData],
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt: Arc<Mutex<impl PromptHandler>>,
|
2024-01-20 06:20:04 +00:00
|
|
|
) -> Result<(Vec<Share>, u8), Self::Error>;
|
|
|
|
|
|
|
|
/// Decrypt a single share and associated metadata from a reaable input. For the current
|
|
|
|
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
|
|
|
/// combine secrets.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
|
|
|
|
/// be decrypted, or if a shard could not be parsed from the decrypted data.
|
|
|
|
fn decrypt_one_shard(
|
|
|
|
&self,
|
|
|
|
private_keys: Option<Self::PrivateKeyData>,
|
2024-02-15 08:01:23 +00:00
|
|
|
encrypted_data: &[Self::EncryptedData],
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt: Arc<Mutex<impl PromptHandler>>,
|
2024-01-20 06:20:04 +00:00
|
|
|
) -> Result<(Share, u8), Self::Error>;
|
|
|
|
|
|
|
|
/// Decrypt multiple shares and combine them to recreate a secret.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the shares can't be decrypted or if the shares can't
|
|
|
|
/// be combined into a secret.
|
|
|
|
fn decrypt_all_shards_to_secret(
|
|
|
|
&self,
|
2024-02-19 01:19:29 +00:00
|
|
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
2024-01-20 06:20:04 +00:00
|
|
|
reader: impl Read + Send + Sync,
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt: impl PromptHandler,
|
2024-01-20 06:20:04 +00:00
|
|
|
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
2024-02-19 01:19:29 +00:00
|
|
|
let private_keys = private_key_discovery
|
|
|
|
.map(|p| p.discover_private_keys())
|
2024-01-20 06:20:04 +00:00
|
|
|
.transpose()?;
|
2024-02-15 08:01:23 +00:00
|
|
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
2024-02-20 23:33:54 +00:00
|
|
|
let (shares, threshold) = self.decrypt_all_shards(
|
|
|
|
private_keys,
|
|
|
|
&encrypted_messages,
|
|
|
|
Arc::new(Mutex::new(prompt)),
|
|
|
|
)?;
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
let secret = Sharks(threshold)
|
|
|
|
.recover(&shares)
|
|
|
|
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
|
|
|
|
|
|
|
|
Ok(secret)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
|
|
|
|
/// shard to the AES key.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if a share can't be decrypted. The method will not return an
|
|
|
|
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
|
|
|
|
/// QR code; instead, a mnemonic prompt will be used.
|
|
|
|
fn decrypt_one_shard_for_transport(
|
|
|
|
&self,
|
2024-02-19 01:19:29 +00:00
|
|
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
2024-01-20 06:20:04 +00:00
|
|
|
reader: impl Read + Send + Sync,
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt: impl PromptHandler,
|
2024-01-20 06:20:04 +00:00
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
2024-02-20 23:33:54 +00:00
|
|
|
let prompt = Arc::new(Mutex::new(prompt));
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
// parse input
|
2024-02-19 01:19:29 +00:00
|
|
|
let private_keys = private_key_discovery
|
|
|
|
.map(|p| p.discover_private_keys())
|
2024-01-20 06:20:04 +00:00
|
|
|
.transpose()?;
|
2024-02-15 08:01:23 +00:00
|
|
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
// establish AES-256-GCM key via ECDH
|
|
|
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
|
|
|
|
|
|
|
// receive remote data via scanning QR code from camera
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
{
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt
|
|
|
|
.lock()
|
2024-02-21 01:39:28 +00:00
|
|
|
.expect(bug!(POISONED_MUTEX))
|
2024-02-20 23:33:54 +00:00
|
|
|
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
2024-04-15 01:19:06 +00:00
|
|
|
if let Ok(Some(qrcode_content)) =
|
2024-01-20 06:20:04 +00:00
|
|
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
|
|
|
{
|
2024-04-15 03:15:35 +00:00
|
|
|
let decoded_data = BASE64_STANDARD
|
|
|
|
.decode(qrcode_content)
|
|
|
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
2024-04-09 23:46:37 +00:00
|
|
|
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
2024-01-20 06:20:04 +00:00
|
|
|
} else {
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt
|
|
|
|
.lock()
|
2024-02-21 01:39:28 +00:00
|
|
|
.expect(bug!(POISONED_MUTEX))
|
2024-02-20 23:33:54 +00:00
|
|
|
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
2024-01-20 06:20:04 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
2024-04-09 23:46:37 +00:00
|
|
|
let their_pubkey = match pubkey_data {
|
|
|
|
Some(pubkey) => pubkey,
|
|
|
|
None => {
|
|
|
|
let validator = MnemonicValidator {
|
|
|
|
word_length: Some(WordLength::Count(24)),
|
2024-01-20 06:20:04 +00:00
|
|
|
};
|
2024-04-09 23:46:37 +00:00
|
|
|
prompt
|
2024-02-20 23:33:54 +00:00
|
|
|
.lock()
|
2024-02-21 01:39:28 +00:00
|
|
|
.expect(bug!(POISONED_MUTEX))
|
2024-02-19 10:41:37 +00:00
|
|
|
.prompt_validated_wordlist::<English, _>(
|
|
|
|
QRCODE_COULDNT_READ,
|
|
|
|
3,
|
|
|
|
validator.to_fn(),
|
2024-04-09 23:46:37 +00:00
|
|
|
)?
|
2024-01-20 06:20:04 +00:00
|
|
|
.as_bytes()
|
|
|
|
.try_into()
|
2024-04-09 23:46:37 +00:00
|
|
|
.map_err(|_| InvalidData)?
|
2024-01-20 06:20:04 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// create our shared key
|
|
|
|
let our_key = EphemeralSecret::random();
|
2024-08-01 13:29:03 +00:00
|
|
|
let our_pubkey_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
|
2024-04-17 19:36:42 +00:00
|
|
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(their_pubkey));
|
2024-04-19 03:39:29 +00:00
|
|
|
assert!(
|
|
|
|
shared_secret.was_contributory(),
|
|
|
|
bug!("shared secret might be insecure")
|
|
|
|
);
|
2024-04-17 19:36:42 +00:00
|
|
|
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
2024-04-09 23:46:37 +00:00
|
|
|
|
|
|
|
let mut shared_key_data = [0u8; 256 / 8];
|
|
|
|
hkdf.expand(b"key", &mut shared_key_data)?;
|
|
|
|
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
|
|
|
|
|
|
|
let mut nonce_data = [0u8; 12];
|
|
|
|
hkdf.expand(b"nonce", &mut nonce_data)?;
|
|
|
|
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
// decrypt a single shard and create the payload
|
2024-02-20 23:33:54 +00:00
|
|
|
let (share, threshold) =
|
|
|
|
self.decrypt_one_shard(private_keys, &encrypted_messages, prompt.clone())?;
|
2024-01-20 06:20:04 +00:00
|
|
|
let mut payload = Vec::from(&share);
|
|
|
|
payload.insert(0, HUNK_VERSION);
|
|
|
|
payload.insert(1, threshold);
|
|
|
|
assert!(
|
2024-04-15 00:27:00 +00:00
|
|
|
payload.len() < PLAINTEXT_LENGTH as usize,
|
2024-05-04 02:41:38 +00:00
|
|
|
"invalid share length (too long, must be less than {PLAINTEXT_LENGTH} bytes)"
|
2024-01-20 06:20:04 +00:00
|
|
|
);
|
|
|
|
|
2024-04-15 00:27:00 +00:00
|
|
|
// convert plaintext to static-size payload
|
2024-01-20 06:20:04 +00:00
|
|
|
#[allow(clippy::assertions_on_constants)]
|
|
|
|
{
|
2024-04-15 00:27:00 +00:00
|
|
|
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
|
2024-01-20 06:20:04 +00:00
|
|
|
}
|
|
|
|
|
2024-04-15 00:27:00 +00:00
|
|
|
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
|
|
|
|
// duplicate mnemonic words. This version does not include that, and instead uses a
|
|
|
|
// repeated length byte.
|
2024-01-20 06:20:04 +00:00
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
2024-04-15 00:27:00 +00:00
|
|
|
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
|
|
|
|
"previously asserted length must be < {PLAINTEXT_LENGTH}",
|
|
|
|
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
|
|
|
|
)); PLAINTEXT_LENGTH as usize];
|
|
|
|
plaintext_bytes[..payload.len()].clone_from_slice(&payload);
|
|
|
|
|
|
|
|
// encrypt data
|
|
|
|
let encrypted_bytes = shared_key.encrypt(nonce, plaintext_bytes.as_slice())?;
|
2024-04-19 03:39:29 +00:00
|
|
|
assert_eq!(
|
|
|
|
encrypted_bytes.len(),
|
|
|
|
ENCRYPTED_LENGTH as usize,
|
|
|
|
bug!("encrypted bytes size != expected len"),
|
|
|
|
);
|
|
|
|
let mut mnemonic_bytes = [0u8; ENCRYPTED_LENGTH as usize];
|
|
|
|
mnemonic_bytes.copy_from_slice(&encrypted_bytes);
|
2024-04-15 00:27:00 +00:00
|
|
|
|
2024-08-01 13:29:03 +00:00
|
|
|
let payload_mnemonic = Mnemonic::from_array(mnemonic_bytes);
|
2024-01-20 06:20:04 +00:00
|
|
|
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
{
|
|
|
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
|
|
|
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
|
|
|
qrcode_data.extend(payload_mnemonic.as_bytes());
|
2024-04-15 03:15:35 +00:00
|
|
|
if let Ok(qrcode) = qrencode(
|
|
|
|
&BASE64_STANDARD.encode(qrcode_data),
|
|
|
|
ErrorCorrection::Highest,
|
|
|
|
) {
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt
|
|
|
|
.lock()
|
2024-02-21 01:39:28 +00:00
|
|
|
.expect(bug!(POISONED_MUTEX))
|
|
|
|
.prompt_message(PromptMessage::Text(
|
|
|
|
concat!(
|
|
|
|
"A QR code will be displayed after this prompt. ",
|
|
|
|
"Send the QR code back to the operator combining the shards. ",
|
|
|
|
"Nobody else should scan this QR code."
|
|
|
|
)
|
|
|
|
.to_string(),
|
|
|
|
))?;
|
|
|
|
prompt
|
|
|
|
.lock()
|
|
|
|
.expect(bug!(POISONED_MUTEX))
|
2024-02-20 23:33:54 +00:00
|
|
|
.prompt_message(PromptMessage::Data(qrcode))?;
|
2024-01-20 06:20:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-20 23:33:54 +00:00
|
|
|
prompt
|
|
|
|
.lock()
|
2024-02-21 01:39:28 +00:00
|
|
|
.expect(bug!(POISONED_MUTEX))
|
2024-02-20 23:33:54 +00:00
|
|
|
.prompt_message(PromptMessage::Text(format!(
|
2024-01-20 06:20:04 +00:00
|
|
|
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
|
|
|
|
)))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2024-02-15 08:01:23 +00:00
|
|
|
|
|
|
|
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
|
|
|
|
/// threshold.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
/// The method may return an error if the shares can't be encrypted.
|
|
|
|
fn shard_and_encrypt(
|
|
|
|
&self,
|
|
|
|
threshold: u8,
|
|
|
|
max: u8,
|
|
|
|
secret: &[u8],
|
2024-02-19 01:19:29 +00:00
|
|
|
public_key_discovery: impl KeyDiscovery<Self>,
|
2024-02-15 08:01:23 +00:00
|
|
|
writer: impl Write + Send + Sync,
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
let mut signing_key = self.derive_signing_key(secret);
|
|
|
|
|
|
|
|
let sharks = Sharks(threshold);
|
|
|
|
let dealer = sharks.dealer(secret);
|
|
|
|
|
2024-02-19 01:19:29 +00:00
|
|
|
let public_keys = public_key_discovery.discover_public_keys()?;
|
2024-02-15 08:01:23 +00:00
|
|
|
assert!(
|
|
|
|
public_keys.len() < u8::MAX as usize,
|
|
|
|
"must have less than u8::MAX public keys"
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
max,
|
|
|
|
public_keys.len() as u8,
|
|
|
|
"max must be equal to amount of public keys"
|
|
|
|
);
|
|
|
|
let max = public_keys.len() as u8;
|
|
|
|
assert!(max >= threshold, "threshold must not exceed max keys");
|
|
|
|
|
|
|
|
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
|
|
|
|
let mut messages = vec![header];
|
|
|
|
for (pk, share) in public_keys.iter().zip(dealer) {
|
|
|
|
let shard = Vec::from(&share);
|
|
|
|
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.format_shard_file(&messages, writer)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2024-01-20 06:20:04 +00:00
|
|
|
}
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
|
2024-01-05 04:11:15 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum SharksError {
|
2024-01-16 02:44:48 +00:00
|
|
|
/// A Shamir Share could not be created.
|
2024-01-05 04:11:15 +00:00
|
|
|
#[error("Error creating share: {0}")]
|
|
|
|
Share(String),
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The Shamir shares could not be combined.
|
2024-01-05 04:11:15 +00:00
|
|
|
#[error("Error combining shares: {0}")]
|
|
|
|
CombineShare(String),
|
|
|
|
}
|
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// The mnemonic or QR code used to transport an encrypted shard did not store the correct amount
|
|
|
|
/// of data.
|
2024-01-07 04:23:03 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
2024-01-12 00:49:56 +00:00
|
|
|
#[error("Mnemonic or QR code did not store enough data")]
|
|
|
|
pub struct InvalidData;
|
2024-01-07 04:23:03 +00:00
|
|
|
|
2024-01-05 04:05:30 +00:00
|
|
|
/// Decrypt hunk version 1:
|
|
|
|
/// 1 byte: Version
|
|
|
|
/// 1 byte: Threshold
|
|
|
|
/// Data: &[u8]
|
2024-04-15 00:27:00 +00:00
|
|
|
pub(crate) const HUNK_VERSION: u8 = 2;
|
2024-01-05 04:05:30 +00:00
|
|
|
pub(crate) const HUNK_OFFSET: usize = 2;
|
|
|
|
|
2024-02-04 22:51:38 +00:00
|
|
|
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
|
|
|
const QRCODE_TIMEOUT: u64 = 60; // One minute
|
2024-02-06 01:19:05 +00:00
|
|
|
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
|
|
|
const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
|
2024-02-04 22:51:38 +00:00
|
|
|
|
2024-01-16 02:44:48 +00:00
|
|
|
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
|
|
|
/// shares, and combine them.
|
2024-01-07 04:23:03 +00:00
|
|
|
///
|
2024-01-16 02:44:48 +00:00
|
|
|
/// # Errors
|
|
|
|
/// The function may error if:
|
|
|
|
/// * Prompting for transport-encrypted shards fails.
|
|
|
|
/// * Decrypting shards fails.
|
|
|
|
/// * Combining shards fails.
|
|
|
|
///
|
|
|
|
/// # Panics
|
2024-01-07 04:23:03 +00:00
|
|
|
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
|
|
|
/// incompatible with the currently running version.
|
2024-01-07 05:44:59 +00:00
|
|
|
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
2024-01-11 03:35:31 +00:00
|
|
|
let mut pm = Terminal::new(stdin(), stdout())?;
|
2024-01-05 04:05:30 +00:00
|
|
|
|
|
|
|
let mut iter_count = None;
|
|
|
|
let mut shares = vec![];
|
|
|
|
|
|
|
|
let mut threshold = 0;
|
2024-02-06 01:19:05 +00:00
|
|
|
let mut iter = 0;
|
2024-01-05 04:05:30 +00:00
|
|
|
|
|
|
|
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
2024-02-06 01:19:05 +00:00
|
|
|
iter += 1;
|
2024-01-05 04:05:30 +00:00
|
|
|
let our_key = EphemeralSecret::random();
|
2024-08-01 13:29:03 +00:00
|
|
|
let key_mnemonic = Mnemonic::try_from_slice(PublicKey::from(&our_key).as_bytes())?;
|
2024-01-12 00:49:56 +00:00
|
|
|
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
{
|
|
|
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
2024-04-09 23:46:37 +00:00
|
|
|
let qrcode_data = key_mnemonic.to_bytes();
|
2024-04-15 03:15:35 +00:00
|
|
|
if let Ok(qrcode) = qrencode(
|
|
|
|
&BASE64_STANDARD.encode(qrcode_data),
|
|
|
|
ErrorCorrection::Highest,
|
|
|
|
) {
|
2024-02-06 01:19:05 +00:00
|
|
|
pm.prompt_message(PromptMessage::Text(format!(
|
|
|
|
concat!(
|
2024-04-24 17:13:48 +00:00
|
|
|
"QR code #{iter} will be displayed after this prompt. ",
|
|
|
|
"Send the QR code to the next shardholder. ",
|
|
|
|
"Only the next shardholder should scan the QR code."
|
2024-02-06 01:19:05 +00:00
|
|
|
),
|
|
|
|
iter = iter
|
|
|
|
)))?;
|
2024-01-12 00:49:56 +00:00
|
|
|
pm.prompt_message(PromptMessage::Data(qrcode))?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-11 04:28:56 +00:00
|
|
|
pm.prompt_message(PromptMessage::Text(format!(
|
2024-02-06 01:19:05 +00:00
|
|
|
concat!(
|
2024-04-24 17:13:48 +00:00
|
|
|
"Upon request, these words should be sent to the shardholder: ",
|
2024-04-09 23:46:37 +00:00
|
|
|
"{key_mnemonic}"
|
2024-02-06 01:19:05 +00:00
|
|
|
),
|
|
|
|
key_mnemonic = key_mnemonic,
|
2024-01-05 04:05:30 +00:00
|
|
|
)))?;
|
|
|
|
|
2024-01-12 00:49:56 +00:00
|
|
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
|
|
|
let mut payload_data = None;
|
|
|
|
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
{
|
2024-02-04 22:51:38 +00:00
|
|
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
2024-04-15 01:19:06 +00:00
|
|
|
if let Ok(Some(qrcode_content)) =
|
2024-02-04 22:51:38 +00:00
|
|
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
2024-01-12 00:49:56 +00:00
|
|
|
{
|
2024-04-15 03:15:35 +00:00
|
|
|
let decoded_data = BASE64_STANDARD
|
|
|
|
.decode(qrcode_content)
|
|
|
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
|
|
|
assert_eq!(
|
|
|
|
decoded_data.len(),
|
2024-05-29 20:15:17 +00:00
|
|
|
// Include length of public key
|
|
|
|
ENCRYPTED_LENGTH as usize + 32,
|
2024-04-15 03:15:35 +00:00
|
|
|
bug!("invalid payload data")
|
|
|
|
);
|
2024-01-12 00:49:56 +00:00
|
|
|
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
|
|
|
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
|
|
|
} else {
|
2024-02-06 01:19:05 +00:00
|
|
|
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
2024-01-12 00:49:56 +00:00
|
|
|
};
|
2024-01-06 05:58:18 +00:00
|
|
|
}
|
|
|
|
|
2024-01-12 00:49:56 +00:00
|
|
|
let (pubkey, payload) = match (pubkey_data, payload_data) {
|
|
|
|
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
|
|
|
_ => {
|
|
|
|
let validator = MnemonicSetValidator {
|
2024-04-15 00:27:00 +00:00
|
|
|
word_lengths: [24, 39],
|
2024-01-12 00:49:56 +00:00
|
|
|
};
|
|
|
|
|
2024-02-19 10:20:33 +00:00
|
|
|
let [pubkey_mnemonic, payload_mnemonic] = pm
|
2024-02-19 10:32:24 +00:00
|
|
|
.prompt_validated_wordlist::<English, _>(
|
2024-02-19 10:20:33 +00:00
|
|
|
QRCODE_COULDNT_READ,
|
|
|
|
3,
|
|
|
|
validator.to_fn(),
|
|
|
|
)?;
|
2024-01-12 00:49:56 +00:00
|
|
|
let pubkey = pubkey_mnemonic
|
2024-01-19 04:50:23 +00:00
|
|
|
.as_bytes()
|
2024-01-12 00:49:56 +00:00
|
|
|
.try_into()
|
|
|
|
.map_err(|_| InvalidData)?;
|
2024-01-19 04:50:23 +00:00
|
|
|
let payload = payload_mnemonic.to_bytes();
|
2024-01-12 00:49:56 +00:00
|
|
|
(pubkey, payload)
|
|
|
|
}
|
2024-01-10 20:34:29 +00:00
|
|
|
};
|
2024-01-05 04:05:30 +00:00
|
|
|
|
2024-04-15 00:27:00 +00:00
|
|
|
assert_eq!(
|
|
|
|
payload.len(),
|
|
|
|
ENCRYPTED_LENGTH as usize,
|
|
|
|
bug!("invalid payload data")
|
|
|
|
);
|
|
|
|
|
2024-04-17 19:36:42 +00:00
|
|
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey));
|
2024-04-19 03:39:29 +00:00
|
|
|
assert!(
|
|
|
|
shared_secret.was_contributory(),
|
|
|
|
bug!("shared secret might be insecure")
|
|
|
|
);
|
2024-04-17 19:36:42 +00:00
|
|
|
let hkdf = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
|
2024-01-05 04:05:30 +00:00
|
|
|
|
2024-04-09 23:46:37 +00:00
|
|
|
let mut shared_key_data = [0u8; 256 / 8];
|
|
|
|
hkdf.expand(b"key", &mut shared_key_data)?;
|
|
|
|
let shared_key = Aes256Gcm::new_from_slice(&shared_key_data)?;
|
|
|
|
|
|
|
|
let mut nonce_data = [0u8; 12];
|
|
|
|
hkdf.expand(b"nonce", &mut nonce_data)?;
|
|
|
|
let nonce = Nonce::<U12>::from_slice(&nonce_data);
|
|
|
|
|
2024-04-15 00:27:00 +00:00
|
|
|
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
|
2024-01-05 04:05:30 +00:00
|
|
|
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
|
|
|
|
|
|
|
match &mut iter_count {
|
|
|
|
Some(n) => {
|
|
|
|
// Must be > 0 to start loop, can't go lower
|
|
|
|
*n -= 1;
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
|
|
|
|
threshold = payload[1];
|
|
|
|
let _ = iter_count.insert(threshold - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-15 00:27:00 +00:00
|
|
|
let payload_len = payload.last().expect(bug!("payload should not be empty"));
|
|
|
|
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
|
2024-01-05 04:05:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let shares = shares
|
|
|
|
.into_iter()
|
|
|
|
.map(|s| Share::try_from(s.as_slice()))
|
|
|
|
.collect::<Result<Vec<_>, &str>>()
|
2024-01-05 04:11:15 +00:00
|
|
|
.map_err(|e| SharksError::Share(e.to_string()))?;
|
2024-01-05 04:05:30 +00:00
|
|
|
let secret = Sharks(threshold)
|
|
|
|
.recover(&shares)
|
2024-01-05 04:11:15 +00:00
|
|
|
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
|
2024-01-05 04:05:30 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Verification would take up too much size, mnemonic would be very large
|
|
|
|
let userid = UserID::from("keyfork-sss");
|
|
|
|
let kdr = DerivationRequest::new(
|
|
|
|
DerivationAlgorithm::Ed25519,
|
|
|
|
&DerivationPath::from_str("m/7366512'/0'")?,
|
|
|
|
)
|
|
|
|
.derive_with_master_seed(secret.to_vec())?;
|
|
|
|
let derived_cert = keyfork_derive_openpgp::derive(
|
|
|
|
kdr,
|
|
|
|
&[KeyFlags::empty().set_certification().set_signing()],
|
|
|
|
userid,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
// NOTE: Signatures on certs will be different. Compare fingerprints instead.
|
|
|
|
let derived_fp = derived_cert.fingerprint();
|
|
|
|
let expected_fp = root_cert.fingerprint();
|
|
|
|
if derived_fp != expected_fp {
|
|
|
|
return Err(Error::InvalidSecret(derived_fp, expected_fp));
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
2024-01-07 05:44:59 +00:00
|
|
|
w.write_all(&secret)?;
|
2024-01-05 04:05:30 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|