use anyhow::{Context, Result, anyhow};
use bip39::Mnemonic;
use hkdf::Hkdf;
use prost::Message;
use sha2::Sha256;
const BIP39_PASSPHRASE: &str = "";
pub type Key = [u8; 32];
pub struct DerivedKeys {
pub app_stream_key: Key,
pub file_stream_key: Key,
pub chunk_id_key: Key,
}
pub fn derive_keys(mnemonic: &Mnemonic) -> Result<DerivedKeys> {
let seed = mnemonic.to_seed(BIP39_PASSPHRASE);
let main_key: Key = seed[32..64]
.try_into()
.context("Failed to extract main_key from seed")?;
let hk = Hkdf::<Sha256>::from_prk(&main_key)
.map_err(|e| anyhow!("Invalid PRK length for HKDF: {}", e))?;
let expand_key = |info: &[u8]| -> Result<Key> {
let mut okm = [0u8; 32];
hk.expand(info, &mut okm).map_err(|e| {
anyhow!(
"HKDF expand failed for info '{:?}': {}",
String::from_utf8_lossy(info),
e
)
})?;
Ok(okm)
};
Ok(DerivedKeys {
app_stream_key: expand_key(b"app backup stream key")?,
file_stream_key: expand_key(b"stream key")?,
chunk_id_key: expand_key(b"Chunk ID calculation")?,
})
}
pub fn create_streaming_aead(key: Key) -> Result<Box<dyn tink_core::StreamingAead>> {
tink_streaming_aead::init();
let key_template = tink_streaming_aead::aes256_gcm_hkdf_1mb_key_template();
let key_format = tink_proto::AesGcmHkdfStreamingKeyFormat::decode(key_template.value.as_ref())
.context("Failed to decode key format from template")?;
let streaming_key_proto = tink_proto::AesGcmHkdfStreamingKey {
version: key_format.version,
params: key_format.params,
key_value: key.to_vec(),
};
let key_data = tink_proto::KeyData {
type_url: key_template.type_url.clone(),
value: streaming_key_proto.encode_to_vec(),
key_material_type: tink_proto::key_data::KeyMaterialType::Symmetric as i32,
};
let keyset_key = tink_proto::keyset::Key {
key_data: Some(key_data),
status: tink_proto::KeyStatusType::Enabled as i32,
key_id: 1,
output_prefix_type: tink_proto::OutputPrefixType::Raw as i32,
};
let keyset = tink_proto::Keyset {
primary_key_id: 1,
key: vec![keyset_key],
};
let handle = tink_core::keyset::insecure::new_handle(keyset)
.map_err(|e| anyhow!("Failed to create Tink KeysetHandle: {}", e))?;
let primitive = tink_streaming_aead::new(&handle)
.map_err(|e| anyhow!("Failed to create StreamingAead primitive: {}", e))?;
Ok(primitive)
}
#[cfg(test)]
mod tests {
use super::*;
use bip39::{Language, Mnemonic};
use std::io::{Cursor, Read, Write};
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct SharedBuffer {
data: Arc<Mutex<Vec<u8>>>,
}
impl Write for SharedBuffer {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.data.lock().unwrap().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn test_derive_keys_deterministic() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let mnemonic = Mnemonic::parse_in(Language::English, phrase).unwrap();
let keys1 = derive_keys(&mnemonic).unwrap();
let keys2 = derive_keys(&mnemonic).unwrap();
assert_eq!(keys1.app_stream_key, keys2.app_stream_key);
assert_eq!(keys1.file_stream_key, keys2.file_stream_key);
assert_eq!(keys1.chunk_id_key, keys2.chunk_id_key);
}
#[test]
fn test_create_streaming_aead_encrypt_decrypt() {
let key = [42u8; 32];
let aead = create_streaming_aead(key).unwrap();
let plaintext = b"Hello, Seednaut!";
let aad = b"associated data";
let buffer = Arc::new(Mutex::new(Vec::new()));
let writer_buf = SharedBuffer {
data: buffer.clone(),
};
let mut writer = aead
.new_encrypting_writer(Box::new(writer_buf), aad)
.map_err(|e| anyhow!("Encryption failed: {:?}", e))
.unwrap();
writer.write_all(plaintext).unwrap();
writer.flush().unwrap();
drop(writer);
let ciphertext = Arc::try_unwrap(buffer).unwrap().into_inner().unwrap();
assert!(!ciphertext.is_empty());
let mut decrypted = Vec::new();
let mut reader = aead
.new_decrypting_reader(Box::new(Cursor::new(ciphertext)), aad)
.map_err(|e| anyhow!("Decryption failed: {:?}", e))
.unwrap();
reader.read_to_end(&mut decrypted).unwrap();
assert_eq!(decrypted, plaintext);
}
}