Files
friendolls/src-tauri/src/keypair/mod.rs
T

190 lines
5.4 KiB
Rust

use std::fmt;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::{Signer, SigningKey};
use sqlx::FromRow;
use tauri::{AppHandle, Manager, State};
use crate::db::{self, AppDatabase};
#[cfg(test)]
mod tests;
const KEYPAIR_ID: i64 = 1;
#[derive(Debug, FromRow)]
struct StoredKeypair {
public_key: String,
secret_key: String,
}
#[derive(Debug)]
pub enum KeypairError {
Database(sqlx::Error),
Randomness(getrandom::Error),
InvalidEncoding(base64::DecodeError),
InvalidKeyLength {
key: &'static str,
expected: usize,
actual: usize,
},
PublicKeyMismatch,
MissingAfterPrepare,
}
impl fmt::Display for KeypairError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Database(error) => write!(formatter, "keypair database error: {error}"),
Self::Randomness(error) => write!(formatter, "could not generate a keypair: {error}"),
Self::InvalidEncoding(error) => {
write!(formatter, "stored keypair is not valid base64: {error}")
}
Self::InvalidKeyLength {
key,
expected,
actual,
} => write!(
formatter,
"stored {key} has an invalid length: expected {expected} bytes, got {actual}"
),
Self::PublicKeyMismatch => {
write!(formatter, "stored public key does not match the secret key")
}
Self::MissingAfterPrepare => {
write!(formatter, "keypair was not present after preparation")
}
}
}
}
impl std::error::Error for KeypairError {}
impl From<sqlx::Error> for KeypairError {
fn from(error: sqlx::Error) -> Self {
Self::Database(error)
}
}
impl From<getrandom::Error> for KeypairError {
fn from(error: getrandom::Error) -> Self {
Self::Randomness(error)
}
}
impl From<base64::DecodeError> for KeypairError {
fn from(error: base64::DecodeError) -> Self {
Self::InvalidEncoding(error)
}
}
pub async fn init(handle: &AppHandle) -> Result<(), KeypairError> {
let keypair = prepare(&handle.state::<db::AppDatabase>()).await?;
handle.manage(keypair);
Ok(())
}
fn decode_key<const LENGTH: usize>(
encoded: &str,
key: &'static str,
) -> Result<[u8; LENGTH], KeypairError> {
let decoded = URL_SAFE_NO_PAD.decode(encoded)?;
let actual = decoded.len();
decoded
.try_into()
.map_err(|_| KeypairError::InvalidKeyLength {
key,
expected: LENGTH,
actual,
})
}
impl StoredKeypair {
fn signing_key(&self) -> Result<SigningKey, KeypairError> {
let secret_key = decode_key::<32>(&self.secret_key, "secret key")?;
let public_key = decode_key::<32>(&self.public_key, "public key")?;
let signing_key = SigningKey::from_bytes(&secret_key);
if signing_key.verifying_key().to_bytes() != public_key {
return Err(KeypairError::PublicKeyMismatch);
}
Ok(signing_key)
}
}
#[derive(Clone)]
pub struct AppKeypair {
signing_key: SigningKey,
public_key: String,
}
impl AppKeypair {
fn from_stored(keypair: StoredKeypair) -> Result<Self, KeypairError> {
let signing_key = keypair.signing_key()?;
Ok(Self {
signing_key,
public_key: keypair.public_key,
})
}
pub fn public_key(&self) -> &str {
&self.public_key
}
#[allow(dead_code)] // Exercised by tests until the WebSocket sender is connected.
pub fn sign(&self, payload: &[u8]) -> String {
let signature = self.signing_key.sign(payload);
URL_SAFE_NO_PAD.encode(signature.to_bytes())
}
}
async fn stored(database: &AppDatabase) -> Result<Option<StoredKeypair>, KeypairError> {
sqlx::query_as::<_, StoredKeypair>("SELECT public_key, secret_key FROM keypair WHERE id = ?1")
.bind(KEYPAIR_ID)
.fetch_optional(database.pool())
.await
.map_err(Into::into)
}
/// Creates and persists the app identity when it does not exist yet.
///
/// The returned keypair contains the stable entity ID shared with friends and
/// the decoded signing key. The secret key is intentionally stored unencrypted
/// in SQLite and loaded into memory once during app startup.
pub async fn prepare(database: &AppDatabase) -> Result<AppKeypair, KeypairError> {
if let Some(keypair) = stored(database).await? {
return AppKeypair::from_stored(keypair);
}
let mut secret_key = [0_u8; 32];
getrandom::fill(&mut secret_key)?;
let signing_key = SigningKey::from_bytes(&secret_key);
let public_key = URL_SAFE_NO_PAD.encode(signing_key.verifying_key().as_bytes());
let secret_key = URL_SAFE_NO_PAD.encode(secret_key);
// Another caller may prepare the singleton concurrently. In that case the
// already-persisted identity remains authoritative.
sqlx::query(
"INSERT INTO keypair (id, public_key, secret_key) VALUES (?1, ?2, ?3) \
ON CONFLICT(id) DO NOTHING",
)
.bind(KEYPAIR_ID)
.bind(public_key)
.bind(secret_key)
.execute(database.pool())
.await?;
let keypair = stored(database)
.await?
.ok_or(KeypairError::MissingAfterPrepare)?;
AppKeypair::from_stored(keypair)
}
#[tauri::command]
#[specta::specta]
pub fn get_public_key(keypair: State<'_, AppKeypair>) -> String {
keypair.public_key().to_owned()
}