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

Add scrypt-pkcs#8 export for ASP tool compatibility

This commit is contained in:
Tyler Beckman 2024-02-19 21:32:54 -07:00
parent a04e048f7c
commit 4a68f17aa4
Signed by: Ty
GPG key ID: 2813440C772555A4
5 changed files with 151 additions and 54 deletions

50
Cargo.lock generated
View file

@ -243,8 +243,11 @@ dependencies = [
"indoc",
"josekit",
"migrations",
"scrypt",
"sea-orm",
"sequoia-openpgp",
"serde",
"serde_json",
"thiserror",
"tokio",
]
@ -1446,9 +1449,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.0"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf2a4f498956c7723dc280afc6a37d0dec50b39a29e232c6187ce4503703e8c2"
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
@ -2055,6 +2058,16 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -2534,6 +2547,15 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -2558,6 +2580,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"password-hash",
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "sct"
version = "0.7.1"
@ -2787,9 +2821,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
@ -2806,9 +2840,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
@ -2817,9 +2851,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.112"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"indexmap",
"itoa",

View file

@ -31,6 +31,9 @@ sequoia-openpgp = { version = "1.18.0", optional = true }
josekit = { version = "0.8.5" }
aes-gcm = "0.10.3"
migrations = { path = "crates/migrations" }
scrypt = "0.11.0"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
[features]
gpg-compat = ["dep:gpgme", "dep:sequoia-openpgp"]

View file

@ -122,7 +122,7 @@ impl AspKey {
Self::from_jwk(key_pair.to_jwk_key_pair())
}
pub fn into_pkcs8(&self) -> Result<String, anyhow::Error> {
pub fn into_pkcs8(&self) -> Result<Vec<u8>, anyhow::Error> {
// This was done because I couldn't find an easy way to get a PKCS#8 encoded private key from the josekit Jwk struct, so I did the following as a workaround, directly using the openssl library:
// 1. Get the josekit EcKeyPair from the Jwk
// 2. Convert that to a PEM private key
@ -142,9 +142,8 @@ impl AspKey {
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()?;
let encoded = BASE64_NOPAD.encode(&pkcs8);
Ok(encoded)
Ok(pkcs8)
}
pub fn generate(key_type: AspKeyType) -> Result<Self, anyhow::Error> {

View file

@ -1,5 +1,5 @@
use aes_gcm::{
aead::{Aead, KeyInit},
aead::{rand_core::RngCore, Aead, KeyInit, OsRng},
Aes256Gcm, Key,
};
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
@ -7,10 +7,11 @@ 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 data_encoding::{BASE64, BASE64_NOPAD};
use dialoguer::{theme::ColorfulTheme, Password};
use indoc::writedoc;
use josekit::jwk::Jwk;
use serde::Serialize;
use std::io::Write;
@ -19,12 +20,35 @@ use crate::{
entities::prelude::*,
};
#[derive(Serialize)]
#[serde(tag = "alg", rename = "scrypt")]
pub struct ASPToolScryptExport {
#[serde(rename = "prm")]
parameters: ASPToolScryptExportParam,
#[serde(rename = "slt")]
salt: String,
#[serde(rename = "key")]
encrypted_key: String,
}
#[derive(Serialize)]
pub struct ASPToolScryptExportParam {
#[serde(rename = "N")]
cost: u64,
#[serde(rename = "r")]
block_size: u32,
#[serde(rename = "p")]
parallelism: u32,
}
#[derive(ValueEnum, Debug, Clone)]
pub enum KeyExportFormat {
/// An unencrypted PKCS#8 format. This is the format used by the asp.keyoxide.org web tool.
/// An unencrypted PKCS#8 format.
#[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.
/// An scrypt-encrypted PKCS#8 key, formatted for the asp.keyoxide.org web tool. The password on the exported key is the same as the one used for ASPM.
ASPTool,
/// An unencrypted raw JSON Web Key (JWK) format.
Jwk,
}
@ -92,10 +116,52 @@ impl AspmSubcommand for KeysExportCommand {
let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?;
let export = match self.format {
KeyExportFormat::PKCS8 => decrypted
.into_pkcs8()
.context("Unable to convert key into PKCS#8 format")?,
KeyExportFormat::Jwk => decrypted.jwk.to_string(),
KeyExportFormat::PKCS8 => BASE64_NOPAD.encode(
decrypted
.into_pkcs8()
.context("Unable to convert key into PKCS#8 format")?
.as_slice(),
),
KeyExportFormat::ASPTool => {
let pkcs8 = decrypted
.into_pkcs8()
.context("Unable to convert key into PKCS#8 format")?;
let mut salt = [0u8; 16];
OsRng.fill_bytes(&mut salt);
let mut derived_key = vec![0u8; pkcs8.len()];
scrypt::scrypt(
key_password.as_bytes(),
&salt,
&scrypt::Params::recommended(),
&mut derived_key,
)
.context("Unable to derive PKCS#8 encryption key with scrypt")?;
let encrypted_key = BASE64.encode(
pkcs8
.into_iter()
.enumerate()
.map(|(i, byte)| byte ^ derived_key[i])
.collect::<Vec<_>>()
.as_slice(),
);
BASE64.encode(
serde_json::to_string(&ASPToolScryptExport {
parameters: ASPToolScryptExportParam {
cost: 1 << scrypt::Params::RECOMMENDED_LOG_N,
block_size: scrypt::Params::RECOMMENDED_R,
parallelism: scrypt::Params::RECOMMENDED_P,
},
salt: BASE64.encode(&salt),
encrypted_key,
})
.context("Unable to construct JSON form of ASPTool export")?
.as_bytes(),
)
}
};
eprintln!(

View file

@ -158,48 +158,42 @@ impl AspmSubcommand for KeysImportCommand {
let public_part = key.mpis();
let (curve, public, secret) = match key.secret() {
key::SecretKeyMaterial::Unencrypted(data) => {
data.map(|secret_material| {
let (curve, public, private) = match secret_material {
mpi::SecretKeyMaterial::ECDSA { scalar } => {
let mpi::PublicKey::ECDSA {
curve,
q: public_part,
} = public_part
else {
unreachable!()
};
(curve, public_part, scalar)
},
mpi::SecretKeyMaterial::EdDSA { scalar } => {
let mpi::PublicKey::EdDSA {
curve,
q: public_part,
} = public_part
else {
unreachable!()
};
(curve, public_part, scalar)
},
_ => bail!("Invalid PGP key type, must be either P-256 or Ed25519")
};
key::SecretKeyMaterial::Unencrypted(data) => data.map(|secret_material| {
let (curve, public, private) = match secret_material {
mpi::SecretKeyMaterial::ECDSA { scalar } => {
let mpi::PublicKey::ECDSA {
curve,
q: public_part,
} = public_part
else {
unreachable!()
};
Ok((
curve.clone(),
public.clone(),
private.clone()
))
})?
}
(curve, public_part, scalar)
}
mpi::SecretKeyMaterial::EdDSA { scalar } => {
let mpi::PublicKey::EdDSA {
curve,
q: public_part,
} = public_part
else {
unreachable!()
};
(curve, public_part, scalar)
}
_ => bail!("Invalid PGP key type, must be either P-256 or Ed25519"),
};
Ok((curve.clone(), public.clone(), private.clone()))
})?,
_ => unreachable!(),
};
AspKey::from_jwk(
Jwk::from_map({
let mut map = josekit::Map::new();
match curve {
Curve::Ed25519 => {
map.insert("kty".to_string(), "OKP".into());
@ -237,7 +231,8 @@ impl AspmSubcommand for KeysImportCommand {
}
map
}).context("Unable to construct Jwk representation of PGP key")?
})
.context("Unable to construct Jwk representation of PGP key")?,
)?
}
};