use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use bytes::Bytes;
use pgp::composed::{
ArmorOptions, DetachedSignature, EncryptionCaps, KeyType, MessageBuilder,
SecretKeyParamsBuilder, SignedSecretKey, SubkeyParamsBuilder,
};
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::types::{CompressionAlgorithm, EncryptionKey, KeyDetails, KeyVersion};
use rand::RngCore;
use super::errors::CryptoError;
use super::keys::PrivateKey;
pub(crate) trait RecipientOp {
type Out;
fn run(self, pubkey: &impl EncryptionKey) -> pgp::errors::Result<Self::Out>;
}
pub(crate) fn recipient_encryption_key<Op: RecipientOp>(
key: &SignedSecretKey,
op: Op,
) -> Result<Op::Out, CryptoError> {
let primary = key.primary_key.public_key();
if primary.algorithm().can_encrypt() {
return op
.run(primary)
.map_err(|e| CryptoError::Encrypt(format!("encrypt to primary key: {e}")));
}
for sub in &key.secret_subkeys {
let pubsub = sub.public_key();
if pubsub.algorithm().can_encrypt() {
return op
.run(&pubsub)
.map_err(|e| CryptoError::Encrypt(format!("encrypt to subkey: {e}")));
}
}
Err(CryptoError::Encrypt(
"key has no encryption-capable (sub)key".into(),
))
}
pub struct GeneratedNodeKey {
pub key: PrivateKey,
pub locked_armored: String,
pub passphrase: Vec<u8>,
}
pub fn generate_node_key() -> Result<GeneratedNodeKey, CryptoError> {
generate_node_key_versioned(KeyVersion::V4)
}
pub fn generate_node_key_aead() -> Result<GeneratedNodeKey, CryptoError> {
generate_node_key_versioned(KeyVersion::V6)
}
fn generate_node_key_versioned(version: KeyVersion) -> Result<GeneratedNodeKey, CryptoError> {
let mut rng = rand::thread_rng();
let passphrase = generate_passphrase();
let pw_string = String::from_utf8(passphrase.clone())
.map_err(|e| CryptoError::Encrypt(format!("passphrase is not ascii: {e}")))?;
let subkey = SubkeyParamsBuilder::default()
.version(version)
.key_type(KeyType::X25519)
.can_encrypt(EncryptionCaps::All)
.passphrase(Some(pw_string.clone()))
.build()
.map_err(|e| CryptoError::Encrypt(format!("node subkey params: {e}")))?;
let params = SecretKeyParamsBuilder::default()
.version(version)
.key_type(KeyType::Ed25519)
.can_certify(true)
.can_sign(true)
.primary_user_id("Drive key <no-reply@proton.me>".into())
.passphrase(Some(pw_string))
.subkey(subkey)
.build()
.map_err(|e| CryptoError::Encrypt(format!("node key params: {e}")))?;
let signed = params
.generate(&mut rng)
.map_err(|e| CryptoError::Encrypt(format!("generate node key: {e}")))?;
let locked_armored = signed
.to_armored_string(None.into())
.map_err(|e| CryptoError::Encrypt(format!("armor node key: {e}")))?;
let key = PrivateKey::from_armored(&locked_armored, &passphrase)?;
Ok(GeneratedNodeKey {
key,
locked_armored,
passphrase,
})
}
fn generate_passphrase() -> Vec<u8> {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
BASE64.encode(bytes).into_bytes()
}
pub fn generate_node_hash_key(node_key: &PrivateKey) -> Result<String, CryptoError> {
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
encrypt_and_sign(node_key.key(), Some(node_key), &bytes, false, false)
}
pub struct VolumeCreationMaterial {
pub share_key_armored: String,
pub share_passphrase: String,
pub share_passphrase_signature: String,
pub folder_name: String,
pub folder_key_armored: String,
pub folder_passphrase: String,
pub folder_passphrase_signature: String,
pub folder_hash_key: String,
}
pub fn build_volume_creation_material(
address_key: &PrivateKey,
root_folder_name: &str,
) -> Result<VolumeCreationMaterial, CryptoError> {
let share = generate_node_key()?;
let folder = generate_node_key()?;
let share_passphrase = address_key.encrypt(&share.passphrase)?;
let share_passphrase_signature = address_key.sign_detached(&share.passphrase)?;
let folder_name =
share
.key
.encrypt_and_sign(address_key, root_folder_name.as_bytes(), true, false)?;
let folder_passphrase = share.key.encrypt(&folder.passphrase)?;
let folder_passphrase_signature = address_key.sign_detached(&folder.passphrase)?;
let mut hash_key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut hash_key);
let folder_hash_key = folder
.key
.encrypt_and_sign(address_key, &hash_key, false, false)?;
Ok(VolumeCreationMaterial {
share_key_armored: share.locked_armored,
share_passphrase,
share_passphrase_signature,
folder_name,
folder_key_armored: folder.locked_armored,
folder_passphrase,
folder_passphrase_signature,
folder_hash_key,
})
}
pub(crate) fn encrypt_and_sign(
recipient: &SignedSecretKey,
signer: Option<&PrivateKey>,
data: &[u8],
text: bool,
compress: bool,
) -> Result<String, CryptoError> {
recipient_encryption_key(
recipient,
EncryptSignOp {
data: data.to_vec(),
signer,
text,
compress,
},
)
}
pub(crate) fn sign_detached(signer: &PrivateKey, data: &[u8]) -> Result<String, CryptoError> {
let mut rng = rand::thread_rng();
let key = &signer.key().primary_key;
let pw = signer.password();
let mut config = SignatureConfig::from_key(&mut rng, key, SignatureType::Binary)
.map_err(|e| CryptoError::Encrypt(format!("signature config: {e}")))?;
config.hashed_subpackets = vec![
Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint()))
.map_err(|e| CryptoError::Encrypt(format!("issuer fingerprint subpacket: {e}")))?,
Subpacket::regular(SubpacketData::SignatureCreationTime(
pgp::types::Timestamp::now(),
))
.map_err(|e| CryptoError::Encrypt(format!("creation-time subpacket: {e}")))?,
];
if key.version() <= KeyVersion::V4 {
config.unhashed_subpackets = vec![
Subpacket::regular(SubpacketData::IssuerKeyId(key.legacy_key_id()))
.map_err(|e| CryptoError::Encrypt(format!("issuer subpacket: {e}")))?,
];
}
let signature = config
.sign(key, &pw, data)
.map_err(|e| CryptoError::Encrypt(format!("detached sign: {e}")))?;
DetachedSignature::new(signature)
.to_armored_string(ArmorOptions::default())
.map_err(|e| CryptoError::Encrypt(format!("armor signature: {e}")))
}
struct EncryptSignOp<'a> {
data: Vec<u8>,
signer: Option<&'a PrivateKey>,
text: bool,
compress: bool,
}
impl RecipientOp for EncryptSignOp<'_> {
type Out = String;
fn run(self, pubkey: &impl EncryptionKey) -> pgp::errors::Result<String> {
let mut rng = rand::thread_rng();
let mut builder = MessageBuilder::from_bytes(Bytes::new(), self.data)
.seipd_v1(&mut rng, SymmetricKeyAlgorithm::AES256);
if self.compress {
builder.compression(CompressionAlgorithm::ZLIB);
}
if self.text {
builder.sign_text();
}
if let Some(signer) = self.signer {
builder.sign(
&signer.key().primary_key,
signer.password(),
HashAlgorithm::Sha256,
);
}
builder.encrypt_to_key(&mut rng, pubkey)?;
builder.to_armored_string(&mut rng, ArmorOptions::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_node_key_encrypt_sign_round_trip() {
let node = generate_node_key().expect("generate node key");
let signer = generate_node_key().expect("generate signer key");
let plaintext = b"extended attributes payload".to_vec();
let armored = node
.key
.encrypt_and_sign(&signer.key, &plaintext, false, true)
.expect("encrypt and sign");
let decrypted = node.key.decrypt_armored_message(&armored).expect("decrypt");
assert_eq!(decrypted, plaintext);
let armored_only = node.key.encrypt(&plaintext).expect("encrypt only");
let decrypted_only = node
.key
.decrypt_armored_message(&armored_only)
.expect("decrypt only");
assert_eq!(decrypted_only, plaintext);
let data = b"manifest bytes";
let sig = signer.key.sign_detached(data).expect("detached sign");
signer
.key
.verify_detached_signature(&sig, data)
.expect("verify detached signature");
use pgp::composed::Deserializable as _;
let (parsed, _) = DetachedSignature::from_string(&sig).expect("parse detached sig");
assert!(
parsed.signature.created().is_some(),
"detached signature missing creation time"
);
assert!(
!parsed.signature.issuer_key_id().is_empty(),
"detached signature missing issuer key id"
);
}
#[test]
fn volume_creation_material_round_trips() {
let address_key = generate_node_key().expect("generate address key");
let material =
build_volume_creation_material(&address_key.key, "root").expect("build material");
let share_pp = address_key
.key
.decrypt_armored_message(&material.share_passphrase)
.expect("decrypt share passphrase");
address_key
.key
.verify_detached_signature(&material.share_passphrase_signature, &share_pp)
.expect("verify share passphrase signature");
let share_key = PrivateKey::from_armored(&material.share_key_armored, &share_pp)
.expect("unlock share key");
let folder_pp = share_key
.decrypt_armored_message(&material.folder_passphrase)
.expect("decrypt folder passphrase");
address_key
.key
.verify_detached_signature(&material.folder_passphrase_signature, &folder_pp)
.expect("verify folder passphrase signature");
let folder_key = PrivateKey::from_armored(&material.folder_key_armored, &folder_pp)
.expect("unlock folder key");
let name = share_key
.decrypt_armored_message(&material.folder_name)
.expect("decrypt folder name");
assert_eq!(name, b"root");
let hash_key = folder_key
.decrypt_armored_message(&material.folder_hash_key)
.expect("decrypt folder hash key");
assert_eq!(hash_key.len(), 32);
}
#[test]
fn node_hash_key_round_trips_under_node_key() {
let node = generate_node_key().expect("generate node key");
let armored = generate_node_hash_key(&node.key).expect("generate hash key");
let hash_key = node
.key
.decrypt_armored_message(&armored)
.expect("decrypt hash key");
assert_eq!(hash_key.len(), 32);
}
}