use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use bytes::Bytes;
use pgp::composed::{
ArmorOptions, DetachedSignature, EncryptionCaps, KeyType, MessageBuilder,
SecretKeyParamsBuilder, SignedPublicKey, SignedSecretKey, SubkeyParamsBuilder,
};
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{Notation, SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::types::{CompressionAlgorithm, EncryptionKey, KeyDetails, KeyVersion};
use rand_08::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(crate) fn recipient_encryption_key_public<Op: RecipientOp>(
key: &SignedPublicKey,
op: Op,
) -> Result<Op::Out, CryptoError> {
if key.primary_key.algorithm().can_encrypt() {
return op
.run(&key.primary_key)
.map_err(|e| CryptoError::Encrypt(format!("encrypt to primary key: {e}")));
}
for sub in &key.public_subkeys {
if sub.key.algorithm().can_encrypt() {
return op
.run(&sub.key)
.map_err(|e| CryptoError::Encrypt(format!("encrypt to subkey: {e}")));
}
}
Err(CryptoError::Encrypt(
"public 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_08::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_08::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_08::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_08::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_08::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}")))
}
const SIGNATURE_CONTEXT_NOTATION: &[u8] = b"context@proton.ch";
pub(crate) fn sign_detached_binary_with_context(
signer: &PrivateKey,
data: &[u8],
context: &str,
) -> Result<Vec<u8>, CryptoError> {
let mut rng = rand_08::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}")))?,
Subpacket::critical(SubpacketData::Notation(Notation {
readable: true,
name: Bytes::from_static(SIGNATURE_CONTEXT_NOTATION),
value: Bytes::copy_from_slice(context.as_bytes()),
}))
.map_err(|e| CryptoError::Encrypt(format!("context notation 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}")))?;
let mut bytes = Vec::new();
pgp::ser::Serialize::to_writer(&DetachedSignature::new(signature), &mut bytes)
.map_err(|e| CryptoError::Encrypt(format!("serialize signature: {e}")))?;
Ok(bytes)
}
pub const SHARING_INVITER_CONTEXT: &str = "drive.share-member.inviter";
pub const SHARING_MEMBER_CONTEXT: &str = "drive.share-member.member";
pub const SHARING_EXTERNAL_INVITATION_CONTEXT: &str = "drive.share-member.external-invitation";
pub struct StandardShareMaterial {
pub share_key_armored: String,
pub share_passphrase: String,
pub share_passphrase_signature: String,
pub passphrase_key_packet: String,
pub name_key_packet: String,
pub share_key: PrivateKey,
pub share_session_key: super::content::ContentKey,
}
pub fn build_standard_share_material(
node_key: &PrivateKey,
node_passphrase_session_key: &super::content::ContentKey,
node_name_session_key: &super::content::ContentKey,
address_key: &PrivateKey,
) -> Result<StandardShareMaterial, CryptoError> {
let generated = generate_node_key()?;
let share_session_key = super::content::ContentKey::generate();
let share_passphrase =
share_session_key.encrypt_message_to(&[node_key, address_key], &generated.passphrase)?;
let share_passphrase_signature = address_key.sign_detached(&generated.passphrase)?;
let passphrase_key_packet =
BASE64.encode(node_passphrase_session_key.to_packet(&generated.key)?);
let name_key_packet = BASE64.encode(node_name_session_key.to_packet(&generated.key)?);
Ok(StandardShareMaterial {
share_key_armored: generated.locked_armored,
share_passphrase,
share_passphrase_signature,
passphrase_key_packet,
name_key_packet,
share_key: generated.key,
share_session_key,
})
}
pub fn encrypt_invitation(
share_session_key: &super::content::ContentKey,
inviter_key: &PrivateKey,
invitee_public_key: &super::verify::PublicKey,
) -> Result<(String, String), CryptoError> {
let key_packet = share_session_key.to_packet_for_public(invitee_public_key)?;
let signature =
sign_detached_binary_with_context(inviter_key, &key_packet, SHARING_INVITER_CONTEXT)?;
Ok((BASE64.encode(&key_packet), BASE64.encode(&signature)))
}
pub fn accept_invitation(
base64_key_packet: &str,
invitee_keys: &[PrivateKey],
signing_key: &PrivateKey,
) -> Result<String, CryptoError> {
let key_packet = BASE64
.decode(base64_key_packet)
.map_err(|e| CryptoError::Parse(format!("invitation key packet base64: {e}")))?;
let session_key = invitee_keys
.iter()
.find_map(|k| k.decrypt_content_key(&key_packet).ok())
.ok_or_else(|| {
CryptoError::Decrypt("no invitee key could decrypt the invitation key packet".into())
})?;
let signature = sign_detached_binary_with_context(
signing_key,
&session_key.export()?,
SHARING_MEMBER_CONTEXT,
)?;
Ok(BASE64.encode(&signature))
}
pub fn encrypt_external_invitation(
share_session_key: &super::content::ContentKey,
inviter_key: &PrivateKey,
invitee_email: &str,
) -> Result<String, CryptoError> {
let payload = format!(
"{invitee_email}|{}",
BASE64.encode(share_session_key.export()?)
);
let signature = sign_detached_binary_with_context(
inviter_key,
payload.as_bytes(),
SHARING_EXTERNAL_INVITATION_CONTEXT,
)?;
Ok(BASE64.encode(&signature))
}
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_08::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);
}
#[test]
fn standard_share_material_round_trips() {
use super::super::content::ContentKey;
let node = generate_node_key().expect("generate node key");
let address = generate_node_key().expect("generate address key");
let node_passphrase_sk = ContentKey::generate();
let node_name_sk = ContentKey::generate();
let material = build_standard_share_material(
&node.key,
&node_passphrase_sk,
&node_name_sk,
&address.key,
)
.expect("build share material");
let via_node = node
.key
.decrypt_armored_message(&material.share_passphrase)
.expect("decrypt share passphrase via node key");
let via_address = address
.key
.decrypt_armored_message(&material.share_passphrase)
.expect("decrypt share passphrase via address key");
assert_eq!(via_node, via_address);
address
.key
.verify_detached_signature(&material.share_passphrase_signature, &via_node)
.expect("verify share passphrase signature");
let share_key =
PrivateKey::from_armored(&material.share_key_armored, &via_node).expect("unlock share");
let recovered_pp = share_key
.decrypt_content_key(&BASE64.decode(&material.passphrase_key_packet).unwrap())
.expect("recover passphrase session key");
assert_eq!(
recovered_pp.export().unwrap(),
node_passphrase_sk.export().unwrap()
);
let recovered_name = share_key
.decrypt_content_key(&BASE64.decode(&material.name_key_packet).unwrap())
.expect("recover name session key");
assert_eq!(
recovered_name.export().unwrap(),
node_name_sk.export().unwrap()
);
}
#[test]
fn invitation_key_packet_round_trips_to_invitee() {
use super::super::content::ContentKey;
use super::super::verify::PublicKey;
use pgp::composed::Deserializable as _;
let node = generate_node_key().expect("generate node key");
let address = generate_node_key().expect("generate address key");
let material = build_standard_share_material(
&node.key,
&ContentKey::generate(),
&ContentKey::generate(),
&address.key,
)
.expect("build share material");
let invitee = generate_node_key().expect("generate invitee key");
let invitee_pub_armored = invitee
.key
.signed_public_key()
.to_armored_string(Default::default())
.expect("armor invitee public key");
let invitee_pub = PublicKey::from_armored(&invitee_pub_armored).expect("parse invitee pub");
let (key_packet_b64, signature_b64) =
encrypt_invitation(&material.share_session_key, &address.key, &invitee_pub)
.expect("encrypt invitation");
let key_packet = BASE64.decode(&key_packet_b64).expect("decode key packet");
let recovered = invitee
.key
.decrypt_content_key(&key_packet)
.expect("invitee decrypts key packet");
assert_eq!(
recovered.export().unwrap(),
material.share_session_key.export().unwrap()
);
let sig_bytes = BASE64.decode(&signature_b64).expect("decode signature");
let sig = DetachedSignature::from_bytes(&sig_bytes[..]).expect("parse signature");
sig.signature
.verify(&address.key.signed_public_key(), &key_packet[..])
.expect("verify inviter context signature");
}
#[test]
fn signing_with_a_clone_does_not_corrupt_the_original_for_decryption() {
let address = generate_node_key().expect("generate address key");
let secret = b"share passphrase bytes".to_vec();
let armored = address
.key
.encrypt(&secret)
.expect("encrypt to address key");
assert_eq!(
address
.key
.decrypt_armored_message(&armored)
.expect("decrypt #1"),
secret
);
let clone = address.key.clone();
let _sig = clone
.sign_detached(b"device name")
.expect("sign with clone");
let _enc = clone
.encrypt_and_sign(&clone, b"device name", true, false)
.expect("encrypt+sign with clone");
assert_eq!(
address
.key
.decrypt_armored_message(&armored)
.expect("decrypt #2 after signing with clone"),
secret,
"signing with a clone must not corrupt the original key"
);
}
}