mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-23 01:19:28 -07:00
Remove JWE formatting
This commit is contained in:
parent
620a8632bb
commit
424d3ce4fc
12 changed files with 244 additions and 254 deletions
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -17,6 +17,16 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aes"
|
name = "aes"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -28,6 +38,20 @@ dependencies = [
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes-gcm"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"ghash",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.7.7"
|
version = "0.7.7"
|
||||||
|
@ -195,6 +219,7 @@ dependencies = [
|
||||||
name = "aspm"
|
name = "aspm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app_dirs2",
|
"app_dirs2",
|
||||||
|
@ -748,6 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -761,6 +787,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.1"
|
version = "4.1.1"
|
||||||
|
@ -1258,6 +1293,16 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
|
||||||
|
dependencies = [
|
||||||
|
"opaque-debug",
|
||||||
|
"polyval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
|
@ -1934,6 +1979,12 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.63"
|
version = "0.10.63"
|
||||||
|
@ -2187,6 +2238,18 @@ version = "3.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c"
|
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyval"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -3587,6 +3650,16 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
|
@ -31,6 +31,7 @@ gpgme = { version = "0.11.0", optional = true }
|
||||||
pgp = { version = "0.10.2", optional = true }
|
pgp = { version = "0.10.2", optional = true }
|
||||||
josekit = { version = "0.8.5", optional = true }
|
josekit = { version = "0.8.5", optional = true }
|
||||||
elliptic-curve = { version = "0.13.8", optional = true }
|
elliptic-curve = { version = "0.13.8", optional = true }
|
||||||
|
aes-gcm = "0.10.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
gpg-compat = ["dep:gpgme", "dep:pgp", "dep:josekit", "dep:elliptic-curve"]
|
gpg-compat = ["dep:gpgme", "dep:pgp", "dep:josekit", "dep:elliptic-curve"]
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
use anyhow::bail;
|
use anyhow::{bail, Context};
|
||||||
|
use data_encoding::{BASE32_NOPAD, BASE64_NOPAD};
|
||||||
use josekit::{
|
use josekit::{
|
||||||
jwk::{
|
jwk::{
|
||||||
alg::{ec::EcCurve, ed::EdCurve},
|
alg::{
|
||||||
Jwk,
|
ec::{EcCurve, EcKeyPair},
|
||||||
|
ed::{EdCurve, EdKeyPair},
|
||||||
|
},
|
||||||
|
Jwk, KeyPair,
|
||||||
},
|
},
|
||||||
jws::{
|
jws::{
|
||||||
alg::{ecdsa::EcdsaJwsAlgorithm::Es256, eddsa::EddsaJwsAlgorithm::Eddsa},
|
alg::{ecdsa::EcdsaJwsAlgorithm::Es256, eddsa::EddsaJwsAlgorithm::Eddsa},
|
||||||
JwsHeader, JwsSigner, JwsVerifier,
|
JwsHeader, JwsSigner, JwsVerifier, ES256,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use openssl::pkey::PKey;
|
||||||
|
use serde_json::Map;
|
||||||
use crate::utils::jwk::JwtExt;
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
/// An enum representing the possible types of JWK for ASPs
|
/// An enum representing the possible types of JWK for ASPs
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -49,64 +53,119 @@ pub struct AspKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AspKey {
|
impl AspKey {
|
||||||
pub fn from_jwk(jwk: Jwk) -> Result<Self, AspKeyError> {
|
/// Calculates the ASP fingerprint of an arbitrary [Jwk]
|
||||||
|
pub fn calculate_fingerprint(jwk: &Jwk) -> Result<String, anyhow::Error> {
|
||||||
|
// Construct a JSON object with only the "crv", "kty", "x", and potentially "y" values
|
||||||
|
let fingerprint: String = {
|
||||||
|
let mut map = Map::new();
|
||||||
|
map.insert(
|
||||||
|
"crv".to_string(),
|
||||||
|
jwk.curve()
|
||||||
|
.context("Key did not contain a 'crv' value")?
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
map.insert("kty".to_string(), jwk.key_type().into());
|
||||||
|
map.insert(
|
||||||
|
"x".to_string(),
|
||||||
|
jwk.parameter("x")
|
||||||
|
.context("Key did not contain an 'x' value")?
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
if let Some(y) = jwk.parameter("y") {
|
||||||
|
map.insert("y".to_string(), y.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::to_string(&map)
|
||||||
|
.context("Unable to serialize key into ordered, minimal JSON")?
|
||||||
|
};
|
||||||
|
// Sha512 hash the JSON
|
||||||
|
let fingerprint: Vec<u8> = {
|
||||||
|
let mut hash = Sha512::new();
|
||||||
|
hash.update(fingerprint);
|
||||||
|
hash.finalize().to_vec()
|
||||||
|
};
|
||||||
|
// Get the first 16 bytes of the hash
|
||||||
|
let fingerprint = &fingerprint[0..16];
|
||||||
|
// Base32 encode the first 16 bytes, and that is the fingerprint
|
||||||
|
let fingerprint = BASE32_NOPAD.encode(fingerprint);
|
||||||
|
Ok(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an [AspKey] from an arbitrary [Jwk] of the correct key type
|
||||||
|
pub fn from_jwk(jwk: Jwk) -> Result<Self, anyhow::Error> {
|
||||||
|
// Calculate the fingerprint
|
||||||
match jwk.key_type() {
|
match jwk.key_type() {
|
||||||
"OKP" => match jwk.curve() {
|
"OKP" => match jwk.curve() {
|
||||||
Some("Ed25519") => Ok(Self {
|
Some("Ed25519") => Ok(Self {
|
||||||
key_type: AspKeyType::Ed25519,
|
key_type: AspKeyType::Ed25519,
|
||||||
fingerprint: jwk
|
fingerprint: Self::calculate_fingerprint(&jwk)?,
|
||||||
.get_fingerprint()
|
|
||||||
.or(Err(AspKeyError::FingerprintError))?,
|
|
||||||
jwk,
|
jwk,
|
||||||
}),
|
}),
|
||||||
_ => Err(AspKeyError::InvalidJwkType),
|
_ => bail!("Invalid JWK type"),
|
||||||
},
|
},
|
||||||
"EC" => match jwk.curve() {
|
"EC" => match jwk.curve() {
|
||||||
Some("P-256") => Ok(Self {
|
Some("P-256") => Ok(Self {
|
||||||
key_type: AspKeyType::ES256,
|
key_type: AspKeyType::ES256,
|
||||||
fingerprint: jwk
|
fingerprint: Self::calculate_fingerprint(&jwk)?,
|
||||||
.get_fingerprint()
|
|
||||||
.or(Err(AspKeyError::FingerprintError))?,
|
|
||||||
jwk,
|
jwk,
|
||||||
}),
|
}),
|
||||||
_ => Err(AspKeyError::InvalidJwkType),
|
_ => bail!("Invalid JWK type"),
|
||||||
},
|
},
|
||||||
_ => Err(AspKeyError::InvalidJwkType),
|
_ => bail!("Invalid JWK type"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_pkcs8(key: &str) -> Result<Self, AspKeyError> {
|
pub fn from_pkcs8(key: &str) -> Result<Self, anyhow::Error> {
|
||||||
Self::from_jwk(Jwk::from_pkcs8(key.as_bytes()).or(Err(AspKeyError::Pkcs8ConversionError))?)
|
let decoded_pkcs8 = BASE64_NOPAD.decode(key.as_bytes())?;
|
||||||
|
let key_pair = ES256.key_pair_from_der(decoded_pkcs8)?;
|
||||||
|
|
||||||
|
Self::from_jwk(key_pair.to_jwk_key_pair())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_pkcs8(&self) -> Result<String, AspKeyError> {
|
pub fn into_pkcs8(&self) -> Result<String, anyhow::Error> {
|
||||||
self.jwk
|
// 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:
|
||||||
.to_pkcs8()
|
// 1. Get the josekit EcKeyPair from the Jwk
|
||||||
.or(Err(AspKeyError::Pkcs8ConversionError))
|
// 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: Box<dyn KeyPair> = match self.jwk.key_type() {
|
||||||
|
"EC" => match self.jwk.curve() {
|
||||||
|
Some("P-256") => Box::new(EcKeyPair::from_jwk(&self.jwk)?),
|
||||||
|
_ => bail!("Unsupported curve type"),
|
||||||
|
},
|
||||||
|
"OKP" => match self.jwk.curve() {
|
||||||
|
Some("Ed25519") => Box::new(EdKeyPair::from_jwk(&self.jwk)?),
|
||||||
|
_ => 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()?;
|
||||||
|
let encoded = BASE64_NOPAD.encode(&pkcs8);
|
||||||
|
|
||||||
|
Ok(encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(key_type: AspKeyType) -> Result<Self, AspKeyError> {
|
pub fn generate(key_type: AspKeyType) -> Result<Self, anyhow::Error> {
|
||||||
(|| -> anyhow::Result<Self> {
|
match key_type {
|
||||||
match key_type {
|
AspKeyType::Ed25519 => {
|
||||||
AspKeyType::Ed25519 => {
|
let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?;
|
||||||
let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?;
|
Ok(Self {
|
||||||
Ok(Self {
|
key_type,
|
||||||
key_type,
|
fingerprint: Self::calculate_fingerprint(&jwk)?,
|
||||||
fingerprint: jwk.get_fingerprint()?,
|
jwk,
|
||||||
jwk,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
AspKeyType::ES256 => {
|
|
||||||
let jwk = Jwk::generate_ec_key(EcCurve::P256)?;
|
|
||||||
Ok(Self {
|
|
||||||
key_type,
|
|
||||||
fingerprint: jwk.get_fingerprint()?,
|
|
||||||
jwk,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})()
|
AspKeyType::ES256 => {
|
||||||
.or(Err(AspKeyError::GenerationError))
|
let jwk = Jwk::generate_ec_key(EcCurve::P256)?;
|
||||||
|
Ok(Self {
|
||||||
|
key_type,
|
||||||
|
fingerprint: Self::calculate_fingerprint(&jwk)?,
|
||||||
|
jwk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_signer(&self) -> anyhow::Result<Box<dyn JwsSigner>> {
|
pub fn create_signer(&self) -> anyhow::Result<Box<dyn JwsSigner>> {
|
||||||
|
@ -122,19 +181,10 @@ impl AspKey {
|
||||||
AspKeyType::ES256 => Box::new(Es256.verifier_from_jwk(&self.jwk)?),
|
AspKeyType::ES256 => Box::new(Es256.verifier_from_jwk(&self.jwk)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_encrypted(&self, secret: &[u8]) -> anyhow::Result<String> {
|
|
||||||
self.jwk.encrypt(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Self> {
|
|
||||||
let jwk = Jwk::from_encrypted(secret, jwe)?;
|
|
||||||
Ok(Self::from_jwk(jwk)?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Jwk> for AspKey {
|
impl TryFrom<Jwk> for AspKey {
|
||||||
type Error = AspKeyError;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
fn try_from(value: Jwk) -> Result<Self, Self::Error> {
|
fn try_from(value: Jwk) -> Result<Self, Self::Error> {
|
||||||
Self::from_jwk(value)
|
Self::from_jwk(value)
|
||||||
|
@ -157,18 +207,6 @@ impl JwsHeaderExt for JwsHeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug, PartialEq)]
|
|
||||||
pub enum AspKeyError {
|
|
||||||
#[error("provided jwk was not a valid type")]
|
|
||||||
InvalidJwkType,
|
|
||||||
#[error("unable to calculate fingerprint of key")]
|
|
||||||
FingerprintError,
|
|
||||||
#[error("an error occurred during key generation")]
|
|
||||||
GenerationError,
|
|
||||||
#[error("unable to convert PKCS#8 key to/from a jwt key")]
|
|
||||||
Pkcs8ConversionError,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use josekit::jwk::{
|
use josekit::jwk::{
|
||||||
|
@ -176,7 +214,7 @@ mod tests {
|
||||||
Jwk,
|
Jwk,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::keys::{AspKey, AspKeyError, AspKeyType};
|
use crate::keys::{AspKey, AspKeyType};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_eddsa() {
|
fn generate_eddsa() {
|
||||||
|
@ -239,31 +277,6 @@ mod tests {
|
||||||
let jwk = Jwk::generate_ec_key(EcCurve::P521); // Invalid curve type!
|
let jwk = Jwk::generate_ec_key(EcCurve::P521); // Invalid curve type!
|
||||||
assert!(jwk.is_ok(), "jwk should generate successfully");
|
assert!(jwk.is_ok(), "jwk should generate successfully");
|
||||||
let key = AspKey::from_jwk(jwk.unwrap());
|
let key = AspKey::from_jwk(jwk.unwrap());
|
||||||
assert_eq!(
|
assert!(key.is_err(), "key should fail to convert");
|
||||||
key.err(),
|
|
||||||
Some(AspKeyError::InvalidJwkType),
|
|
||||||
"key should fail to convert"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn export_encrypted() {
|
|
||||||
let mut secret = [0u8; 32];
|
|
||||||
assert!(openssl::rand::rand_bytes(&mut secret).is_ok());
|
|
||||||
let key = AspKey::generate(AspKeyType::Ed25519);
|
|
||||||
assert!(key.is_ok());
|
|
||||||
let jwe = key.unwrap().export_encrypted(&secret);
|
|
||||||
assert!(jwe.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn import_encrypted() {
|
|
||||||
let mut secret = [0u8; 32];
|
|
||||||
assert!(openssl::rand::rand_bytes(&mut secret).is_ok());
|
|
||||||
let key = AspKey::generate(AspKeyType::Ed25519).unwrap();
|
|
||||||
let encrypted = key.export_encrypted(&secret);
|
|
||||||
assert!(encrypted.is_ok());
|
|
||||||
let decrypted = AspKey::from_encrypted(&secret, &encrypted.unwrap());
|
|
||||||
assert!(decrypted.is_ok());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
use anyhow::{bail, Context};
|
|
||||||
use data_encoding::{BASE32_NOPAD, BASE64_NOPAD};
|
|
||||||
use josekit::{
|
|
||||||
jwe::JweHeader,
|
|
||||||
jwe::{self, alg::aesgcmkw::AesgcmkwJweAlgorithm::A256gcmkw},
|
|
||||||
jwk::{
|
|
||||||
alg::{ec::EcKeyPair, ed::EdKeyPair},
|
|
||||||
Jwk, KeyPair,
|
|
||||||
},
|
|
||||||
jws::ES256,
|
|
||||||
};
|
|
||||||
use openssl::pkey::PKey;
|
|
||||||
use serde_json::Map;
|
|
||||||
use sha2::{Digest, Sha512};
|
|
||||||
|
|
||||||
pub trait JwtExt {
|
|
||||||
fn get_fingerprint(&self) -> anyhow::Result<String>;
|
|
||||||
fn to_pkcs8(&self) -> anyhow::Result<String>;
|
|
||||||
fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result<Jwk>;
|
|
||||||
fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Jwk>;
|
|
||||||
fn encrypt(&self, secret: &[u8]) -> anyhow::Result<String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JwtExt for Jwk {
|
|
||||||
fn get_fingerprint(&self) -> anyhow::Result<String> {
|
|
||||||
// Construct a JSON object with only the "crv", "kty", "x", and potentially "y" values
|
|
||||||
let fingerprint: String = {
|
|
||||||
let mut map = Map::new();
|
|
||||||
map.insert(
|
|
||||||
"crv".to_string(),
|
|
||||||
self.curve()
|
|
||||||
.context("Key did not contain a 'crv' value")?
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
map.insert("kty".to_string(), self.key_type().into());
|
|
||||||
map.insert(
|
|
||||||
"x".to_string(),
|
|
||||||
self.parameter("x")
|
|
||||||
.context("Key did not contain an 'x' value")?
|
|
||||||
.clone(),
|
|
||||||
);
|
|
||||||
if let Some(y) = self.parameter("y") {
|
|
||||||
map.insert("y".to_string(), y.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::to_string(&map)
|
|
||||||
.context("Unable to serialize key into ordered, minimal JSON")?
|
|
||||||
};
|
|
||||||
// Sha512 hash the JSON
|
|
||||||
let fingerprint: Vec<u8> = {
|
|
||||||
let mut hash = Sha512::new();
|
|
||||||
hash.update(fingerprint);
|
|
||||||
hash.finalize().to_vec()
|
|
||||||
};
|
|
||||||
// Get the first 16 bytes of the hash
|
|
||||||
let fingerprint = &fingerprint[0..16];
|
|
||||||
// Base32 encode the first 16 bytes, and that is the fingerprint
|
|
||||||
let fingerprint = BASE32_NOPAD.encode(fingerprint);
|
|
||||||
Ok(fingerprint)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_pkcs8(&self) -> anyhow::Result<String> {
|
|
||||||
// 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
|
|
||||||
// 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: 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()?;
|
|
||||||
let encoded = BASE64_NOPAD.encode(&pkcs8);
|
|
||||||
Ok(encoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result<Self> {
|
|
||||||
let decoded_pkcs8 = BASE64_NOPAD.decode(pkcs8)?;
|
|
||||||
let key_pair = ES256.key_pair_from_der(decoded_pkcs8)?;
|
|
||||||
Ok(key_pair.to_jwk_key_pair())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encrypt(&self, secret: &[u8]) -> anyhow::Result<String> {
|
|
||||||
let mut header = JweHeader::new();
|
|
||||||
header.set_content_type("jwt+json");
|
|
||||||
header.set_content_encryption("A128CBC-HS256");
|
|
||||||
|
|
||||||
let payload = self.to_string();
|
|
||||||
|
|
||||||
let encrypter = A256gcmkw.encrypter_from_bytes(secret)?;
|
|
||||||
let jwt = jwe::serialize_compact(payload.as_bytes(), &header, &encrypter)?;
|
|
||||||
|
|
||||||
Ok(jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Jwk> {
|
|
||||||
let decrypter = A256gcmkw.decrypter_from_bytes(secret)?;
|
|
||||||
let (deserialized, _) = jwe::deserialize_compact(jwe, &decrypter)?;
|
|
||||||
let jwk = Jwk::from_bytes(deserialized)?;
|
|
||||||
|
|
||||||
Ok(jwk)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +1 @@
|
||||||
pub mod jwk;
|
|
||||||
pub mod jwt;
|
pub mod jwt;
|
||||||
|
|
|
@ -10,7 +10,7 @@ use std::io::Write;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
||||||
entities::prelude::*
|
entities::prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Deletes a saved key, after asking for confirmation.
|
/// Deletes a saved key, after asking for confirmation.
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
Aes256Gcm, Key,
|
||||||
|
};
|
||||||
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
use anstyle::{AnsiColor, Color as AnstyleColor, Reset, Style as Anstyle};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
|
@ -6,18 +10,17 @@ use clap::{Parser, ValueEnum};
|
||||||
use data_encoding::BASE64_NOPAD;
|
use data_encoding::BASE64_NOPAD;
|
||||||
use dialoguer::{theme::ColorfulTheme, Password};
|
use dialoguer::{theme::ColorfulTheme, Password};
|
||||||
use indoc::writedoc;
|
use indoc::writedoc;
|
||||||
|
use josekit::jwk::Jwk;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
||||||
entities::prelude::*
|
entities::prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
pub enum KeyExportFormat {
|
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.
|
/// An unencrypted PKCS#8 format. This is the format used by the asp.keyoxide.org web tool.
|
||||||
#[clap(alias = "PKCS#8")]
|
#[clap(alias = "PKCS#8")]
|
||||||
PKCS8,
|
PKCS8,
|
||||||
|
@ -83,11 +86,12 @@ impl AspmSubcommand for KeysExportCommand {
|
||||||
let aes_key = hash.hash.context("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 aes_key = &aes_key.as_bytes()[0..32];
|
||||||
|
|
||||||
if let Ok(decrypted) = AspKey::from_encrypted(aes_key, &key.encrypted) {
|
if let Ok(decrypted) = Aes256Gcm::new(&Key::<Aes256Gcm>::from_slice(aes_key))
|
||||||
|
.decrypt((&aes_key[0..12]).into(), key.cipher_text.as_slice())
|
||||||
|
{
|
||||||
|
let decrypted = AspKey::from_jwk(Jwk::from_bytes(decrypted)?)?;
|
||||||
|
|
||||||
let export = match self.format {
|
let export = match self.format {
|
||||||
KeyExportFormat::Encrypted => decrypted
|
|
||||||
.export_encrypted(aes_key)
|
|
||||||
.context("Unable to convert key into encrypted format")?,
|
|
||||||
KeyExportFormat::PKCS8 => decrypted
|
KeyExportFormat::PKCS8 => decrypted
|
||||||
.into_pkcs8()
|
.into_pkcs8()
|
||||||
.context("Unable to convert key into PKCS#8 format")?,
|
.context("Unable to convert key into PKCS#8 format")?,
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
Aes256Gcm, Key,
|
||||||
|
};
|
||||||
use anyhow::{anyhow, bail, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
use asp::keys::{AspKey, AspKeyType};
|
use asp::keys::{AspKey, AspKeyType};
|
||||||
|
@ -68,16 +72,18 @@ impl AspmSubcommand for KeysGenerateCommand {
|
||||||
let aes_key = hash.hash.context("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 aes_key = &aes_key.as_bytes()[0..32];
|
||||||
|
|
||||||
let encrypted = key
|
let Ok(cipher_text) = Aes256Gcm::new(&Key::<Aes256Gcm>::from_slice(aes_key))
|
||||||
.export_encrypted(aes_key)
|
.encrypt((&aes_key[0..12]).into(), key.jwk.to_string().as_bytes())
|
||||||
.context("Unable to derive the encryption key")?;
|
else {
|
||||||
|
bail!("Failure encrypting key")
|
||||||
|
};
|
||||||
|
|
||||||
// Write to db
|
// Write to db
|
||||||
let entry = keys::ActiveModel {
|
let entry = keys::ActiveModel {
|
||||||
fingerprint: ActiveValue::Set(key.fingerprint.clone()),
|
fingerprint: ActiveValue::Set(key.fingerprint.clone()),
|
||||||
key_type: ActiveValue::Set(key.key_type.into()),
|
key_type: ActiveValue::Set(key.key_type.into()),
|
||||||
alias: ActiveValue::Set(alias),
|
alias: ActiveValue::Set(alias),
|
||||||
encrypted: ActiveValue::Set(encrypted),
|
cipher_text: ActiveValue::Set(cipher_text),
|
||||||
};
|
};
|
||||||
let res = keys::Entity::insert(entry)
|
let res = keys::Entity::insert(entry)
|
||||||
.exec(&state.db)
|
.exec(&state.db)
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
use std::{io::Write, sync::Arc};
|
use std::{io::Write, sync::Arc};
|
||||||
|
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
Aes256Gcm, Key,
|
||||||
|
};
|
||||||
use anyhow::{anyhow, bail, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -251,13 +255,18 @@ impl AspmSubcommand for KeysImportGpgCommand {
|
||||||
let aes_key = hash.hash.context("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 aes_key = &aes_key.as_bytes()[0..32];
|
||||||
|
|
||||||
let encrypted = asp_key.export_encrypted(aes_key)?;
|
let key = Key::<Aes256Gcm>::from_slice(aes_key);
|
||||||
|
let Ok(cipher_text) = Aes256Gcm::new(&key)
|
||||||
|
.encrypt((&aes_key[0..12]).into(), asp_key.jwk.to_string().as_bytes())
|
||||||
|
else {
|
||||||
|
bail!("Failure encrypting key")
|
||||||
|
};
|
||||||
|
|
||||||
let entry = keys::ActiveModel {
|
let entry = keys::ActiveModel {
|
||||||
fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()),
|
fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()),
|
||||||
key_type: ActiveValue::Set(asp_key.key_type.clone().into()),
|
key_type: ActiveValue::Set(asp_key.key_type.clone().into()),
|
||||||
alias: ActiveValue::Set(format!("{uid}", uid = uid.id.id())),
|
alias: ActiveValue::Set(format!("{uid}", uid = uid.id.id())),
|
||||||
encrypted: ActiveValue::Set(encrypted),
|
cipher_text: ActiveValue::Set(cipher_text),
|
||||||
};
|
};
|
||||||
// Because GPGME's context object is not 'Send', normal .await can't be used here, so this just blocks as a workaround
|
// Because GPGME's context object is not 'Send', normal .await can't be used here, so this just blocks as a workaround
|
||||||
let res = runtime
|
let res = runtime
|
||||||
|
|
|
@ -2,11 +2,16 @@ use std::io::Read;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
use asp::keys::{AspKey, AspKeyError};
|
use asp::keys::AspKey;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use clap_stdin::FileOrStdin;
|
use clap_stdin::FileOrStdin;
|
||||||
use data_encoding::BASE64_NOPAD;
|
use data_encoding::BASE64_NOPAD;
|
||||||
use dialoguer::{theme::ColorfulTheme, Input, Password};
|
use dialoguer::{theme::ColorfulTheme, Input, Password};
|
||||||
|
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
Aes256Gcm, Key,
|
||||||
|
};
|
||||||
use indoc::printdoc;
|
use indoc::printdoc;
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
use sea_orm::{ActiveValue, EntityTrait};
|
use sea_orm::{ActiveValue, EntityTrait};
|
||||||
|
@ -53,31 +58,20 @@ impl AspmSubcommand for KeysImportJwkCommand {
|
||||||
.context("Unable to prompt on stderr")
|
.context("Unable to prompt on stderr")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let key = match AspKey::from_jwk(
|
let asp_key = AspKey::from_jwk(
|
||||||
Jwk::from_bytes({
|
Jwk::from_bytes({
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
self.key.into_reader()?.read_to_end(&mut buf)?;
|
self.key.into_reader()?.read_to_end(&mut buf)?;
|
||||||
buf
|
buf
|
||||||
}).context("Unable to parse provided JWK")?,
|
})
|
||||||
|
.context("Unable to parse provided JWK")?,
|
||||||
)
|
)
|
||||||
.context("Unable to convert parsed JWK to an AspKey")
|
.context("Unable to convert parsed JWK to an AspKey")?;
|
||||||
{
|
|
||||||
Ok(key) => key,
|
|
||||||
Err(e) => match e
|
|
||||||
.downcast_ref::<AspKeyError>()
|
|
||||||
.context("Invalid error returned from asp parsing")?
|
|
||||||
{
|
|
||||||
AspKeyError::InvalidJwkType => {
|
|
||||||
eprintln!("The provided JWK used a type and curve that is unsupported by ASPs, please use a correct key type");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => return Err(e),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let argon_salt =
|
let argon_salt = SaltString::from_b64(
|
||||||
SaltString::from_b64(&BASE64_NOPAD.encode(key.fingerprint.to_uppercase().as_bytes()))
|
&BASE64_NOPAD.encode(asp_key.fingerprint.to_uppercase().as_bytes()),
|
||||||
.context("Unable to derive argon2 salt")?;
|
)
|
||||||
|
.context("Unable to derive argon2 salt")?;
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::default();
|
||||||
let hash = argon2
|
let hash = argon2
|
||||||
.hash_password(key_password.as_bytes(), &argon_salt)
|
.hash_password(key_password.as_bytes(), &argon_salt)
|
||||||
|
@ -85,22 +79,25 @@ impl AspmSubcommand for KeysImportJwkCommand {
|
||||||
let aes_key = hash.hash.context("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 aes_key = &aes_key.as_bytes()[0..32];
|
||||||
|
|
||||||
let encrypted = key
|
let key = Key::<Aes256Gcm>::from_slice(aes_key);
|
||||||
.export_encrypted(aes_key)
|
let Ok(cipher_text) = Aes256Gcm::new(&key)
|
||||||
.context("Unable to derive the encryption key")?;
|
.encrypt((&aes_key[0..12]).into(), asp_key.jwk.to_string().as_bytes())
|
||||||
|
else {
|
||||||
|
bail!("Failure encrypting key")
|
||||||
|
};
|
||||||
|
|
||||||
// Write to db
|
// Write to db
|
||||||
let entry = keys::ActiveModel {
|
let entry = keys::ActiveModel {
|
||||||
fingerprint: ActiveValue::Set(key.fingerprint.clone()),
|
fingerprint: ActiveValue::Set(asp_key.fingerprint.clone()),
|
||||||
key_type: ActiveValue::Set(key.key_type.clone().into()),
|
key_type: ActiveValue::Set(asp_key.key_type.clone().into()),
|
||||||
alias: ActiveValue::Set(alias),
|
alias: ActiveValue::Set(alias),
|
||||||
encrypted: ActiveValue::Set(encrypted),
|
cipher_text: ActiveValue::Set(cipher_text),
|
||||||
};
|
};
|
||||||
let res = keys::Entity::insert(entry)
|
let res = keys::Entity::insert(entry)
|
||||||
.exec(&state.db)
|
.exec(&state.db)
|
||||||
.await
|
.await
|
||||||
.context("Unable to add key to database")?;
|
.context("Unable to add key to database")?;
|
||||||
if res.last_insert_id != key.fingerprint {
|
if res.last_insert_id != asp_key.fingerprint {
|
||||||
bail!("The key was unable to be saved to the database")
|
bail!("The key was unable to be saved to the database")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,8 +107,8 @@ impl AspmSubcommand for KeysImportJwkCommand {
|
||||||
Fingerprint: {fpr}
|
Fingerprint: {fpr}
|
||||||
Type: {type:?}
|
Type: {type:?}
|
||||||
",
|
",
|
||||||
fpr = key.fingerprint,
|
fpr = asp_key.fingerprint,
|
||||||
r#type = key.key_type
|
r#type = asp_key.key_type
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub struct Model {
|
||||||
pub fingerprint: String,
|
pub fingerprint: String,
|
||||||
pub key_type: i32,
|
pub key_type: i32,
|
||||||
pub alias: String,
|
pub alias: String,
|
||||||
pub encrypted: String,
|
pub cipher_text: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -10,7 +10,7 @@ impl MigrationName for Migration {
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
// Define how to apply this migration: Create the Bakery table.
|
// Define how to apply this migration: Create the Keys table.
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
|
@ -24,13 +24,13 @@ impl MigrationTrait for Migration {
|
||||||
)
|
)
|
||||||
.col(ColumnDef::new(Keys::KeyType).integer().not_null())
|
.col(ColumnDef::new(Keys::KeyType).integer().not_null())
|
||||||
.col(ColumnDef::new(Keys::Alias).string().not_null())
|
.col(ColumnDef::new(Keys::Alias).string().not_null())
|
||||||
.col(ColumnDef::new(Keys::Encrypted).string().not_null())
|
.col(ColumnDef::new(Keys::CipherText).binary().not_null())
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define how to rollback this migration: Drop the Bakery table.
|
// Define how to rollback this migration: Drop the Keys table.
|
||||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table(Keys::Table).to_owned())
|
.drop_table(Table::drop().table(Keys::Table).to_owned())
|
||||||
|
@ -44,5 +44,5 @@ enum Keys {
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
KeyType,
|
KeyType,
|
||||||
Alias,
|
Alias,
|
||||||
Encrypted,
|
CipherText,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue