mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-22 21:49:28 -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",
|
"indoc",
|
||||||
"josekit",
|
"josekit",
|
||||||
"migrations",
|
"migrations",
|
||||||
|
"scrypt",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sequoia-openpgp",
|
"sequoia-openpgp",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
@ -1446,9 +1449,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.0"
|
version = "2.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf2a4f498956c7723dc280afc6a37d0dec50b39a29e232c6187ce4503703e8c2"
|
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.3",
|
"hashbrown 0.14.3",
|
||||||
|
@ -2055,6 +2058,16 @@ version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
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]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -2534,6 +2547,15 @@ version = "1.0.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "salsa20"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -2558,6 +2580,18 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
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]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -2787,9 +2821,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.196"
|
version = "1.0.197"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
@ -2806,9 +2840,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.196"
|
version = "1.0.197"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2817,9 +2851,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.112"
|
version = "1.0.114"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed"
|
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
|
|
@ -31,6 +31,9 @@ sequoia-openpgp = { version = "1.18.0", optional = true }
|
||||||
josekit = { version = "0.8.5" }
|
josekit = { version = "0.8.5" }
|
||||||
aes-gcm = "0.10.3"
|
aes-gcm = "0.10.3"
|
||||||
migrations = { path = "crates/migrations" }
|
migrations = { path = "crates/migrations" }
|
||||||
|
scrypt = "0.11.0"
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
serde_json = "1.0.114"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
gpg-compat = ["dep:gpgme", "dep:sequoia-openpgp"]
|
gpg-compat = ["dep:gpgme", "dep:sequoia-openpgp"]
|
||||||
|
|
|
@ -122,7 +122,7 @@ impl AspKey {
|
||||||
Self::from_jwk(key_pair.to_jwk_key_pair())
|
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:
|
// 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
|
// 1. Get the josekit EcKeyPair from the Jwk
|
||||||
// 2. Convert that to a PEM private key
|
// 2. Convert that to a PEM private key
|
||||||
|
@ -142,9 +142,8 @@ impl AspKey {
|
||||||
let pem_private = key_pair.to_pem_private_key();
|
let pem_private = key_pair.to_pem_private_key();
|
||||||
let pkey = PKey::private_key_from_pem(&pem_private)?;
|
let pkey = PKey::private_key_from_pem(&pem_private)?;
|
||||||
let pkcs8 = pkey.as_ref().private_key_to_pkcs8()?;
|
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> {
|
pub fn generate(key_type: AspKeyType) -> Result<Self, anyhow::Error> {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{Aead, KeyInit},
|
aead::{rand_core::RngCore, Aead, KeyInit, OsRng},
|
||||||
Aes256Gcm, Key,
|
Aes256Gcm, Key,
|
||||||
};
|
};
|
||||||
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
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 argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
use asp::keys::AspKey;
|
use asp::keys::AspKey;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use data_encoding::BASE64_NOPAD;
|
use data_encoding::{BASE64, BASE64_NOPAD};
|
||||||
use dialoguer::{theme::ColorfulTheme, Password};
|
use dialoguer::{theme::ColorfulTheme, Password};
|
||||||
use indoc::writedoc;
|
use indoc::writedoc;
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
@ -19,12 +20,35 @@ use crate::{
|
||||||
entities::prelude::*,
|
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)]
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
pub enum KeyExportFormat {
|
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")]
|
#[clap(alias = "PKCS#8")]
|
||||||
PKCS8,
|
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,
|
Jwk,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,10 +116,52 @@ impl AspmSubcommand for KeysExportCommand {
|
||||||
let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?;
|
let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?;
|
||||||
|
|
||||||
let export = match self.format {
|
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::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!(
|
eprintln!(
|
||||||
|
|
|
@ -158,48 +158,42 @@ impl AspmSubcommand for KeysImportCommand {
|
||||||
let public_part = key.mpis();
|
let public_part = key.mpis();
|
||||||
|
|
||||||
let (curve, public, secret) = match key.secret() {
|
let (curve, public, secret) = match key.secret() {
|
||||||
key::SecretKeyMaterial::Unencrypted(data) => {
|
key::SecretKeyMaterial::Unencrypted(data) => data.map(|secret_material| {
|
||||||
data.map(|secret_material| {
|
let (curve, public, private) = match secret_material {
|
||||||
let (curve, public, private) = match secret_material {
|
mpi::SecretKeyMaterial::ECDSA { scalar } => {
|
||||||
mpi::SecretKeyMaterial::ECDSA { scalar } => {
|
let mpi::PublicKey::ECDSA {
|
||||||
let mpi::PublicKey::ECDSA {
|
curve,
|
||||||
curve,
|
q: public_part,
|
||||||
q: public_part,
|
} = public_part
|
||||||
} = public_part
|
else {
|
||||||
else {
|
unreachable!()
|
||||||
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")
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
(curve, public_part, scalar)
|
||||||
curve.clone(),
|
}
|
||||||
public.clone(),
|
mpi::SecretKeyMaterial::EdDSA { scalar } => {
|
||||||
private.clone()
|
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!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
AspKey::from_jwk(
|
AspKey::from_jwk(
|
||||||
Jwk::from_map({
|
Jwk::from_map({
|
||||||
let mut map = josekit::Map::new();
|
let mut map = josekit::Map::new();
|
||||||
|
|
||||||
match curve {
|
match curve {
|
||||||
Curve::Ed25519 => {
|
Curve::Ed25519 => {
|
||||||
map.insert("kty".to_string(), "OKP".into());
|
map.insert("kty".to_string(), "OKP".into());
|
||||||
|
@ -237,7 +231,8 @@ impl AspmSubcommand for KeysImportCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
map
|
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