1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-22 20:39:29 -07:00

Migrate to sqlite+sea_orm

This commit is contained in:
TymanWasTaken 2023-07-01 13:10:32 -04:00
parent 5390a9389a
commit 19c42f6138
Signed by: Ty
GPG key ID: 2813440C772555A4
14 changed files with 1423 additions and 246 deletions

1252
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,10 @@ thiserror = "1.0.40"
asp = { path = "crates/asp" }
indoc = "2.0.1"
anstyle = "1.0.1"
redb = "1.0.3"
dialoguer = { version = "0.10.4", features = ["password"] }
argon2 = { version = "0.5.0", features = ["std"] }
data-encoding = "2.4.0"
sea-orm = { version = "0.11.3", features = ["sqlx-sqlite", "runtime-tokio-native-tls"] }
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
sea-orm-migration = "0.11.3"
async-trait = "0.1.68"

View file

@ -1,3 +1,4 @@
use anyhow::bail;
use josekit::{
jwk::{
alg::{ec::EcCurve, ed::EdCurve},
@ -19,6 +20,26 @@ pub enum AspKeyType {
ES256,
}
impl Into<i32> for AspKeyType {
fn into(self) -> i32 {
match self {
Self::Ed25519 => 0,
Self::ES256 => 1,
}
}
}
impl TryFrom<i32> for AspKeyType {
type Error = anyhow::Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Ed25519),
1 => Ok(Self::ES256),
_ => bail!("Invalid index for AspKeyType"),
}
}
}
/// A struct representing a key that can be used to create profiles or ASPE requests
#[derive(Debug)]
pub struct AspKey {

View file

@ -4,9 +4,9 @@ use asp::keys::AspKey;
use clap::{Parser, ValueEnum};
use data_encoding::BASE64_NOPAD;
use dialoguer::{theme::ColorfulTheme, Password};
use redb::ReadableTable;
use sea_orm::EntityTrait;
use crate::{commands::AspmSubcommand, db::KEYS_TABLE};
use crate::{commands::AspmSubcommand, entities::keys};
#[derive(ValueEnum, Debug, Clone)]
pub enum KeyExportFormat {
@ -29,20 +29,16 @@ pub struct KeysExportCommand {
fingerprint: String,
}
#[async_trait::async_trait]
impl AspmSubcommand for KeysExportCommand {
fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let txn = state
.db
.begin_read()
.context("Unable to start db read transaction")?;
let table = txn.open_table(KEYS_TABLE)?;
let entry = table
.get(&*self.fingerprint)
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
// Fetch key from db
let entry = keys::Entity::find_by_id(&self.fingerprint)
.one(&state.db)
.await
.context("Unable to fetch key from database")?;
if let Some(entry) = entry {
let value = entry.value();
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
Password::with_theme(&ColorfulTheme::default())
.with_prompt("Please enter a password to decrypt the key with")
@ -61,7 +57,7 @@ impl AspmSubcommand for KeysExportCommand {
let aes_key = hash.hash.context("Unable to derive encryption key")?;
let aes_key = &aes_key.as_bytes()[0..32];
if let Ok(decrypted) = AspKey::from_encrypted(aes_key, &value.key) {
if let Ok(decrypted) = AspKey::from_encrypted(aes_key, &entry.encrypted) {
let export = match self.format {
KeyExportFormat::Encrypted => decrypted
.export_encrypted(aes_key)

View file

@ -1,15 +1,13 @@
use anyhow::{anyhow, Context};
use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use asp::keys::{AspKey, AspKeyType};
use clap::{Parser, ValueEnum};
use data_encoding::BASE64_NOPAD;
use dialoguer::{theme::ColorfulTheme, Input, Password};
use indoc::printdoc;
use sea_orm::{ActiveValue, EntityTrait};
use crate::{
commands::AspmSubcommand,
db::{KeysTableValue, KEYS_TABLE},
};
use crate::{commands::AspmSubcommand, entities::keys};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum KeyGenerationType {
@ -25,19 +23,23 @@ pub struct KeysGenerateCommand {
/// It doesn't really matter that much which one is used, as they both work fine, but Ed25519 is used as a safe default.
#[clap(value_enum, default_value_t = KeyGenerationType::Ed25519, long_about, ignore_case = true)]
key_type: KeyGenerationType,
/// Tha alias of the key to generate. This can be anything, and it can also be omitted to prompt interactively. This has no purpose other than providing a way to nicely name keys, rather than having to remember a fingerprint.
#[arg(short = 'n', long)]
key_alias: Option<String>,
}
#[async_trait::async_trait]
impl AspmSubcommand for KeysGenerateCommand {
fn execute(&self, config: crate::AspmState) -> Result<(), anyhow::Error> {
let alias = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Please enter an alias to give to this key")
.allow_empty(false)
.validate_with(|input: &String| match input.as_bytes().len() <= 255 {
true => Ok(()),
false => Err("Alias must not be longer than 255 characters!"),
})
.interact()
.context("Unable to prompt on stderr")?;
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let alias = if let Some(alias) = &self.key_alias {
alias.clone()
} else {
Input::with_theme(&ColorfulTheme::default())
.with_prompt("Please enter an alias to give to this key")
.allow_empty(false)
.interact()
.context("Unable to prompt on stderr")?
};
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
Password::with_theme(&ColorfulTheme::default())
@ -70,26 +72,20 @@ impl AspmSubcommand for KeysGenerateCommand {
.export_encrypted(aes_key)
.context("Unable to derive the encryption key")?;
let txn = config
.db
.begin_write()
.context("Unable to open the database for writing")?;
{
let mut table = txn.open_table(KEYS_TABLE)?;
table
.insert(
key.fingerprint.as_str(),
KeysTableValue {
key: encrypted,
alias: alias
.try_into()
.context("alias must be less than or equal to 255 characters")?,
key_type: key.key_type,
},
)
.context("Unable to write to database")?;
// Write to db
let entry = keys::ActiveModel {
fingerprint: ActiveValue::Set(key.fingerprint.clone()),
key_type: ActiveValue::Set(key.key_type.into()),
alias: ActiveValue::Set(alias),
encrypted: ActiveValue::Set(encrypted),
};
let res = keys::Entity::insert(entry)
.exec(&state.db)
.await
.context("Unable to add key to database")?;
if res.last_insert_id != key.fingerprint {
bail!("The key was unable to be saved to the database")
}
txn.commit().context("Unable to write to database")?;
printdoc! {
"

View file

@ -1,24 +1,23 @@
use anstyle::{AnsiColor, Reset, Style as Anstyle};
use anyhow::Context;
use asp::keys::AspKeyType;
use clap::Parser;
use indoc::printdoc;
use redb::ReadableTable;
use sea_orm::EntityTrait;
use crate::{commands::AspmSubcommand, db::KEYS_TABLE};
use crate::{commands::AspmSubcommand, entities::keys};
/// A command to list all saved keys, along with their fingerprints and types
#[derive(Parser, Debug)]
pub struct KeysListCommand;
#[async_trait::async_trait]
impl AspmSubcommand for KeysListCommand {
fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let txn = state
.db
.begin_read()
.context("Unable to start db read transaction")?;
let table = txn.open_table(KEYS_TABLE)?;
let iter = table.iter().context("Unable to read table entries")?;
let entries: Vec<_> = iter.collect();
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let entries = keys::Entity::find()
.all(&state.db)
.await
.context("Unable to read keys from database")?;
// Construct styles
let reset = Reset::default().render();
@ -44,18 +43,15 @@ impl AspmSubcommand for KeysListCommand {
n = entries.len(),
);
for entry in entries.iter() {
if let Ok((fingerprint, value)) = entry {
let value = value.value();
printdoc! {
"
{alias_style}{alias}:{reset}
{key_style}Fingerprint{reset} {value_style}{fingerprint}{reset}
{key_style}Key Type{reset} {value_style}{key_type:?}{reset}
",
fingerprint = fingerprint.value(),
key_type = value.key_type,
alias = value.alias
}
printdoc! {
"
{alias_style}{alias}:{reset}
{key_style}Fingerprint{reset} {value_style}{fingerprint}{reset}
{key_style}Key Type{reset} {value_style}{key_type:?}{reset}
",
fingerprint = entry.fingerprint,
key_type = TryInto::<AspKeyType>::try_into(entry.key_type).context("Unable to get key type from database")?,
alias = entry.alias
}
}

View file

@ -4,6 +4,7 @@ use clap::Parser;
use crate::AspmState;
#[async_trait::async_trait]
pub trait AspmSubcommand: Parser {
fn execute(&self, state: AspmState) -> Result<(), anyhow::Error>;
async fn execute(&self, state: AspmState) -> Result<(), anyhow::Error>;
}

128
src/db.rs
View file

@ -1,128 +0,0 @@
use std::fmt::Display;
use anyhow::bail;
use asp::keys::AspKeyType;
use redb::{RedbValue, TableDefinition};
#[derive(Debug)]
pub struct KeysTableValue {
pub alias: BoundedString,
pub key: String,
pub key_type: AspKeyType,
}
#[derive(Debug, Clone)]
pub struct BoundedString {
inner: String,
}
impl Display for BoundedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
impl TryFrom<String> for BoundedString {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.as_bytes().len() > 255 {
bail!("Value was too long");
}
Ok(Self { inner: value })
}
}
impl Into<Vec<u8>> for BoundedString {
fn into(self) -> Vec<u8> {
self.inner.as_bytes().to_vec()
}
}
impl RedbValue for KeysTableValue {
type SelfType<'a> = KeysTableValue;
/// The first u8 is a length, specifying how long the alias is (therefore the alias can only be a max of 255 bytes)
/// The next bytes (bounded by the length of the first u8) is the alias
/// The next byte is a u8 representing the type of key
/// The rest of the bytes are the key
type AsBytes<'a> = Vec<u8>;
fn fixed_width() -> Option<usize> {
None
}
// This does panic, but I don't know if there is a better way to do it. If you manage to insert bad data into the table, good job, that is your problem
fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
where
Self: 'a,
{
let mut iter = data.iter().map(|b| *b);
let alias_length: usize = iter
.next()
.expect("parsing key table value failed: unable to get first byte")
.into();
// Get alias
let alias_bytes = {
let mut vec = Vec::new();
for _ in 0..alias_length {
vec.push(
iter.next()
.expect("parsing key table value failed: ran out of bytes on alias"),
);
}
vec
};
if alias_bytes.len() != alias_length {
panic!("parsing key table value failed: unable to get full alias");
};
// Get the type of key
let key_type_byte = iter
.next()
.expect("parsing key table value failed: unable to get the key type");
// Get key
let key_bytes = iter.collect::<Vec<u8>>();
// Assemble bytes into strings and struct
Self {
key: String::from_utf8(key_bytes)
.expect("parsing key table value failed: unable to decode key into string"),
alias: String::from_utf8(alias_bytes)
.expect("parsing key table value failed: unable to decode alias into string")
.try_into()
.unwrap(),
key_type: match key_type_byte {
0 => AspKeyType::Ed25519,
1 => AspKeyType::ES256,
_ => panic!("parsing key table value failed: unknown key type byte found"),
},
}
}
fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
where
Self: 'a,
Self: 'b,
{
let key_bytes = value.key.as_bytes();
let alias_bytes: Vec<u8> = value.alias.clone().into();
let mut serialized: Vec<u8> = vec![];
serialized.push(alias_bytes.len().try_into().unwrap()); // Add the first byte (alias length)
serialized.extend_from_slice(alias_bytes.as_slice()); // Add the alias bytes
serialized.push(match value.key_type {
AspKeyType::Ed25519 => 0,
AspKeyType::ES256 => 1,
}); // Add the key type byte
serialized.extend_from_slice(key_bytes); // Add the rest of the bytes, all of which are the key
serialized
}
fn type_name() -> redb::TypeName {
redb::TypeName::new("aspm::KeysTableValue")
}
}
/// A table to contain saved keys
pub const KEYS_TABLE: TableDefinition<&str, KeysTableValue> = TableDefinition::new("claims");

18
src/entities/keys.rs Normal file
View file

@ -0,0 +1,18 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "keys")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub fingerprint: String,
pub key_type: i32,
pub alias: String,
pub encrypted: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

5
src/entities/mod.rs Normal file
View file

@ -0,0 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
pub mod prelude;
pub mod keys;

3
src/entities/prelude.rs Normal file
View file

@ -0,0 +1,3 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
pub use super::keys::Entity as Keys;

View file

@ -1,12 +1,15 @@
mod commands;
mod db;
mod entities;
mod migrations;
use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle};
use anyhow::Context;
use app_dirs2::{AppDataType, AppInfo};
use clap::{Parser, Subcommand};
use commands::{keys::KeysSubcommands, AspmSubcommand};
use redb::Database;
use migrations::Migrator;
use sea_orm::{Database, DatabaseConnection};
use sea_orm_migration::{MigratorTrait, SchemaManager};
use thiserror::Error;
use std::path::PathBuf;
@ -15,11 +18,12 @@ const APP_INFO: AppInfo = AppInfo {
name: env!("CARGO_CRATE_NAME"),
author: "Ty",
};
const DATABASE_URL: &str = "sqlite://DB_PATH?mode=rwc";
#[derive(Debug)]
pub struct AspmState {
pub data_dir: PathBuf,
pub db: Database,
pub db: DatabaseConnection,
}
#[derive(Parser)]
@ -73,8 +77,9 @@ pub enum AspmSubcommands {
Keys(commands::keys::KeysSubcommand),
}
fn main() {
match cli() {
#[tokio::main]
async fn main() {
match cli().await {
Err(e) => {
eprintln!("An error occurred while running that command:\n{e}");
}
@ -82,7 +87,7 @@ fn main() {
}
}
fn cli() -> Result<(), anyhow::Error> {
async fn cli() -> Result<(), anyhow::Error> {
let parsed = AspmCommand::parse();
// Check the data dir (read and write)
@ -96,14 +101,24 @@ fn cli() -> Result<(), anyhow::Error> {
std::fs::read_dir(&data_dir).or(Err(AspmError::DataDirReadError))?;
// Construct the database in the dir
let mut db = Database::create({
let db = Database::connect(DATABASE_URL.replace("DB_PATH", &{
let mut new = data_dir.clone();
new.push("db.redb");
new
})
.or(Err(AspmError::DatabaseCreateError))?;
db.check_integrity()
.context("Unable to check database integrity")?;
new.push("db.sqlite");
new.to_str()
.context("Unable to parse database path into string")
.map(|s| s.to_string())
}?))
.await
.context("Unable to open database")?;
let schema_manager = SchemaManager::new(&db);
Migrator::up(&db, None)
.await
.context("Unable to migrate database")?;
assert!(schema_manager
.has_table("keys")
.await
.context("Unable to check database for keys table")?);
// Make the state
let state = AspmState { data_dir, db };
@ -111,9 +126,9 @@ fn cli() -> Result<(), anyhow::Error> {
// Call the subcommand
match &parsed.subcommand {
AspmSubcommands::Keys(subcommand) => match &subcommand.subcommand {
KeysSubcommands::Generate(subcommand) => subcommand.execute(state),
KeysSubcommands::List(subcommand) => subcommand.execute(state),
KeysSubcommands::Export(subcommand) => subcommand.execute(state),
KeysSubcommands::Generate(subcommand) => subcommand.execute(state).await,
KeysSubcommands::List(subcommand) => subcommand.execute(state).await,
KeysSubcommands::Export(subcommand) => subcommand.execute(state).await,
},
}
}
@ -122,8 +137,6 @@ fn cli() -> Result<(), anyhow::Error> {
enum AspmError {
#[error("The data directory was unable to be created, is it correct, and does the current user have permission to create it?")]
DataDirCreateError,
#[error("The database was unable to be created, is the data dir correct, and does the current user have permission to modify it?")]
DatabaseCreateError,
#[error("The data directory was unable to be read, is it correct, and does the current user have permission to read it?")]
DataDirReadError,
#[error("An unknown internal error occurred, please report this to the developer")]

View file

@ -0,0 +1,48 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m_20230701_000001_create_keys_table"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
// Define how to apply this migration: Create the Bakery table.
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Keys::Table)
.col(
ColumnDef::new(Keys::Fingerprint)
.string_len(26)
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Keys::KeyType).integer().not_null())
.col(ColumnDef::new(Keys::Alias).string().not_null())
.col(ColumnDef::new(Keys::Encrypted).string().not_null())
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Keys::Table).to_owned())
.await
}
}
#[derive(Iden)]
enum Keys {
Table,
Fingerprint,
KeyType,
Alias,
Encrypted,
}

11
src/migrations/mod.rs Normal file
View file

@ -0,0 +1,11 @@
mod m_20230701_000001_create_keys_table;
use sea_orm_migration::prelude::*;
pub struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m_20230701_000001_create_keys_table::Migration)]
}
}