use void_core::collab::{Identity, NostrSecretKey, RecipientSecretKey, SigningSecretKey};
#[cfg(not(test))]
const SERVICE: &str = "void";
const ENVELOPE_VERSION: u8 = 0x01;
const FLAG_HAS_NOSTR: u8 = 0x01;
#[cfg(test)]
mod mock_store {
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
static STORE: OnceLock<Mutex<HashMap<String, Vec<u8>>>> = OnceLock::new();
fn store() -> &'static Mutex<HashMap<String, Vec<u8>>> {
STORE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn get(key: &str) -> Option<Vec<u8>> {
store().lock().unwrap().get(key).cloned()
}
pub fn set(key: &str, value: Vec<u8>) {
store().lock().unwrap().insert(key.to_string(), value);
}
pub fn remove(key: &str) -> bool {
store().lock().unwrap().remove(key).is_some()
}
}
#[cfg(not(test))]
fn warn_keyring_unavailable(err: &keyring::Error) {
use std::sync::OnceLock;
static WARNED: OnceLock<()> = OnceLock::new();
WARNED.get_or_init(|| {
eprintln!(
"void: keyring unavailable ({}), PIN will be required each time",
err
);
});
}
fn build_cache_envelope(identity: &Identity) -> Vec<u8> {
let signing = identity.signing_key_bytes();
let recipient = identity.recipient_key_bytes();
let nostr = identity.nostr_key_bytes();
let has_nostr = nostr.is_some();
let flags = if has_nostr { FLAG_HAS_NOSTR } else { 0 };
let capacity = 2 + 32 + 32 + if has_nostr { 32 } else { 0 };
let mut buf = Vec::with_capacity(capacity);
buf.push(ENVELOPE_VERSION);
buf.push(flags);
buf.extend_from_slice(signing.as_bytes());
buf.extend_from_slice(recipient.as_bytes());
if let Some(ref nostr_key) = nostr {
buf.extend_from_slice(nostr_key.as_bytes());
}
buf
}
fn parse_cache_envelope(data: &[u8]) -> Option<Identity> {
if data.len() < 2 {
return None;
}
if data[0] != ENVELOPE_VERSION {
return None;
}
let flags = data[1];
let has_nostr = flags & FLAG_HAS_NOSTR != 0;
let expected_len = 2 + 32 + 32 + if has_nostr { 32 } else { 0 };
if data.len() != expected_len {
return None;
}
let mut signing_bytes = [0u8; 32];
let mut recipient_bytes = [0u8; 32];
signing_bytes.copy_from_slice(&data[2..34]);
recipient_bytes.copy_from_slice(&data[34..66]);
let signing = SigningSecretKey::from_bytes(signing_bytes);
let recipient = RecipientSecretKey::from_bytes(recipient_bytes);
if has_nostr {
let mut nostr_bytes = [0u8; 32];
nostr_bytes.copy_from_slice(&data[66..98]);
let nostr = NostrSecretKey::from_bytes(nostr_bytes);
Some(Identity::from_bytes_with_nostr(&signing, &recipient, nostr))
} else {
Some(Identity::from_bytes(&signing, &recipient))
}
}
pub fn load_cached_keys(signing_pubkey_hex: &str) -> Option<Identity> {
#[cfg(test)]
{
return mock_store::get(signing_pubkey_hex).and_then(|data| parse_cache_envelope(&data));
}
#[cfg(not(test))]
{
let entry = match keyring::Entry::new(SERVICE, signing_pubkey_hex) {
Ok(e) => e,
Err(err) => {
warn_keyring_unavailable(&err);
return None;
}
};
match entry.get_secret() {
Ok(data) => {
let result = parse_cache_envelope(&data);
if result.is_none() {
eprintln!(
"void: keyring cache corrupt (len={}), re-prompting",
data.len()
);
}
result
}
Err(keyring::Error::NoEntry) => None,
Err(err) => {
warn_keyring_unavailable(&err);
None
}
}
}
}
pub fn cache_keys(signing_pubkey_hex: &str, identity: &Identity) {
let envelope = build_cache_envelope(identity);
#[cfg(test)]
{
mock_store::set(signing_pubkey_hex, envelope);
return;
}
#[cfg(not(test))]
{
let entry = match keyring::Entry::new(SERVICE, signing_pubkey_hex) {
Ok(e) => e,
Err(err) => {
warn_keyring_unavailable(&err);
return;
}
};
if let Err(err) = entry.set_secret(&envelope) {
warn_keyring_unavailable(&err);
}
}
}
pub fn clear_cached_keys(signing_pubkey_hex: &str) -> bool {
#[cfg(test)]
{
return mock_store::remove(signing_pubkey_hex);
}
#[cfg(not(test))]
{
let entry = match keyring::Entry::new(SERVICE, signing_pubkey_hex) {
Ok(e) => e,
Err(_) => return false,
};
match entry.delete_credential() {
Ok(()) => true,
Err(keyring::Error::NoEntry) => false,
Err(_) => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_envelope_roundtrip_without_nostr() {
let signing = SigningSecretKey::from_bytes([0xaa; 32]);
let recipient = RecipientSecretKey::from_bytes([0xbb; 32]);
let identity = Identity::from_bytes(&signing, &recipient);
let envelope = build_cache_envelope(&identity);
assert_eq!(envelope.len(), 66);
assert_eq!(envelope[0], ENVELOPE_VERSION);
assert_eq!(envelope[1], 0);
let restored = parse_cache_envelope(&envelope).expect("should parse");
assert_eq!(identity.signing_pubkey(), restored.signing_pubkey());
assert_eq!(identity.recipient_pubkey(), restored.recipient_pubkey());
}
#[test]
fn test_envelope_roundtrip_with_nostr() {
let signing = SigningSecretKey::from_bytes([0xaa; 32]);
let recipient = RecipientSecretKey::from_bytes([0xbb; 32]);
let nostr = NostrSecretKey::from_bytes([0xcc; 32]);
let identity = Identity::from_bytes_with_nostr(&signing, &recipient, nostr);
let envelope = build_cache_envelope(&identity);
assert_eq!(envelope.len(), 98);
assert_eq!(envelope[0], ENVELOPE_VERSION);
assert_eq!(envelope[1], FLAG_HAS_NOSTR);
let restored = parse_cache_envelope(&envelope).expect("should parse");
assert_eq!(identity.signing_pubkey(), restored.signing_pubkey());
assert_eq!(identity.recipient_pubkey(), restored.recipient_pubkey());
assert_eq!(identity.nostr_pubkey(), restored.nostr_pubkey());
}
#[test]
fn test_mock_store_roundtrip() {
let signing = SigningSecretKey::from_bytes([0x11; 32]);
let recipient = RecipientSecretKey::from_bytes([0x22; 32]);
let identity = Identity::from_bytes(&signing, &recipient);
let key = "test_mock_store_roundtrip";
cache_keys(key, &identity);
let loaded = load_cached_keys(key).expect("should find cached identity");
assert_eq!(identity.signing_pubkey(), loaded.signing_pubkey());
assert_eq!(identity.recipient_pubkey(), loaded.recipient_pubkey());
assert!(clear_cached_keys(key));
assert!(load_cached_keys(key).is_none());
}
#[test]
fn test_parse_empty_data() {
assert!(parse_cache_envelope(&[]).is_none());
}
#[test]
fn test_parse_wrong_version() {
let mut data = vec![0x99, 0x00];
data.extend_from_slice(&[0u8; 64]);
assert!(parse_cache_envelope(&data).is_none());
}
#[test]
fn test_parse_wrong_length() {
let data = vec![ENVELOPE_VERSION, 0x00, 0x01, 0x02];
assert!(parse_cache_envelope(&data).is_none());
}
#[test]
fn test_parse_nostr_flag_but_missing_data() {
let mut data = vec![ENVELOPE_VERSION, FLAG_HAS_NOSTR];
data.extend_from_slice(&[0u8; 64]);
assert!(parse_cache_envelope(&data).is_none());
}
}