|
|
|
@ -7,22 +7,28 @@ use std::{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use aes_gcm::{
|
|
|
|
|
aead::{consts::U12, Aead, AeadCore, OsRng},
|
|
|
|
|
aead::{consts::U12, Aead},
|
|
|
|
|
Aes256Gcm, KeyInit, Nonce,
|
|
|
|
|
};
|
|
|
|
|
use hkdf::Hkdf;
|
|
|
|
|
use keyfork_bug::{bug, POISONED_MUTEX};
|
|
|
|
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
|
|
|
|
use keyfork_prompt::{
|
|
|
|
|
validators::{mnemonic::MnemonicSetValidator, Validator},
|
|
|
|
|
validators::{
|
|
|
|
|
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
|
|
|
|
Validator,
|
|
|
|
|
},
|
|
|
|
|
Message as PromptMessage, PromptHandler, Terminal,
|
|
|
|
|
};
|
|
|
|
|
use sha2::Sha256;
|
|
|
|
|
use sharks::{Share, Sharks};
|
|
|
|
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
|
|
|
|
use base64::prelude::{BASE64_STANDARD, Engine};
|
|
|
|
|
|
|
|
|
|
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
|
|
|
|
|
const ENC_LEN: u8 = 4 * 16;
|
|
|
|
|
// 32-byte share, 1-byte index, 1-byte threshold, 1-byte version == 36 bytes
|
|
|
|
|
// Encrypted, is 52 bytes
|
|
|
|
|
const PLAINTEXT_LENGTH: u8 = 36;
|
|
|
|
|
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "openpgp")]
|
|
|
|
|
pub mod openpgp;
|
|
|
|
@ -194,7 +200,6 @@ pub trait Format {
|
|
|
|
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
|
|
|
|
|
|
|
|
|
// establish AES-256-GCM key via ECDH
|
|
|
|
|
let mut nonce_data: Option<[u8; 12]> = None;
|
|
|
|
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
|
|
|
|
|
|
|
|
|
// receive remote data via scanning QR code from camera
|
|
|
|
@ -204,12 +209,11 @@ pub trait Format {
|
|
|
|
|
.lock()
|
|
|
|
|
.expect(bug!(POISONED_MUTEX))
|
|
|
|
|
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
|
|
|
|
if let Ok(Some(hex)) =
|
|
|
|
|
if let Ok(Some(qrcode_content)) =
|
|
|
|
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
|
|
|
|
{
|
|
|
|
|
let decoded_data = smex::decode(&hex)?;
|
|
|
|
|
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
|
|
|
|
|
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
|
|
|
|
|
let decoded_data = BASE64_STANDARD.decode(qrcode_content).unwrap();
|
|
|
|
|
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
|
|
|
|
} else {
|
|
|
|
|
prompt
|
|
|
|
|
.lock()
|
|
|
|
@ -219,30 +223,23 @@ pub trait Format {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
|
|
|
|
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
|
|
|
|
|
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
|
|
|
|
|
_ => {
|
|
|
|
|
let validator = MnemonicSetValidator {
|
|
|
|
|
word_lengths: [9, 24],
|
|
|
|
|
let their_pubkey = match pubkey_data {
|
|
|
|
|
Some(pubkey) => pubkey,
|
|
|
|
|
None => {
|
|
|
|
|
let validator = MnemonicValidator {
|
|
|
|
|
word_length: Some(WordLength::Count(24)),
|
|
|
|
|
};
|
|
|
|
|
let [nonce_mnemonic, pubkey_mnemonic] = prompt
|
|
|
|
|
prompt
|
|
|
|
|
.lock()
|
|
|
|
|
.expect(bug!(POISONED_MUTEX))
|
|
|
|
|
.prompt_validated_wordlist::<English, _>(
|
|
|
|
|
QRCODE_COULDNT_READ,
|
|
|
|
|
3,
|
|
|
|
|
validator.to_fn(),
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
let nonce = nonce_mnemonic
|
|
|
|
|
)?
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.map_err(|_| InvalidData)?;
|
|
|
|
|
let pubkey = pubkey_mnemonic
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.try_into()
|
|
|
|
|
.map_err(|_| InvalidData)?;
|
|
|
|
|
(nonce, pubkey)
|
|
|
|
|
.map_err(|_| InvalidData)?
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -253,9 +250,14 @@ pub trait Format {
|
|
|
|
|
.diffie_hellman(&PublicKey::from(their_pubkey))
|
|
|
|
|
.to_bytes();
|
|
|
|
|
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
|
|
|
|
let mut hkdf_output = [0u8; 256 / 8];
|
|
|
|
|
hkdf.expand(&[], &mut hkdf_output)?;
|
|
|
|
|
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// decrypt a single shard and create the payload
|
|
|
|
|
let (share, threshold) =
|
|
|
|
@ -264,49 +266,41 @@ pub trait Format {
|
|
|
|
|
payload.insert(0, HUNK_VERSION);
|
|
|
|
|
payload.insert(1, threshold);
|
|
|
|
|
assert!(
|
|
|
|
|
payload.len() <= ENC_LEN as usize,
|
|
|
|
|
"invalid share length (too long, max {ENC_LEN} bytes)"
|
|
|
|
|
payload.len() < PLAINTEXT_LENGTH as usize,
|
|
|
|
|
"invalid share length (too long, max {PLAINTEXT_LENGTH} bytes)"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// encrypt data
|
|
|
|
|
let nonce = Nonce::<U12>::from_slice(&nonce);
|
|
|
|
|
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
|
|
|
|
|
|
|
|
|
|
// convert data to a static-size payload
|
|
|
|
|
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
|
|
|
|
|
// convert plaintext to static-size payload
|
|
|
|
|
#[allow(clippy::assertions_on_constants)]
|
|
|
|
|
{
|
|
|
|
|
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
|
|
|
|
|
assert!(PLAINTEXT_LENGTH < u8::MAX, "length byte can be u8");
|
|
|
|
|
}
|
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
|
|
|
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
|
|
|
|
|
assert!(
|
|
|
|
|
payload_bytes.len() < out_bytes.len(),
|
|
|
|
|
"encrypted payload larger than acceptable limit"
|
|
|
|
|
);
|
|
|
|
|
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
|
|
|
|
|
|
|
|
|
|
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
|
|
|
|
|
// difficulty when entering in prompts manually, as one's place could be lost due to
|
|
|
|
|
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
|
|
|
|
|
// but not including the last byte.
|
|
|
|
|
// 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.
|
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
|
|
|
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
|
|
|
|
*byte = (i % u8::MAX as usize) as u8;
|
|
|
|
|
}
|
|
|
|
|
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())?;
|
|
|
|
|
|
|
|
|
|
assert_eq!(encrypted_bytes.len(), ENCRYPTED_LENGTH as usize);
|
|
|
|
|
|
|
|
|
|
// safety: size of out_bytes is constant and always % 4 == 0
|
|
|
|
|
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
|
|
|
|
|
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&encrypted_bytes) };
|
|
|
|
|
dbg!(payload_mnemonic.words().len());
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
|
{
|
|
|
|
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
|
|
|
|
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
|
|
|
|
qrcode_data.extend(payload_mnemonic.as_bytes());
|
|
|
|
|
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
|
|
|
|
if let Ok(qrcode) = qrencode(&BASE64_STANDARD.encode(qrcode_data), ErrorCorrection::Highest) {
|
|
|
|
|
prompt
|
|
|
|
|
.lock()
|
|
|
|
|
.expect(bug!(POISONED_MUTEX))
|
|
|
|
@ -401,7 +395,7 @@ pub struct InvalidData;
|
|
|
|
|
/// 1 byte: Version
|
|
|
|
|
/// 1 byte: Threshold
|
|
|
|
|
/// Data: &[u8]
|
|
|
|
|
pub(crate) const HUNK_VERSION: u8 = 1;
|
|
|
|
|
pub(crate) const HUNK_VERSION: u8 = 2;
|
|
|
|
|
pub(crate) const HUNK_OFFSET: usize = 2;
|
|
|
|
|
|
|
|
|
|
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
|
|
|
@ -432,17 +426,14 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|
|
|
|
|
|
|
|
|
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
|
|
|
|
iter += 1;
|
|
|
|
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
|
|
|
|
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
|
|
|
|
|
let our_key = EphemeralSecret::random();
|
|
|
|
|
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
|
{
|
|
|
|
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
|
|
|
|
let mut qrcode_data = nonce_mnemonic.to_bytes();
|
|
|
|
|
qrcode_data.extend(key_mnemonic.as_bytes());
|
|
|
|
|
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
|
|
|
|
let qrcode_data = key_mnemonic.to_bytes();
|
|
|
|
|
if let Ok(qrcode) = qrencode(&BASE64_STANDARD.encode(qrcode_data), ErrorCorrection::Highest) {
|
|
|
|
|
pm.prompt_message(PromptMessage::Text(format!(
|
|
|
|
|
concat!(
|
|
|
|
|
"A QR code will be displayed after this prompt. ",
|
|
|
|
@ -458,10 +449,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|
|
|
|
pm.prompt_message(PromptMessage::Text(format!(
|
|
|
|
|
concat!(
|
|
|
|
|
"Upon request, these words should be sent to shardholder {iter}: ",
|
|
|
|
|
"{nonce_mnemonic} {key_mnemonic}"
|
|
|
|
|
"{key_mnemonic}"
|
|
|
|
|
),
|
|
|
|
|
iter = iter,
|
|
|
|
|
nonce_mnemonic = nonce_mnemonic,
|
|
|
|
|
key_mnemonic = key_mnemonic,
|
|
|
|
|
)))?;
|
|
|
|
|
|
|
|
|
@ -471,10 +461,10 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|
|
|
|
#[cfg(feature = "qrcode")]
|
|
|
|
|
{
|
|
|
|
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
|
|
|
|
if let Ok(Some(hex)) =
|
|
|
|
|
if let Ok(Some(qrcode_content)) =
|
|
|
|
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
|
|
|
|
{
|
|
|
|
|
let decoded_data = smex::decode(&hex)?;
|
|
|
|
|
let decoded_data = BASE64_STANDARD.decode(qrcode_content).unwrap();
|
|
|
|
|
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
|
|
|
|
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
|
|
|
|
} else {
|
|
|
|
@ -486,7 +476,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|
|
|
|
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
|
|
|
|
_ => {
|
|
|
|
|
let validator = MnemonicSetValidator {
|
|
|
|
|
word_lengths: [24, 48],
|
|
|
|
|
word_lengths: [24, 39],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let [pubkey_mnemonic, payload_mnemonic] = pm
|
|
|
|
@ -504,14 +494,24 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
payload.len(),
|
|
|
|
|
ENCRYPTED_LENGTH as usize,
|
|
|
|
|
bug!("invalid payload data")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
|
|
|
|
|
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
|
|
|
|
let mut hkdf_output = [0u8; 256 / 8];
|
|
|
|
|
hkdf.expand(&[], &mut hkdf_output)?;
|
|
|
|
|
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
|
|
|
|
|
|
|
|
|
let payload =
|
|
|
|
|
shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?;
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
|
|
|
|
|
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
|
|
|
|
|
|
|
|
|
|
match &mut iter_count {
|
|
|
|
@ -526,7 +526,8 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shares.push(payload[HUNK_OFFSET..].to_vec());
|
|
|
|
|
let payload_len = payload.last().expect(bug!("payload should not be empty"));
|
|
|
|
|
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let shares = shares
|
|
|
|
|