use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::SigningKey;
use openmls::prelude::{BasicCredential, CredentialWithKey, OpenMlsProvider as _, SignatureScheme};
use openmls_basic_credential::SignatureKeyPair;
use openmls_memory_storage::MemoryStorage;
use openmls_rust_crypto::OpenMlsRustCrypto;
use parley_core::{AgentPubkey, NetworkId};
use parley_mls::PartyKeys;
use serde::{Deserialize, Serialize};
pub fn resolve_home(explicit: Option<PathBuf>) -> Result<PathBuf> {
if let Some(p) = explicit {
return Ok(p);
}
if let Ok(env) = std::env::var("PARLEY_HOME") {
return Ok(PathBuf::from(env));
}
let home = std::env::var("HOME").context("$HOME not set")?;
Ok(PathBuf::from(home).join(".parley"))
}
pub fn ensure_dir(home: &Path) -> Result<()> {
fs::create_dir_all(home).with_context(|| format!("create {home:?}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut perms = fs::metadata(home)?.permissions();
perms.set_mode(0o700);
fs::set_permissions(home, perms).ok();
}
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Identity {
pub secret_b64: String,
pub public_b64: String,
}
impl Identity {
pub fn generate() -> Self {
use rand::RngCore as _;
let mut secret = [0u8; 32];
rand::thread_rng().fill_bytes(&mut secret);
let signing = SigningKey::from_bytes(&secret);
let public = signing.verifying_key().to_bytes();
Self {
secret_b64: URL_SAFE_NO_PAD.encode(secret),
public_b64: URL_SAFE_NO_PAD.encode(public),
}
}
pub fn secret_bytes(&self) -> Result<[u8; 32]> {
let v = URL_SAFE_NO_PAD.decode(&self.secret_b64)?;
<[u8; 32]>::try_from(v.as_slice())
.map_err(|_| anyhow::anyhow!("identity.secret must be 32 bytes"))
}
pub fn public_bytes(&self) -> Result<[u8; 32]> {
let v = URL_SAFE_NO_PAD.decode(&self.public_b64)?;
<[u8; 32]>::try_from(v.as_slice())
.map_err(|_| anyhow::anyhow!("identity.public must be 32 bytes"))
}
pub fn pubkey(&self) -> Result<AgentPubkey> {
Ok(AgentPubkey::from_bytes(self.public_bytes()?))
}
pub fn signing_key(&self) -> Result<SigningKey> {
Ok(SigningKey::from_bytes(&self.secret_bytes()?))
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ServerConfig {
pub server_url: String,
pub network_id: String,
}
impl ServerConfig {
pub fn network(&self) -> Result<NetworkId> {
NetworkId::new(&self.network_id).context("network_id")
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Friends {
#[serde(flatten)]
pub by_name: HashMap<String, String>,
}
impl Friends {
pub fn resolve(&self, name_or_pubkey: &str) -> Option<String> {
if let Some(pk) = self.by_name.get(name_or_pubkey) {
return Some(pk.clone());
}
if name_or_pubkey.len() == 43 {
return Some(name_or_pubkey.to_owned());
}
None
}
pub fn label(&self, pubkey: &str) -> Option<&str> {
self.by_name
.iter()
.find_map(|(k, v)| (v == pubkey).then_some(k.as_str()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelEntry {
pub channel_id: String,
pub mls_group_id: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Channels {
#[serde(flatten)]
pub by_friend: HashMap<String, ChannelEntry>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct LastSeen {
#[serde(flatten)]
pub by_channel: HashMap<String, u64>,
}
fn read_json<T: serde::de::DeserializeOwned + Default>(path: &Path) -> Result<T> {
if !path.exists() {
return Ok(T::default());
}
let s = fs::read_to_string(path).with_context(|| format!("read {path:?}"))?;
serde_json::from_str(&s).with_context(|| format!("parse {path:?}"))
}
fn write_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
let s = serde_json::to_string_pretty(value)?;
let tmp = path.with_extension("tmp");
fs::write(&tmp, s)?;
fs::rename(&tmp, path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(path, perms).ok();
}
Ok(())
}
pub fn identity_path(home: &Path) -> PathBuf {
home.join("identity.json")
}
pub fn server_path(home: &Path) -> PathBuf {
home.join("server.json")
}
pub fn friends_path(home: &Path) -> PathBuf {
home.join("friends.json")
}
pub fn channels_path(home: &Path) -> PathBuf {
home.join("channels.json")
}
pub fn last_seen_path(home: &Path) -> PathBuf {
home.join("last_seen.json")
}
pub fn mls_storage_path(home: &Path) -> PathBuf {
home.join("mls_storage.json")
}
pub fn load_identity(home: &Path) -> Result<Identity> {
let path = identity_path(home);
if !path.exists() {
anyhow::bail!("no identity at {path:?}. Run `parley init` first.");
}
let s = fs::read_to_string(&path)?;
Ok(serde_json::from_str(&s)?)
}
pub fn save_identity(home: &Path, ident: &Identity) -> Result<()> {
write_json(&identity_path(home), ident)
}
pub fn load_server(home: &Path) -> Result<ServerConfig> {
read_json(&server_path(home))
}
pub fn save_server(home: &Path, cfg: &ServerConfig) -> Result<()> {
write_json(&server_path(home), cfg)
}
pub fn load_friends(home: &Path) -> Result<Friends> {
read_json(&friends_path(home))
}
pub fn save_friends(home: &Path, friends: &Friends) -> Result<()> {
write_json(&friends_path(home), friends)
}
pub fn load_channels(home: &Path) -> Result<Channels> {
read_json(&channels_path(home))
}
pub fn save_channels(home: &Path, channels: &Channels) -> Result<()> {
write_json(&channels_path(home), channels)
}
pub fn load_last_seen(home: &Path) -> Result<LastSeen> {
read_json(&last_seen_path(home))
}
pub fn save_last_seen(home: &Path, seen: &LastSeen) -> Result<()> {
write_json(&last_seen_path(home), seen)
}
pub fn load_party_keys(home: &Path, ident: &Identity) -> Result<PartyKeys> {
let provider = OpenMlsRustCrypto::default();
let mls_path = mls_storage_path(home);
if mls_path.exists() {
let file = fs::File::open(&mls_path).with_context(|| format!("open {mls_path:?}"))?;
let storage_ref = provider.storage();
let mut tmp = MemoryStorage::default();
tmp.load_from_file(&file).map_err(anyhow::Error::msg)?;
copy_storage(&tmp, storage_ref).map_err(anyhow::Error::msg)?;
}
let secret = ident.secret_bytes()?;
let public = ident.public_bytes()?;
let signature_keys =
SignatureKeyPair::from_raw(SignatureScheme::ED25519, secret.to_vec(), public.to_vec());
signature_keys
.store(provider.storage())
.map_err(|e| anyhow::anyhow!("store sig keys: {e:?}"))?;
let credential = BasicCredential::new(public.to_vec());
let credential_with_key = CredentialWithKey {
credential: credential.into(),
signature_key: signature_keys.public().into(),
};
Ok(PartyKeys {
provider,
signature_keys,
credential_with_key,
})
}
pub fn save_party_keys(home: &Path, keys: &PartyKeys) -> Result<()> {
let path = mls_storage_path(home);
let file = fs::File::create(&path).with_context(|| format!("create {path:?}"))?;
keys.provider
.storage()
.save_to_file(&file)
.map_err(anyhow::Error::msg)?;
Ok(())
}
fn copy_storage(from: &MemoryStorage, into: &MemoryStorage) -> Result<(), String> {
let src = from.values.read().map_err(|e| e.to_string())?;
let mut dst = into.values.write().map_err(|e| e.to_string())?;
for (k, v) in src.iter() {
dst.insert(k.clone(), v.clone());
}
Ok(())
}