use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use aes_gcm::aead::Aead;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use argon2::Argon2;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use hmac::{Hmac, Mac};
use rand::RngCore;
use sha2::Sha256;
use std::sync::Mutex as StdMutex;
use tokio::sync::Mutex;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
use zeroize::Zeroizing;
use crate::error::PlatformError;
use crate::traits::{
CustodyType, KeyCustody, KeyHandle, KeyType, PseudonymKeypair, PublicKey, SharedSecret,
Signature,
};
const FORMAT_VERSION: u8 = 0x01;
const SALT_LEN: usize = 16;
const NONCE_LEN: usize = 12;
const KEY_LEN: usize = 32;
const TAG_LEN: usize = 16;
const ENTRY_SIZE: usize = 1 + NONCE_LEN + KEY_LEN + TAG_LEN;
const HEADER_SIZE: usize = 1 + SALT_LEN + 4;
const KEY_TYPE_ED25519: u8 = 0x01;
const KEY_TYPE_X25519: u8 = 0x02;
const ARGON2_ITERATIONS: u32 = 3;
const ARGON2_MEMORY_KIB: u32 = 65_536;
const ARGON2_PARALLELISM: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StoredKeyType {
Ed25519,
X25519,
}
impl StoredKeyType {
const fn to_byte(self) -> u8 {
match self {
Self::Ed25519 => KEY_TYPE_ED25519,
Self::X25519 => KEY_TYPE_X25519,
}
}
fn from_byte(b: u8) -> Result<Self, PlatformError> {
match b {
KEY_TYPE_ED25519 => Ok(Self::Ed25519),
KEY_TYPE_X25519 => Ok(Self::X25519),
_ => Err(PlatformError::CustodyError(format!(
"unknown key type byte: {b:#04x}"
))),
}
}
}
struct HandleMap {
entries: HashMap<u64, (StoredKeyType, usize)>,
}
impl HandleMap {
fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
}
pub struct FileKeyCustody {
path: PathBuf,
derived_key: Zeroizing<[u8; 32]>,
handle_map: Mutex<HandleMap>,
next_id: AtomicU64,
pseudonym_keys: Mutex<HashMap<u64, SigningKey>>,
file_write_lock: StdMutex<()>,
}
impl FileKeyCustody {
pub fn new(path: &Path, passphrase: &str) -> Result<Self, PlatformError> {
if path.exists() {
Self::open_existing(path, passphrase)
} else {
Self::create_new(path, passphrase)
}
}
fn create_new(path: &Path, passphrase: &str) -> Result<Self, PlatformError> {
let mut salt = [0u8; SALT_LEN];
rand::rngs::OsRng.fill_bytes(&mut salt);
let derived_key = Self::derive_key(passphrase, &salt)?;
let mut data = Vec::with_capacity(HEADER_SIZE);
data.push(FORMAT_VERSION);
data.extend_from_slice(&salt);
data.extend_from_slice(&0u32.to_le_bytes());
std::fs::write(path, &data)
.map_err(|e| PlatformError::CustodyError(format!("failed to create key file: {e}")))?;
Ok(Self {
path: path.to_path_buf(),
derived_key,
handle_map: Mutex::new(HandleMap::new()),
next_id: AtomicU64::new(1),
pseudonym_keys: Mutex::new(HashMap::new()),
file_write_lock: StdMutex::new(()),
})
}
fn open_existing(path: &Path, passphrase: &str) -> Result<Self, PlatformError> {
let data = std::fs::read(path)
.map_err(|e| PlatformError::CustodyError(format!("failed to read key file: {e}")))?;
if data.len() < HEADER_SIZE {
return Err(PlatformError::CustodyError(
"key file too short for header".into(),
));
}
if data[0] != FORMAT_VERSION {
return Err(PlatformError::CustodyError(format!(
"unsupported key file version: {:#04x}",
data[0]
)));
}
let mut salt = [0u8; SALT_LEN];
salt.copy_from_slice(&data[1..=SALT_LEN]);
let entry_count = u32::from_le_bytes(
data[1 + SALT_LEN..HEADER_SIZE]
.try_into()
.map_err(|_| PlatformError::CustodyError("invalid entry count bytes".into()))?,
) as usize;
let expected_len = HEADER_SIZE + entry_count * ENTRY_SIZE;
if data.len() < expected_len {
return Err(PlatformError::CustodyError(format!(
"key file truncated: expected {expected_len} bytes, got {}",
data.len()
)));
}
let derived_key = Self::derive_key(passphrase, &salt)?;
let mut handle_map = HandleMap::new();
let mut next_id = 1u64;
for i in 0..entry_count {
let offset = HEADER_SIZE + i * ENTRY_SIZE;
let key_type_byte = data[offset];
let key_type = StoredKeyType::from_byte(key_type_byte)?;
let handle_id = next_id;
next_id += 1;
handle_map.entries.insert(handle_id, (key_type, i));
}
Ok(Self {
path: path.to_path_buf(),
derived_key,
handle_map: Mutex::new(handle_map),
next_id: AtomicU64::new(next_id),
pseudonym_keys: Mutex::new(HashMap::new()),
file_write_lock: StdMutex::new(()),
})
}
fn derive_key(
passphrase: &str,
salt: &[u8; SALT_LEN],
) -> Result<Zeroizing<[u8; 32]>, PlatformError> {
let params = argon2::Params::new(
ARGON2_MEMORY_KIB,
ARGON2_ITERATIONS,
ARGON2_PARALLELISM,
Some(32),
)
.map_err(|e| PlatformError::CustodyError(format!("argon2 params error: {e}")))?;
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key = Zeroizing::new([0u8; 32]);
argon2
.hash_password_into(passphrase.as_bytes(), salt, key.as_mut())
.map_err(|e| {
PlatformError::CustodyError(format!("argon2 key derivation failed: {e}"))
})?;
Ok(key)
}
fn encrypt_key(
&self,
plaintext: &[u8; KEY_LEN],
) -> Result<([u8; NONCE_LEN], Vec<u8>), PlatformError> {
let cipher = Aes256Gcm::new_from_slice(self.derived_key.as_ref())
.map_err(|e| PlatformError::CustodyError(format!("cipher init failed: {e}")))?;
let mut nonce_bytes = [0u8; NONCE_LEN];
rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext.as_ref())
.map_err(|e| PlatformError::CustodyError(format!("encryption failed: {e}")))?;
Ok((nonce_bytes, ciphertext))
}
fn decrypt_entry(
&self,
data: &[u8],
entry_index: usize,
) -> Result<Zeroizing<[u8; KEY_LEN]>, PlatformError> {
let offset = HEADER_SIZE + entry_index * ENTRY_SIZE;
let nonce_start = offset + 1;
let ct_start = nonce_start + NONCE_LEN;
let ct_end = ct_start + KEY_LEN + TAG_LEN;
let nonce = Nonce::from_slice(&data[nonce_start..ct_start]);
let ciphertext_and_tag = &data[ct_start..ct_end];
let cipher = Aes256Gcm::new_from_slice(self.derived_key.as_ref())
.map_err(|e| PlatformError::CustodyError(format!("cipher init failed: {e}")))?;
let plaintext = cipher.decrypt(nonce, ciphertext_and_tag).map_err(|_| {
PlatformError::CustodyError("decryption failed (wrong passphrase?)".into())
})?;
let mut key_bytes = Zeroizing::new([0u8; KEY_LEN]);
if plaintext.len() != KEY_LEN {
return Err(PlatformError::CustodyError(format!(
"decrypted key has wrong length: expected {KEY_LEN}, got {}",
plaintext.len()
)));
}
key_bytes.copy_from_slice(&plaintext);
Ok(key_bytes)
}
fn read_file(&self) -> Result<Vec<u8>, PlatformError> {
std::fs::read(&self.path)
.map_err(|e| PlatformError::CustodyError(format!("failed to read key file: {e}")))
}
fn append_entry(
&self,
key_type: StoredKeyType,
private_key: &[u8; KEY_LEN],
) -> Result<usize, PlatformError> {
let _lock = self
.file_write_lock
.lock()
.map_err(|_| PlatformError::CustodyError("file write lock poisoned".into()))?;
let mut data = self.read_file()?;
let count_offset = 1 + SALT_LEN;
let current_count = u32::from_le_bytes(
data[count_offset..count_offset + 4]
.try_into()
.map_err(|_| PlatformError::CustodyError("invalid entry count".into()))?,
);
let new_index = current_count as usize;
let (nonce, ciphertext) = self.encrypt_key(private_key)?;
data.push(key_type.to_byte());
data.extend_from_slice(&nonce);
data.extend_from_slice(&ciphertext);
let new_count = current_count + 1;
data[count_offset..count_offset + 4].copy_from_slice(&new_count.to_le_bytes());
std::fs::write(&self.path, &data)
.map_err(|e| PlatformError::CustodyError(format!("failed to write key file: {e}")))?;
Ok(new_index)
}
fn next_handle(&self) -> KeyHandle {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
KeyHandle::new(id)
}
async fn lookup_handle(
&self,
handle: &KeyHandle,
) -> Result<(StoredKeyType, usize), PlatformError> {
let map = self.handle_map.lock().await;
map.entries
.get(&handle.id())
.copied()
.ok_or(PlatformError::KeyNotFound)
}
async fn decrypt_ed25519_key(
&self,
handle: &KeyHandle,
) -> Result<(Zeroizing<[u8; KEY_LEN]>, SigningKey), PlatformError> {
let (key_type, entry_index) = self.lookup_handle(handle).await?;
if key_type != StoredKeyType::Ed25519 {
return Err(PlatformError::WrongKeyType {
expected: KeyType::Ed25519,
actual: KeyType::X25519,
});
}
let data = self.read_file()?;
let key_bytes = self.decrypt_entry(&data, entry_index)?;
let signing_key = SigningKey::from_bytes(&key_bytes);
Ok((key_bytes, signing_key))
}
pub async fn export_ed25519_signing_key(
&self,
handle: &KeyHandle,
) -> Result<SigningKey, PlatformError> {
let (_key_bytes, signing_key) = self.decrypt_ed25519_key(handle).await?;
Ok(signing_key)
}
}
#[allow(clippy::manual_async_fn)]
impl KeyCustody for FileKeyCustody {
fn generate_keypair(
&self,
key_type: KeyType,
) -> impl Future<Output = Result<KeyHandle, PlatformError>> + Send {
async move {
let mut key_bytes = Zeroizing::new([0u8; KEY_LEN]);
rand::rngs::OsRng.fill_bytes(key_bytes.as_mut());
let stored_type = match key_type {
KeyType::Ed25519 => StoredKeyType::Ed25519,
KeyType::X25519 => StoredKeyType::X25519,
};
let entry_index = self.append_entry(stored_type, &key_bytes)?;
let handle = self.next_handle();
let mut map = self.handle_map.lock().await;
map.entries.insert(handle.id(), (stored_type, entry_index));
drop(map);
Ok(handle)
}
}
fn sign(
&self,
key: &KeyHandle,
data: &[u8],
) -> impl Future<Output = Result<Signature, PlatformError>> + Send {
let key_id = key.id();
let handle = KeyHandle::new(key_id);
async move {
{
let pseudonyms = self.pseudonym_keys.lock().await;
if let Some(signing_key) = pseudonyms.get(&key_id) {
let signature = signing_key.sign(data);
return Ok(Signature::new(signature.to_bytes().to_vec()));
}
}
let (_key_bytes, signing_key) = self.decrypt_ed25519_key(&handle).await?;
let signature = signing_key.sign(data);
Ok(Signature::new(signature.to_bytes().to_vec()))
}
}
fn public_key(
&self,
key: &KeyHandle,
) -> impl Future<Output = Result<PublicKey, PlatformError>> + Send {
let key_id = key.id();
let handle = KeyHandle::new(key_id);
async move {
{
let pseudonyms = self.pseudonym_keys.lock().await;
if let Some(signing_key) = pseudonyms.get(&key_id) {
let vk = signing_key.verifying_key();
return Ok(PublicKey::new(vk.to_bytes().to_vec()));
}
}
let (key_type, entry_index) = self.lookup_handle(&handle).await?;
let data = self.read_file()?;
let key_bytes = self.decrypt_entry(&data, entry_index)?;
match key_type {
StoredKeyType::Ed25519 => {
let signing_key = SigningKey::from_bytes(&key_bytes);
let vk: VerifyingKey = signing_key.verifying_key();
Ok(PublicKey::new(vk.to_bytes().to_vec()))
}
StoredKeyType::X25519 => {
let secret = StaticSecret::from(*key_bytes);
let public = X25519PublicKey::from(&secret);
Ok(PublicKey::new(public.to_bytes().to_vec()))
}
}
}
}
fn destroy_key(
&self,
key: &KeyHandle,
) -> impl Future<Output = Result<(), PlatformError>> + Send {
let key_id = key.id();
async move {
{
let mut pseudonyms = self.pseudonym_keys.lock().await;
if pseudonyms.remove(&key_id).is_some() {
return Ok(());
}
}
let mut map = self.handle_map.lock().await;
if map.entries.remove(&key_id).is_none() {
return Err(PlatformError::KeyNotFound);
}
drop(map);
Ok(())
}
}
fn dh_agree(
&self,
key: &KeyHandle,
peer_public: &[u8; 32],
) -> impl Future<Output = Result<SharedSecret, PlatformError>> + Send {
let key_id = key.id();
let peer = *peer_public;
async move {
let handle = KeyHandle::new(key_id);
let (key_type, entry_index) = self.lookup_handle(&handle).await?;
if key_type != StoredKeyType::X25519 {
return Err(PlatformError::WrongKeyType {
expected: KeyType::X25519,
actual: KeyType::Ed25519,
});
}
let data = self.read_file()?;
let key_bytes = self.decrypt_entry(&data, entry_index)?;
let secret = StaticSecret::from(*key_bytes);
let peer_key = X25519PublicKey::from(peer);
let shared = secret.diffie_hellman(&peer_key);
let shared_bytes = Zeroizing::new(shared.to_bytes());
Ok(SharedSecret::new(*shared_bytes))
}
}
fn derive_pseudonym(
&self,
key: &KeyHandle,
context_id: &[u8],
) -> impl Future<Output = Result<PseudonymKeypair, PlatformError>> + Send {
let key_id = key.id();
let context_id = context_id.to_vec();
async move {
let handle = KeyHandle::new(key_id);
let (_key_bytes, signing_key) = self.decrypt_ed25519_key(&handle).await?;
let verifying_key = signing_key.verifying_key();
let mut mac =
<Hmac<Sha256> as Mac>::new_from_slice(verifying_key.to_bytes().as_slice())
.map_err(|e| PlatformError::CustodyError(e.to_string()))?;
mac.update(&context_id);
mac.update(b"scp-pseudonym");
let hmac_output = mac.finalize().into_bytes();
let mut seed = Zeroizing::new([0u8; 32]);
seed.copy_from_slice(&hmac_output[..32]);
let pseudonym_signing_key = SigningKey::from_bytes(&seed);
let pseudonym_verifying_key = pseudonym_signing_key.verifying_key();
let pseudo_handle = self.next_handle();
let mut pseudonyms = self.pseudonym_keys.lock().await;
pseudonyms.insert(pseudo_handle.id(), pseudonym_signing_key);
drop(pseudonyms);
Ok(PseudonymKeypair {
public_key: PublicKey::new(pseudonym_verifying_key.to_bytes().to_vec()),
key_handle: pseudo_handle,
})
}
}
fn derive_rotatable_pseudonym(
&self,
key: &KeyHandle,
context_id: &[u8],
pseudonym_epoch: u64,
) -> impl Future<Output = Result<PseudonymKeypair, PlatformError>> + Send {
let key_id = key.id();
let context_id = context_id.to_vec();
async move {
let handle = KeyHandle::new(key_id);
let (_key_bytes, signing_key) = self.decrypt_ed25519_key(&handle).await?;
let verifying_key = signing_key.verifying_key();
let mut mac =
<Hmac<Sha256> as Mac>::new_from_slice(verifying_key.to_bytes().as_slice())
.map_err(|e| PlatformError::CustodyError(e.to_string()))?;
mac.update(&context_id);
mac.update(&pseudonym_epoch.to_be_bytes());
mac.update(b"scp-pseudonym-v2");
let hmac_output = mac.finalize().into_bytes();
let mut seed = Zeroizing::new([0u8; 32]);
seed.copy_from_slice(&hmac_output[..32]);
let pseudonym_signing_key = SigningKey::from_bytes(&seed);
let pseudonym_verifying_key = pseudonym_signing_key.verifying_key();
let pseudo_handle = self.next_handle();
let mut pseudonyms = self.pseudonym_keys.lock().await;
pseudonyms.insert(pseudo_handle.id(), pseudonym_signing_key);
drop(pseudonyms);
Ok(PseudonymKeypair {
public_key: PublicKey::new(pseudonym_verifying_key.to_bytes().to_vec()),
key_handle: pseudo_handle,
})
}
}
fn custody_type(&self, _key: &KeyHandle) -> CustodyType {
CustodyType::Software
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_custody(dir: &TempDir, passphrase: &str) -> FileKeyCustody {
let path = dir.path().join("keys.scp");
FileKeyCustody::new(&path, passphrase).unwrap()
}
#[tokio::test]
async fn generate_ed25519_and_sign_verify() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "test-passphrase");
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let data = b"hello world";
let sig = custody.sign(&handle, data).await.unwrap();
assert_eq!(sig.as_bytes().len(), 64);
let pubkey = custody.public_key(&handle).await.unwrap();
let pk_bytes: [u8; 32] = pubkey.as_bytes().try_into().unwrap();
let verifying_key = VerifyingKey::from_bytes(&pk_bytes).unwrap();
let sig_bytes: [u8; 64] = sig.as_bytes().try_into().unwrap();
let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
assert!(
ed25519_dalek::Verifier::verify(&verifying_key, data, &signature).is_ok(),
"signature must verify"
);
}
#[tokio::test]
async fn generate_x25519_and_dh_agree() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let alice = custody.generate_keypair(KeyType::X25519).await.unwrap();
let bob = custody.generate_keypair(KeyType::X25519).await.unwrap();
let alice_pub = custody.public_key(&alice).await.unwrap();
let bob_pub = custody.public_key(&bob).await.unwrap();
let a_bytes: [u8; 32] = alice_pub.as_bytes().try_into().unwrap();
let b_bytes: [u8; 32] = bob_pub.as_bytes().try_into().unwrap();
let secret_ab = custody.dh_agree(&alice, &b_bytes).await.unwrap();
let secret_ba = custody.dh_agree(&bob, &a_bytes).await.unwrap();
assert_eq!(secret_ab.as_bytes(), secret_ba.as_bytes());
}
#[tokio::test]
async fn reopen_with_same_passphrase_succeeds() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("keys.scp");
let passphrase = "correct-horse-battery-staple";
let custody = FileKeyCustody::new(&path, passphrase).unwrap();
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let pubkey = custody.public_key(&handle).await.unwrap();
let sig = custody.sign(&handle, b"test data").await.unwrap();
drop(custody);
let custody2 = FileKeyCustody::new(&path, passphrase).unwrap();
let handle2 = KeyHandle::new(1);
let pubkey2 = custody2.public_key(&handle2).await.unwrap();
assert_eq!(
pubkey.as_bytes(),
pubkey2.as_bytes(),
"public key must be the same after reopening"
);
let sig2 = custody2.sign(&handle2, b"test data").await.unwrap();
assert_eq!(
sig.as_bytes(),
sig2.as_bytes(),
"deterministic signing must produce same signature"
);
}
#[tokio::test]
async fn reopen_with_wrong_passphrase_fails() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("keys.scp");
let custody = FileKeyCustody::new(&path, "correct").unwrap();
custody.generate_keypair(KeyType::Ed25519).await.unwrap();
drop(custody);
let custody2 = FileKeyCustody::new(&path, "wrong").unwrap();
let handle = KeyHandle::new(1);
let result = custody2.sign(&handle, b"data").await;
assert!(
result.is_err(),
"wrong passphrase must cause decryption failure"
);
match result.unwrap_err() {
PlatformError::CustodyError(msg) => {
assert!(
msg.contains("decryption failed"),
"error must mention decryption: {msg}"
);
}
other => panic!("expected CustodyError, got {other:?}"),
}
}
#[tokio::test]
async fn key_file_does_not_contain_raw_private_key() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("keys.scp");
let custody = FileKeyCustody::new(&path, "passphrase").unwrap();
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let pubkey = custody.public_key(&handle).await.unwrap();
let file_data = std::fs::read(&path).unwrap();
let pub_bytes = pubkey.as_bytes();
let mut found_raw_key = false;
for window in file_data.windows(32) {
let candidate = SigningKey::from_bytes(window.try_into().unwrap_or(&[0u8; 32]));
if candidate.verifying_key().to_bytes() == <[u8; 32]>::try_from(pub_bytes).unwrap() {
found_raw_key = true;
break;
}
}
assert!(
!found_raw_key,
"key file must not contain the raw private key bytes"
);
}
#[tokio::test]
async fn destroy_key_makes_operations_fail() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
custody.sign(&handle, b"test").await.unwrap();
custody.destroy_key(&handle).await.unwrap();
assert!(custody.sign(&handle, b"test").await.is_err());
assert!(custody.public_key(&handle).await.is_err());
assert!(custody.destroy_key(&handle).await.is_err());
}
#[tokio::test]
async fn sign_with_x25519_key_fails() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let handle = custody.generate_keypair(KeyType::X25519).await.unwrap();
let result = custody.sign(&handle, b"data").await;
assert!(result.is_err());
match result.unwrap_err() {
PlatformError::WrongKeyType { expected, actual } => {
assert_eq!(expected, KeyType::Ed25519);
assert_eq!(actual, KeyType::X25519);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn dh_agree_with_ed25519_key_fails() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let result = custody.dh_agree(&handle, &[0u8; 32]).await;
assert!(result.is_err());
match result.unwrap_err() {
PlatformError::WrongKeyType { expected, actual } => {
assert_eq!(expected, KeyType::X25519);
assert_eq!(actual, KeyType::Ed25519);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn custody_type_returns_software() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
assert_eq!(custody.custody_type(&handle), CustodyType::Software);
}
#[tokio::test]
async fn derive_pseudonym_is_deterministic() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let first = custody.derive_pseudonym(&handle, b"ctx").await.unwrap();
let second = custody.derive_pseudonym(&handle, b"ctx").await.unwrap();
assert_eq!(first.public_key.as_bytes(), second.public_key.as_bytes());
}
#[tokio::test]
async fn derive_pseudonym_key_can_sign() {
let dir = TempDir::new().unwrap();
let custody = make_custody(&dir, "pw");
let handle = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let pseudo = custody.derive_pseudonym(&handle, b"ctx").await.unwrap();
let sig = custody.sign(&pseudo.key_handle, b"msg").await.unwrap();
assert_eq!(sig.as_bytes().len(), 64);
let pk_bytes: [u8; 32] = pseudo.public_key.as_bytes().try_into().unwrap();
let vk = VerifyingKey::from_bytes(&pk_bytes).unwrap();
let sig_bytes: [u8; 64] = sig.as_bytes().try_into().unwrap();
let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);
assert!(ed25519_dalek::Verifier::verify(&vk, b"msg", &signature).is_ok());
}
#[tokio::test]
async fn multiple_keys_roundtrip() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("keys.scp");
let passphrase = "multi-key";
let custody = FileKeyCustody::new(&path, passphrase).unwrap();
let h1 = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let h2 = custody.generate_keypair(KeyType::X25519).await.unwrap();
let h3 = custody.generate_keypair(KeyType::Ed25519).await.unwrap();
let pk1 = custody.public_key(&h1).await.unwrap();
let pk2 = custody.public_key(&h2).await.unwrap();
let pk3 = custody.public_key(&h3).await.unwrap();
drop(custody);
let custody2 = FileKeyCustody::new(&path, passphrase).unwrap();
let rh1 = KeyHandle::new(1);
let rh2 = KeyHandle::new(2);
let rh3 = KeyHandle::new(3);
assert_eq!(
custody2.public_key(&rh1).await.unwrap().as_bytes(),
pk1.as_bytes()
);
assert_eq!(
custody2.public_key(&rh2).await.unwrap().as_bytes(),
pk2.as_bytes()
);
assert_eq!(
custody2.public_key(&rh3).await.unwrap().as_bytes(),
pk3.as_bytes()
);
}
}