mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 20:39:29 -07:00
Add 3 subcommands for storing keys, add database framework
This commit is contained in:
parent
556d9ed4fc
commit
46fa993f45
11 changed files with 635 additions and 24 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"anstyle",
|
||||
"Aspe",
|
||||
"Aspm",
|
||||
"josekit",
|
||||
|
|
132
Cargo.lock
generated
132
Cargo.lock
generated
|
@ -84,6 +84,17 @@ dependencies = [
|
|||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.4"
|
||||
|
@ -113,9 +124,16 @@ dependencies = [
|
|||
name = "aspm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anyhow",
|
||||
"app_dirs2",
|
||||
"argon2",
|
||||
"asp",
|
||||
"clap",
|
||||
"data-encoding",
|
||||
"dialoguer",
|
||||
"indoc",
|
||||
"redb",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -131,12 +149,27 @@ version = "0.21.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
@ -234,6 +267,19 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
|
@ -284,6 +330,18 @@ version = "2.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87"
|
||||
dependencies = [
|
||||
"console",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -292,6 +350,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -303,6 +362,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.32"
|
||||
|
@ -621,6 +686,12 @@ dependencies = [
|
|||
"hashbrown 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f2cb48b81b1dc9f39676bf99f5499babfec7cd8fe14307f7b3d747208fb5690"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
@ -854,6 +925,17 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.0"
|
||||
|
@ -893,6 +975,16 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713eccf888fb05f1a96eb78c0dbc51907fee42b3377272dc902eb38985f418d5"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.28"
|
||||
|
@ -932,6 +1024,16 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redb"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1770bc0931171df3ced2adc9fd72d59cb47a4dc693d184c73cd382067f6ff44e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.3.5"
|
||||
|
@ -1121,6 +1223,12 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.8"
|
||||
|
@ -1146,6 +1254,12 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.22"
|
||||
|
@ -1157,6 +1271,12 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.6.0"
|
||||
|
@ -1333,6 +1453,12 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.4.0"
|
||||
|
@ -1659,3 +1785,9 @@ checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee"
|
|||
dependencies = [
|
||||
"home",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||
|
|
|
@ -13,5 +13,12 @@ members = ["crates/*"]
|
|||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
app_dirs2 = "2.5.5"
|
||||
clap = { version = "4.3.9", features = ["derive"] }
|
||||
clap = { version = "4.3.9", features = ["derive", "unstable-styles", "env"] }
|
||||
thiserror = "1.0.40"
|
||||
asp = { path = "crates/asp" }
|
||||
indoc = "2.0.1"
|
||||
anstyle = "1.0.1"
|
||||
redb = "1.0.2"
|
||||
dialoguer = { version = "0.10.4", features = ["password"] }
|
||||
argon2 = { version = "0.5.0", features = ["std"] }
|
||||
data-encoding = "2.4.0"
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use anyhow::Context;
|
||||
use anyhow::{bail, Context};
|
||||
use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD, BASE64_NOPAD};
|
||||
use josekit::{
|
||||
jwe::JweHeader,
|
||||
jwe::{self, alg::aesgcmkw::AesgcmkwJweAlgorithm::A256gcmkw},
|
||||
jwk::{alg::ec::EcKeyPair, Jwk},
|
||||
jwk::{
|
||||
alg::{ec::EcKeyPair, ed::EdKeyPair},
|
||||
Jwk, KeyPair,
|
||||
},
|
||||
jws::ES256,
|
||||
};
|
||||
use openssl::pkey::PKey;
|
||||
|
@ -49,7 +52,17 @@ impl JwtExt for Jwk {
|
|||
// 2. Convert that to a PEM private key
|
||||
// 3. Get the openssl Pkey struct by loading the PRM private key
|
||||
// 4. Convert that Pkey into the PKCS#8 encoded private key (and then base64 encode it)
|
||||
let key_pair = EcKeyPair::from_jwk(self)?;
|
||||
let key_pair: Box<dyn KeyPair> = match self.key_type() {
|
||||
"EC" => match self.curve() {
|
||||
Some("p-256") => Box::new(EcKeyPair::from_jwk(self)?),
|
||||
_ => bail!("Unsupported curve type"),
|
||||
},
|
||||
"OKP" => match self.curve() {
|
||||
Some("Ed25519") => Box::new(EdKeyPair::from_jwk(self)?),
|
||||
_ => bail!("Unsupported curve type"),
|
||||
},
|
||||
_ => bail!("Unsupported key type"),
|
||||
};
|
||||
let pem_private = key_pair.to_pem_private_key();
|
||||
let pkey = PKey::private_key_from_pem(&pem_private)?;
|
||||
let pkcs8 = pkey.as_ref().private_key_to_pkcs8()?;
|
||||
|
|
88
src/commands/keys/export.rs
Normal file
88
src/commands/keys/export.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use anyhow::{anyhow, Context};
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||
use asp::keys::AspKey;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use data_encoding::BASE64_NOPAD;
|
||||
use dialoguer::{theme::ColorfulTheme, Password};
|
||||
use redb::ReadableTable;
|
||||
|
||||
use crate::{commands::AspmSubcommand, db::KEYS_TABLE};
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
pub enum KeyExportFormat {
|
||||
/// An encrypted JWE format, the same way it is stored internally. This is likely only compatible with this tool specifically due to how it is decrypted.
|
||||
Encrypted,
|
||||
/// An unencrypted PKCS#8 format. This is the format used by the asp.keyoxide.org web tool.
|
||||
#[clap(alias = "PKCS#8")]
|
||||
PKCS8,
|
||||
/// An unencrypted raw JSON Web Key format. This is likely not the best output to use unless you know what you are doing.
|
||||
Jwk,
|
||||
}
|
||||
|
||||
/// Exports a saved key, specified by its fingerprint, into multiple formats. Run this command with `--help` in order to see a list of the possible formats, and explanations for all of them.
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct KeysExportCommand {
|
||||
/// The format to export the key into. All of these formats can then be correctly imported back into this tool, but only some are encrypted, so keep that in mind when handling the exported keys.
|
||||
#[clap(value_enum, ignore_case = true)]
|
||||
format: KeyExportFormat,
|
||||
/// The fingerprint of the key to export. This can be obtained with the `keys list` command.
|
||||
fingerprint: String,
|
||||
}
|
||||
|
||||
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)
|
||||
.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")
|
||||
.interact()
|
||||
.context("Unable to prompt on stderr")
|
||||
})?;
|
||||
|
||||
let argon_salt = SaltString::from_b64(
|
||||
&BASE64_NOPAD.encode(self.fingerprint.to_uppercase().as_bytes()),
|
||||
)
|
||||
.context("Unable to decode argon2 salt")?;
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(key_password.as_bytes(), &argon_salt)
|
||||
.or(Err(anyhow!("Unable to derive encryption key")))?;
|
||||
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) {
|
||||
let export = match self.format {
|
||||
KeyExportFormat::Encrypted => decrypted
|
||||
.export_encrypted(aes_key)
|
||||
.context("Unable to convert key into encrypted format")?,
|
||||
KeyExportFormat::PKCS8 => decrypted
|
||||
.into_pkcs8()
|
||||
.context("Unable to convert key into PKCS#8 format")?,
|
||||
KeyExportFormat::Jwk => decrypted.jwk.to_string(),
|
||||
};
|
||||
|
||||
println!("{export}")
|
||||
} else {
|
||||
eprintln!("There was an error decrypting the key, please make sure the password you entered was correct");
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"The specified key fingerprint {fingerprint} does not exist",
|
||||
fingerprint = self.fingerprint
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
106
src/commands/keys/generate.rs
Normal file
106
src/commands/keys/generate.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use anyhow::{anyhow, 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 crate::{
|
||||
commands::AspmSubcommand,
|
||||
db::{KeysTableValue, KEYS_TABLE},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
|
||||
pub enum KeyGenerationType {
|
||||
Ed25519,
|
||||
ES256,
|
||||
}
|
||||
|
||||
/// Allows you to generate a new key for use with ariadne signature profiles. This will be stored in the ASPM data directory, but can then be exported with the `keys export` command.
|
||||
/// A password needs to be provided, so it should either be given via the KEY_PASSWORD environment variable, or omitted in order to prompt interactively.
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct KeysGenerateCommand {
|
||||
/// The type of key to generate. This must either be Ed25519, or ES256. This argument is case-insensitive.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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")?;
|
||||
|
||||
let key_password = std::env::var("KEY_PASSWORD").or_else(|_| {
|
||||
Password::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Please enter a password to store and encrypt the key with")
|
||||
.with_confirmation(
|
||||
"Please confirm the password",
|
||||
"The two inputs did not match!",
|
||||
)
|
||||
.interact()
|
||||
.context("Unable to prompt on stderr")
|
||||
})?;
|
||||
|
||||
let key = AspKey::generate(match self.key_type {
|
||||
KeyGenerationType::Ed25519 => AspKeyType::EdDSA,
|
||||
KeyGenerationType::ES256 => AspKeyType::ES256,
|
||||
})
|
||||
.context("Key generation failed for an unknown reason")?;
|
||||
|
||||
let argon_salt =
|
||||
SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes()))
|
||||
.context("Unable to derive argon2 salt")?;
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(key_password.as_bytes(), &argon_salt)
|
||||
.or(Err(anyhow!("Unable to derive encryption key")))?;
|
||||
let aes_key = hash.hash.context("Unable to derive encryption key")?;
|
||||
let aes_key = &aes_key.as_bytes()[0..32];
|
||||
|
||||
let encrypted = key
|
||||
.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")?;
|
||||
}
|
||||
txn.commit().context("Unable to write to database")?;
|
||||
|
||||
printdoc! {
|
||||
"
|
||||
Successfully generated a new key!
|
||||
Fingerprint: {fpr}
|
||||
Type: {type:?}
|
||||
",
|
||||
fpr = key.fingerprint,
|
||||
r#type = self.key_type
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
46
src/commands/keys/list.rs
Normal file
46
src/commands/keys/list.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use anstyle::{AnsiColor, Style as Anstyle};
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use redb::ReadableTable;
|
||||
|
||||
use crate::{commands::AspmSubcommand, db::KEYS_TABLE};
|
||||
|
||||
/// A command to list all saved keys, along with their fingerprints and types
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct KeysListCommand;
|
||||
|
||||
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();
|
||||
|
||||
let header_style = Anstyle::new()
|
||||
.bold()
|
||||
.underline()
|
||||
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightGreen)));
|
||||
println!(
|
||||
"{style}Saved keys ({n} total):{reset}\n\n",
|
||||
style = header_style.render(),
|
||||
n = entries.len(),
|
||||
reset = header_style.render_reset()
|
||||
);
|
||||
for entry in entries.iter() {
|
||||
if let Ok((fingerprint, value)) = entry {
|
||||
let value = value.value();
|
||||
println!(
|
||||
"{alias}: {fingerprint}/${key_type:?}",
|
||||
fingerprint = fingerprint.value(),
|
||||
key_type = value.key_type,
|
||||
alias = value.alias
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
19
src/commands/keys/mod.rs
Normal file
19
src/commands/keys/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
pub mod export;
|
||||
pub mod generate;
|
||||
pub mod list;
|
||||
|
||||
/// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles.
|
||||
#[derive(Parser)]
|
||||
pub struct KeysSubcommand {
|
||||
#[command(subcommand)]
|
||||
pub subcommand: KeysSubcommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum KeysSubcommands {
|
||||
Generate(generate::KeysGenerateCommand),
|
||||
List(list::KeysListCommand),
|
||||
Export(export::KeysExportCommand),
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
use crate::AspmConfig;
|
||||
pub mod keys;
|
||||
|
||||
pub trait AspmSubcommand {
|
||||
fn execute(&self, config: AspmConfig) -> u8;
|
||||
use clap::Parser;
|
||||
|
||||
use crate::AspmState;
|
||||
|
||||
pub trait AspmSubcommand: Parser {
|
||||
fn execute(&self, state: AspmState) -> Result<(), anyhow::Error>;
|
||||
}
|
||||
|
|
128
src/db.rs
Normal file
128
src/db.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
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::EdDSA,
|
||||
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::EdDSA => 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");
|
95
src/main.rs
95
src/main.rs
|
@ -1,7 +1,12 @@
|
|||
mod commands;
|
||||
mod db;
|
||||
|
||||
use anstyle::{AnsiColor, Color as AnstyleColor, Style as Anstyle};
|
||||
use anyhow::Context;
|
||||
use app_dirs2::{AppDataType, AppInfo};
|
||||
use clap::Parser;
|
||||
use clap::{Parser, Subcommand};
|
||||
use commands::{keys::KeysSubcommands, AspmSubcommand};
|
||||
use redb::Database;
|
||||
use thiserror::Error;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
@ -12,13 +17,21 @@ const APP_INFO: AppInfo = AppInfo {
|
|||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AspmConfig {
|
||||
data_dir: PathBuf,
|
||||
pub struct AspmState {
|
||||
pub data_dir: PathBuf,
|
||||
pub db: Database,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(
|
||||
version,
|
||||
about,
|
||||
long_about,
|
||||
styles = AspmCommand::styles()
|
||||
)]
|
||||
struct AspmCommand {
|
||||
#[clap(long_about)]
|
||||
|
||||
/// The directory to use, overriding OS defaults. This can also be set with the ASPM_DATA_DIR environment variable.
|
||||
/// The order of precedence is:
|
||||
/// 1. Command line flag
|
||||
|
@ -26,37 +39,91 @@ struct AspmCommand {
|
|||
/// 3. OS default
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
data_dir: Option<PathBuf>,
|
||||
/// The subcommand to use
|
||||
#[command(subcommand)]
|
||||
subcommand: AspmSubcommands,
|
||||
}
|
||||
|
||||
impl AspmCommand {
|
||||
fn styles() -> clap::builder::Styles {
|
||||
clap::builder::Styles::styled()
|
||||
.header(
|
||||
Anstyle::new()
|
||||
.bold()
|
||||
.underline()
|
||||
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightMagenta))),
|
||||
)
|
||||
.literal(
|
||||
Anstyle::new()
|
||||
.bold()
|
||||
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightGreen))),
|
||||
)
|
||||
.usage(
|
||||
Anstyle::new()
|
||||
.bold()
|
||||
.underline()
|
||||
.fg_color(Some(AnstyleColor::Ansi(AnsiColor::BrightMagenta))),
|
||||
)
|
||||
.placeholder(Anstyle::new().fg_color(Some(AnstyleColor::Ansi(AnsiColor::Cyan))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum AspmSubcommands {
|
||||
Keys(commands::keys::KeysSubcommand),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match cli() {
|
||||
Err(e) => eprintln!("An error occurred while running that command:\n{e}"),
|
||||
Err(e) => {
|
||||
eprintln!("An error occurred while running that command:\n{e}");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn cli() -> Result<(), AspmError> {
|
||||
fn cli() -> Result<(), anyhow::Error> {
|
||||
let parsed = AspmCommand::parse();
|
||||
|
||||
let config = AspmConfig {
|
||||
data_dir: parsed.data_dir.unwrap_or(
|
||||
// Check the data dir (read and write)
|
||||
let data_dir = parsed.data_dir.unwrap_or(
|
||||
std::env::var("ASPM_DATA_DIR").map(|s| s.into()).unwrap_or(
|
||||
app_dirs2::get_app_root(AppDataType::UserData, &APP_INFO)
|
||||
.map_err(|e| AspmError::UnknownError(e.into()))?,
|
||||
),
|
||||
),
|
||||
};
|
||||
// Check the dir (read and write)
|
||||
std::fs::read_dir(&config.data_dir).or(Err(AspmError::DataDirReadError))?;
|
||||
std::fs::create_dir_all(&config.data_dir).or(Err(AspmError::DataDirCreateError))?;
|
||||
);
|
||||
std::fs::create_dir_all(&data_dir).or(Err(AspmError::DataDirCreateError))?;
|
||||
std::fs::read_dir(&data_dir).or(Err(AspmError::DataDirReadError))?;
|
||||
|
||||
Ok(())
|
||||
// Construct the database in the dir
|
||||
let mut db = Database::create({
|
||||
let mut new = data_dir.clone();
|
||||
new.push("db.redb");
|
||||
new
|
||||
})
|
||||
.or(Err(AspmError::DatabaseCreateError))?;
|
||||
db.check_integrity()
|
||||
.context("Unable to check database integrity")?;
|
||||
|
||||
// Make the state
|
||||
let state = AspmState { data_dir, db };
|
||||
|
||||
// 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
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")]
|
||||
|
|
Loading…
Reference in a new issue