mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 15:59:29 -07:00
Add scrypt-pkcs#8 export for ASP tool compatibility
This commit is contained in:
parent
a04e048f7c
commit
4a68f17aa4
5 changed files with 151 additions and 54 deletions
50
Cargo.lock
generated
50
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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")?,
|
||||
)?
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue