use crate::keystore::{EncryptedEntry, Error, KeyMetadata, Keystore, Result};
use sqlx::SqlitePool;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use std::path::Path;
use std::str::FromStr;
use tokio::sync::OnceCell;
pub struct SqliteKeystore {
options: SqliteConnectOptions,
pool_options: SqlitePoolOptions,
pool: OnceCell<SqlitePool>,
}
impl Default for SqliteKeystore {
fn default() -> Self {
Self::in_memory().expect("valid configuration for in memory database")
}
}
impl SqliteKeystore {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let options = SqliteConnectOptions::new()
.filename(path)
.create_if_missing(true);
Ok(Self::from_options(options, SqlitePoolOptions::new()))
}
pub fn in_memory() -> Result<Self> {
let options =
SqliteConnectOptions::from_str("sqlite:file::memory:?cache=shared").map_err(backend)?;
let pool_options = SqlitePoolOptions::new()
.min_connections(1)
.idle_timeout(None)
.max_lifetime(None);
Ok(Self::from_options(options, pool_options))
}
fn from_options(options: SqliteConnectOptions, pool_options: SqlitePoolOptions) -> Self {
Self {
options,
pool_options,
pool: OnceCell::new(),
}
}
async fn pool(&self) -> Result<&SqlitePool> {
self.pool
.get_or_try_init(|| async {
let pool = self
.pool_options
.clone()
.connect_with(self.options.clone())
.await
.map_err(backend)?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS keys (label TEXT PRIMARY KEY, data BLOB NOT NULL)",
)
.execute(&pool)
.await
.map_err(backend)?;
Ok(pool)
})
.await
}
}
impl Keystore for SqliteKeystore {
async fn put(&self, entry: EncryptedEntry) -> Result<()> {
let pool = self.pool().await?;
let bytes = cbor4ii::serde::to_vec(Vec::new(), &entry).map_err(backend)?;
sqlx::query("INSERT OR REPLACE INTO keys (label, data) VALUES (?1, ?2)")
.bind(entry.metadata.label)
.bind(bytes)
.execute(pool)
.await
.map_err(backend)?;
Ok(())
}
async fn put_many(&self, entries: Vec<EncryptedEntry>) -> Result<()> {
let pool = self.pool().await?;
let mut tx = pool.begin().await.map_err(backend)?;
for entry in entries {
let bytes = cbor4ii::serde::to_vec(Vec::new(), &entry).map_err(backend)?;
sqlx::query("INSERT OR REPLACE INTO keys (label, data) VALUES (?1, ?2)")
.bind(entry.metadata.label)
.bind(bytes)
.execute(&mut *tx)
.await
.map_err(backend)?;
}
tx.commit().await.map_err(backend)?;
Ok(())
}
async fn get(&self, label: &str) -> Result<Option<EncryptedEntry>> {
let pool = self.pool().await?;
let row: Option<(Vec<u8>,)> = sqlx::query_as("SELECT data FROM keys WHERE label = ?1")
.bind(label)
.fetch_optional(pool)
.await
.map_err(backend)?;
match row {
Some((bytes,)) => Ok(Some(cbor4ii::serde::from_slice(&bytes).map_err(backend)?)),
None => Ok(None),
}
}
async fn list(&self) -> Result<Vec<KeyMetadata>> {
let pool = self.pool().await?;
let rows: Vec<(Vec<u8>,)> = sqlx::query_as("SELECT data FROM keys")
.fetch_all(pool)
.await
.map_err(backend)?;
let mut metadata = Vec::with_capacity(rows.len());
for (bytes,) in rows {
if let Ok(entry) = cbor4ii::serde::from_slice::<EncryptedEntry>(&bytes) {
metadata.push(entry.metadata);
}
}
Ok(metadata)
}
async fn remove(&self, label: &str) -> Result<bool> {
let pool = self.pool().await?;
let result = sqlx::query("DELETE FROM keys WHERE label = ?1")
.bind(label)
.execute(pool)
.await
.map_err(backend)?;
Ok(result.rows_affected() > 0)
}
}
fn backend<E: Into<Box<dyn std::error::Error + Send + Sync>>>(err: E) -> Error {
Error::Backend(std::io::Error::other(err))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "ed25519")]
#[tokio::test]
async fn persists_across_instances() {
use crate::keystore::{Keychain, generate_key};
use libp2p::identity::Keypair;
let path = std::env::temp_dir().join(format!("connexa-sqlite-{}.db", std::process::id()));
let _ = tokio::fs::remove_file(&path).await;
let key = generate_key();
let keypair = Keypair::generate_ed25519();
Keychain::new_with_store(key, SqliteKeystore::new(&path).unwrap())
.insert("identity", &keypair)
.await
.unwrap();
let reopened = Keychain::new_with_store(key, SqliteKeystore::new(&path).unwrap());
assert_eq!(
reopened
.get("identity")
.await
.unwrap()
.public()
.to_peer_id(),
keypair.public().to_peer_id()
);
assert_eq!(reopened.list().await.unwrap().len(), 1);
assert!(reopened.remove("identity").await.unwrap());
assert!(matches!(
reopened.get("identity").await,
Err(Error::NotFound(_))
));
let _ = tokio::fs::remove_file(&path).await;
}
}