use std::time::Duration;
use chrono::{self, SubsecRound};
use rand::{thread_rng, CryptoRng, Rng};
use smallvec::SmallVec;
use crate::pgp::composed::{KeyDetails, SecretKey, SecretSubkey};
use crate::pgp::crypto::{
ecdh, eddsa, rsa, HashAlgorithm, PublicKeyAlgorithm, SymmetricKeyAlgorithm,
};
use crate::pgp::errors::Result;
use crate::pgp::packet::{self, KeyFlags, UserAttribute, UserId};
use crate::pgp::types::{self, CompressionAlgorithm, PublicParams, RevocationKey};
#[derive(Debug, PartialEq, Eq, Builder)]
#[builder(build_fn(validate = "Self::validate"))]
pub struct SecretKeyParams {
key_type: KeyType,
#[builder(default)]
can_sign: bool,
#[builder(default)]
can_create_certificates: bool,
#[builder(default)]
can_encrypt: bool,
#[builder(default)]
preferred_symmetric_algorithms: SmallVec<[SymmetricKeyAlgorithm; 8]>,
#[builder(default)]
preferred_hash_algorithms: SmallVec<[HashAlgorithm; 8]>,
#[builder(default)]
preferred_compression_algorithms: SmallVec<[CompressionAlgorithm; 8]>,
#[builder(default)]
revocation_key: Option<RevocationKey>,
#[builder]
primary_user_id: String,
#[builder(default)]
user_ids: Vec<String>,
#[builder(default)]
user_attributes: Vec<UserAttribute>,
#[builder(default)]
passphrase: Option<String>,
#[builder(default = "chrono::Utc::now().trunc_subsecs(0)")]
created_at: chrono::DateTime<chrono::Utc>,
#[builder(default)]
packet_version: types::Version,
#[builder(default)]
version: types::KeyVersion,
#[builder(default)]
expiration: Option<Duration>,
#[builder(default)]
subkeys: Vec<SubkeyParams>,
#[builder(default)]
key_material: Option<(PublicParams, types::SecretParams)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Builder)]
pub struct SubkeyParams {
key_type: KeyType,
#[builder(default)]
can_sign: bool,
#[builder(default)]
can_create_certificates: bool,
#[builder(default)]
can_encrypt: bool,
#[builder(default)]
can_authenticate: bool,
#[builder(default)]
user_ids: Vec<UserId>,
#[builder(default)]
user_attributes: Vec<UserAttribute>,
#[builder(default)]
passphrase: Option<String>,
#[builder(default = "chrono::Utc::now().trunc_subsecs(0)")]
created_at: chrono::DateTime<chrono::Utc>,
#[builder(default)]
packet_version: types::Version,
#[builder(default)]
version: types::KeyVersion,
#[builder(default)]
expiration: Option<Duration>,
#[builder(default)]
key_material: Option<(PublicParams, types::SecretParams)>,
}
impl SecretKeyParamsBuilder {
fn validate(&self) -> std::result::Result<(), String> {
match self.key_type {
Some(KeyType::Rsa(size)) => {
if size < 2048 {
return Err("Keys with less than 2048bits are considered insecure".into());
}
}
Some(KeyType::EdDSA) => {
if let Some(can_encrypt) = self.can_encrypt {
if can_encrypt {
return Err("EdDSA can only be used for signing keys".into());
}
}
}
Some(KeyType::ECDH) => {
if let Some(can_sign) = self.can_sign {
if can_sign {
return Err("ECDH can only be used for encryption keys".into());
}
}
}
_ => {}
}
Ok(())
}
pub fn user_id<VALUE: Into<String>>(&mut self, value: VALUE) -> &mut Self {
if let Some(ref mut user_ids) = self.user_ids {
user_ids.push(value.into());
} else {
self.user_ids = Some(vec![value.into()]);
}
self
}
pub fn subkey<VALUE: Into<SubkeyParams>>(&mut self, value: VALUE) -> &mut Self {
if let Some(ref mut subkeys) = self.subkeys {
subkeys.push(value.into());
} else {
self.subkeys = Some(vec![value.into()]);
}
self
}
}
impl SecretKeyParams {
pub fn generate(self) -> Result<SecretKey> {
let mut rng = thread_rng();
self.generate_with_rng(&mut rng)
}
pub fn generate_with_rng<R: Rng + CryptoRng>(self, rng: &mut R) -> Result<SecretKey> {
let passphrase = self.passphrase;
let (public_params, secret_params) = match self.key_material {
Some(key_material) => key_material,
None => self.key_type.generate_with_rng(rng, passphrase)?,
};
let primary_key = packet::SecretKey {
details: packet::PublicKey {
packet_version: self.packet_version,
version: self.version,
algorithm: self.key_type.to_alg(),
created_at: self.created_at,
expiration: self.expiration.map(|v| v.as_secs() as u16),
public_params,
},
secret_params,
};
let mut keyflags = KeyFlags::default();
keyflags.set_certify(self.can_create_certificates);
keyflags.set_encrypt_comms(self.can_encrypt);
keyflags.set_encrypt_storage(self.can_encrypt);
keyflags.set_sign(self.can_sign);
Ok(SecretKey::new(
primary_key,
KeyDetails::new(
UserId::from_str(Default::default(), &self.primary_user_id),
self.user_ids
.iter()
.map(|m| UserId::from_str(Default::default(), m))
.collect(),
self.user_attributes,
keyflags,
self.preferred_symmetric_algorithms,
self.preferred_hash_algorithms,
self.preferred_compression_algorithms,
self.revocation_key,
),
Default::default(),
self.subkeys
.into_iter()
.map(|subkey| {
let passphrase = subkey.passphrase;
let (public_params, secret_params) = match subkey.key_material {
Some(key_material) => key_material,
None => self.key_type.generate_with_rng(rng, passphrase)?,
};
let mut keyflags = KeyFlags::default();
keyflags.set_certify(subkey.can_create_certificates);
keyflags.set_encrypt_comms(subkey.can_encrypt);
keyflags.set_encrypt_storage(subkey.can_encrypt);
keyflags.set_sign(subkey.can_sign);
keyflags.set_authentication(subkey.can_authenticate);
Ok(SecretSubkey::new(
packet::SecretSubkey {
details: packet::PublicSubkey {
packet_version: subkey.packet_version,
version: subkey.version,
algorithm: subkey.key_type.to_alg(),
created_at: subkey.created_at,
expiration: subkey.expiration.map(|v| v.as_secs() as u16),
public_params,
},
secret_params,
},
keyflags,
))
})
.collect::<Result<Vec<_>>>()?,
))
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum KeyType {
Rsa(u32),
ECDH,
EdDSA,
}
impl KeyType {
pub fn to_alg(self) -> PublicKeyAlgorithm {
match self {
KeyType::Rsa(_) => PublicKeyAlgorithm::RSA,
KeyType::ECDH => PublicKeyAlgorithm::ECDH,
KeyType::EdDSA => PublicKeyAlgorithm::EdDSA,
}
}
pub fn generate(
self,
passphrase: Option<String>,
) -> Result<(PublicParams, types::SecretParams)> {
let mut rng = thread_rng();
self.generate_with_rng(&mut rng, passphrase)
}
pub fn generate_with_rng<R: Rng + CryptoRng>(
self,
rng: &mut R,
passphrase: Option<String>,
) -> Result<(PublicParams, types::SecretParams)> {
let (pub_params, plain) = match self {
KeyType::Rsa(bit_size) => rsa::generate_key(rng, bit_size as usize)?,
KeyType::ECDH => ecdh::generate_key(rng),
KeyType::EdDSA => eddsa::generate_key(rng),
};
let secret = match passphrase {
Some(passphrase) => {
let s2k = types::StringToKey::new_default(rng);
let alg = SymmetricKeyAlgorithm::AES256;
let id = 254;
let version = types::KeyVersion::default();
types::SecretParams::Encrypted(plain.encrypt(
rng,
&passphrase,
alg,
s2k,
version,
id,
)?)
}
None => types::SecretParams::Plain(plain),
};
Ok((pub_params, secret))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
use crate::pgp::types::SecretKeyTrait;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
#[test]
#[ignore] fn test_key_gen_rsa_2048() {
let _ = pretty_env_logger::try_init();
let mut key_params = SecretKeyParamsBuilder::default();
key_params
.key_type(KeyType::Rsa(2048))
.can_create_certificates(true)
.can_sign(true)
.primary_user_id("Me <me@mail.com>".into())
.preferred_symmetric_algorithms(smallvec![
SymmetricKeyAlgorithm::AES256,
SymmetricKeyAlgorithm::AES192,
SymmetricKeyAlgorithm::AES128,
])
.preferred_hash_algorithms(smallvec![
HashAlgorithm::SHA2_256,
HashAlgorithm::SHA2_384,
HashAlgorithm::SHA2_512,
HashAlgorithm::SHA2_224,
HashAlgorithm::SHA1,
])
.preferred_compression_algorithms(smallvec![
CompressionAlgorithm::ZLIB,
CompressionAlgorithm::ZIP,
]);
let key_params_enc = key_params
.clone()
.passphrase(Some("hello".into()))
.subkey(
SubkeyParamsBuilder::default()
.key_type(KeyType::Rsa(2048))
.passphrase(Some("hello".into()))
.can_encrypt(true)
.build()
.unwrap(),
)
.build()
.unwrap();
let key_enc = key_params_enc
.generate()
.expect("failed to generate secret key, encrypted");
let key_params_plain = key_params
.passphrase(None)
.subkey(
SubkeyParamsBuilder::default()
.key_type(KeyType::Rsa(2048))
.can_encrypt(true)
.build()
.unwrap(),
)
.build()
.unwrap();
let key_plain = key_params_plain
.generate()
.expect("failed to generate secret key");
let signed_key_enc = key_enc.sign(|| "hello".into()).expect("failed to sign key");
let signed_key_plain = key_plain.sign(|| "".into()).expect("failed to sign key");
let armor_enc = signed_key_enc
.to_armored_string(None)
.expect("failed to serialize key");
let armor_plain = signed_key_plain
.to_armored_string(None)
.expect("failed to serialize key");
std::fs::write("sample-rsa-enc.sec.asc", &armor_enc).unwrap();
std::fs::write("sample-rsa.sec.asc", &armor_plain).unwrap();
let (signed_key2_enc, _headers) =
SignedSecretKey::from_string(&armor_enc).expect("failed to parse key (enc)");
signed_key2_enc.verify().expect("invalid key (enc)");
let (signed_key2_plain, _headers) =
SignedSecretKey::from_string(&armor_plain).expect("failed to parse key (plain)");
signed_key2_plain.verify().expect("invalid key (plain)");
signed_key2_enc
.unlock(|| "hello".into(), |_| Ok(()))
.expect("failed to unlock parsed key (enc)");
signed_key2_plain
.unlock(|| "".into(), |_| Ok(()))
.expect("failed to unlock parsed key (plain)");
assert_eq!(signed_key_plain, signed_key2_plain);
let public_key = signed_key_plain.public_key();
let public_signed_key = public_key
.sign(&signed_key_plain, || "".into())
.expect("failed to sign public key");
public_signed_key.verify().expect("invalid public key");
let armor = public_signed_key
.to_armored_string(None)
.expect("failed to serialize public key");
std::fs::write("sample-rsa.pub.asc", &armor).unwrap();
let (signed_key2, _headers) =
SignedPublicKey::from_string(&armor).expect("failed to parse public key");
signed_key2.verify().expect("invalid public key");
}
#[ignore]
#[test]
fn key_gen_x25519_long() {
let rng = &mut ChaCha8Rng::seed_from_u64(0);
for _ in 0..10_000 {
gen_x25519(rng);
}
}
#[test]
fn key_gen_x25519_short() {
let rng = &mut ChaCha8Rng::seed_from_u64(0);
for _ in 0..100 {
gen_x25519(rng);
}
}
fn gen_x25519<R: Rng + CryptoRng>(rng: &mut R) {
let _ = pretty_env_logger::try_init();
let key_params = SecretKeyParamsBuilder::default()
.key_type(KeyType::EdDSA)
.can_create_certificates(true)
.can_sign(true)
.primary_user_id("Me-X <me-x25519@mail.com>".into())
.passphrase(None)
.preferred_symmetric_algorithms(smallvec![
SymmetricKeyAlgorithm::AES256,
SymmetricKeyAlgorithm::AES192,
SymmetricKeyAlgorithm::AES128,
])
.preferred_hash_algorithms(smallvec![
HashAlgorithm::SHA2_256,
HashAlgorithm::SHA2_384,
HashAlgorithm::SHA2_512,
HashAlgorithm::SHA2_224,
HashAlgorithm::SHA1,
])
.preferred_compression_algorithms(smallvec![
CompressionAlgorithm::ZLIB,
CompressionAlgorithm::ZIP,
])
.subkey(
SubkeyParamsBuilder::default()
.key_type(KeyType::ECDH)
.can_encrypt(true)
.passphrase(None)
.build()
.unwrap(),
)
.build()
.unwrap();
let key = key_params
.generate_with_rng(rng)
.expect("failed to generate secret key");
let signed_key = key.sign(|| "".into()).expect("failed to sign key");
let armor = signed_key
.to_armored_string(None)
.expect("failed to serialize key");
std::fs::write("sample-x25519.sec.asc", &armor).unwrap();
let (signed_key2, _headers) =
SignedSecretKey::from_string(&armor).expect("failed to parse key");
signed_key2.verify().expect("invalid key");
assert_eq!(signed_key, signed_key2);
let public_key = signed_key.public_key();
let public_signed_key = public_key
.sign(&signed_key, || "".into())
.expect("failed to sign public key");
public_signed_key.verify().expect("invalid public key");
let armor = public_signed_key
.to_armored_string(None)
.expect("failed to serialize public key");
std::fs::write("sample-x25519.pub.asc", &armor).unwrap();
let (signed_key2, _headers) =
SignedPublicKey::from_string(&armor).expect("failed to parse public key");
signed_key2.verify().expect("invalid public key");
}
}