franziskuskiefer / hpke-rs

Pure Rust implementation of HPKE (https://www.rfc-editor.org/rfc/rfc9180.html)
28 stars 14 forks source link

Reuse of encap? #65

Closed jplock closed 4 weeks ago

jplock commented 1 month ago

On the sender, I'm use the Python hybrid-pke library (Python wrapper around this library) and calling setup_sender with a public key and info. That returns a sender_context and encap in bytes.

Is it possible to store the encap bytes, along with the public key and secret key, and later use secret key and encap to call open on a receiver context (assuming I used the same KEM, KDF, and AEAD options on both the sender and receiver)?

With this setup, I'm able to successfully encrypt values, but I've been unable to decrypt anything.

I'm not getting any errors creating the HpkePrivateKey, and setup_receiver is also completing successfully.

Whenever I try and call ctx.open(&aad, &cipher_text), I get OpenError without any information as to what may be the issue.

A related question, I'm assuming I can reuse the same encap value after encrypting multiple individual attributes, is that correct?

jplock commented 1 month ago

I think I figured out what I need to do.

franziskuskiefer commented 1 month ago

I guess I was too slow. Let me know when there are more questions.

jplock commented 1 month ago

Thanks! I’m now storing the encap value alongside the aad tag and cipher text and using the single shot “open”, but I’m still getting OpenError. Anything else I should be looking at?

franziskuskiefer commented 1 month ago

Do you have a minimal example of what you try to do? That would help me to understand better what you're trying to achieve. Generally, reusing key materials is not safe. But maybe I misunderstand what you're trying to achieve.

jplock commented 1 month ago

I'm building a reference implementation of encrypting data but only being able to decrypt the data from with an AWS Nitro Enclave using the AWS Key Management Service (KMS).

I have a symmetric key defined in KMS. I generate a new ECC_NIST_P256 keypair from KMS then using that to encrypt the data using this library (that part completes without error).

Encryption

I'm using hybrid-pke which is a Python wrapper around this library and the code looks like:

from dataclasses import dataclass
import pickle
from typing import Dict, Any

import hybrid_pke

__all__ = ["encrypt_values"]

@dataclass(frozen=True, kw_only=True, slots=True)
class EncryptedData:
    encapped_key: bytes
    ciphertext: bytes
    tag: bytes

    def encode(self) -> bytes:
        return pickle.dumps(self)

def encrypt_value(encap: bytes, sender_context: hybrid_pke.Context, plaintext: bytes) -> EncryptedData:
    aad = b""
    ciphertext: bytes = sender_context.seal(aad, plaintext)
    return EncryptedData(encapped_key=encap, ciphertext=ciphertext, tag=aad)

def encrypt_values(public_key: bytes, plaintext_values: Dict[str, Any]) -> Dict[str, bytes]:
    if not public_key:
        return plaintext_values
    if not plaintext_values:
        return {}

    hpke = hybrid_pke.default(
        kem=hybrid_pke.Kem.DHKEM_P256,
        kdf=hybrid_pke.Kdf.HKDF_SHA256,
        aead=hybrid_pke.Aead.AES_256_GCM,
    )
    info = b""
    encap, sender_context = hpke.setup_sender(public_key, info)

    encrypted_values: Dict[str, bytes] = {}

    for key, value in plaintext_values.items():
        data = encrypt_value(encap, sender_context, str(value).encode())
        encrypted_values[key] = data.encode()

    del plaintext_values

    return encrypted_values

I'm then storing the Python pickled EncryptedData objects.

Decryption

To decrypt the data, I'm sending the pickled EncryptedData objects into a Rust application, along with the encrypted secret key I got from KMS. I use KMS to decrypt the secret key (that part works fine). I'm then using the following methods to try and decrypt the values, but it's failing on the open call with OpenError and I'm not sure why.

use std::collections::BTreeMap;

use anyhow::{anyhow, Result};
use base64::{prelude::BASE64_STANDARD, Engine as _};
use hpke_rs::{Hpke, HpkePrivateKey, Mode};
use hpke_rs_crypto::types::{AeadAlgorithm, KdfAlgorithm, KemAlgorithm};
use hpke_rs_rust_crypto::HpkeRustCrypto as ImplHpkeCrypto;
use serde_json::Value;
use serde_pickle::from_slice;

use crate::models::EncryptedData;

fn decrypt_value(
    hpke: &Hpke<ImplHpkeCrypto>,
    secret_key: &HpkePrivateKey,
    b64_encrypted_value: &str,
) -> Result<String> {
    let encrypted_value = BASE64_STANDARD
        .decode(b64_encrypted_value)
        .map_err(|err| anyhow!("unable to decode b64_encrypted_value: {:?}", err))?;

    println!("[enclave] encrypted_value: {:?}", encrypted_value);

    let encrypted_data: EncryptedData = from_slice(encrypted_value.as_slice(), Default::default())
        .map_err(|err| anyhow!("unable to unpickle encrypted_value: {:?}", err))?;

    println!("[enclave] encrypted_data: {:?}", encrypted_data);

    let info = "".as_bytes();

    let plaintext_value = hpke
        .open(
            &encrypted_data.encapped_key,
            secret_key,
            info,
            &encrypted_data.tag,
            &encrypted_data.ciphertext,
            None,
            None,
            None,
        )
        .map_err(|err| anyhow!("unable to decrypt data: {:?}", err))?;

    println!("[enclave] plaintext_value: {:?}", plaintext_value);

    let string_value = String::from_utf8(plaintext_value)
        .map_err(|err| anyhow!("unable to convert plaintext_value to string: {:?}", err))?;

    println!("[enclave] string_value: {:?}", string_value);

    Ok(string_value)
}

pub fn decrypt_values(
    secret_key: &HpkePrivateKey,
    fields: &BTreeMap<String, String>,
) -> Result<BTreeMap<String, Value>> {
    let hpke = Hpke::<ImplHpkeCrypto>::new(
        Mode::Base,
        KemAlgorithm::DhKemP256,
        KdfAlgorithm::HkdfSha256,
        AeadAlgorithm::Aes256Gcm,
    );

    let decrypted_fields: BTreeMap<String, Value> = {
        let mut decrypted_fields = BTreeMap::new();

        for (field, b64_encrypted_value) in fields {
            let string_value = decrypt_value(&hpke, secret_key, b64_encrypted_value)
                .map_err(|err| anyhow!("unable to decrypt value: {:?}", err))?;

            decrypted_fields.insert(field.to_string(), string_value.into());
        }

        decrypted_fields
    };

    Ok(decrypted_fields)
}

Loading the HpkePrivateKey looks like:

use hpke_rs::HpkePrivateKey;
use p256::{pkcs8::DecodePrivateKey, SecretKey};
...
// Decode the DER PKCS#8 secret key
let sk: SecretKey = SecretKey::from_pkcs8_der(&plaintext_sk).map_err(|err| anyhow!("unable to decode PKCS#8 private key: {:?}", err))?;
Ok(HpkePrivateKey::new(sk.to_bytes().to_vec()))

which doesn't error out, so I'm assuming it's working as intended. There wasn't a direct way to decode the DER PKCS#8 encoded secret key I get back from KMS without using the p256 library first before sending the data into HpkePrivateKey.

thank you for taking a look, I really appreciate it.

franziskuskiefer commented 1 month ago

Thanks for the description!

I don't see anything wrong with what you're doing here. But I can see that maybe some encodings don't line up. Do you have a sample secret key that you get out after decoding the pkcs8 (not a real one please 😉)? That's the most likely culprit.

jplock commented 1 month ago

We can close this issue as I don't think its related to this library. I've tried three different Python HPKE libraries and I can't seem to get this to work when using a KMS generated data key pair. What is super strange, during my decryption test (which is iterating over a Dict[str,str] with field names and encrypted values), I am able to decrypt the first value from the first iteration, but then all other iterations fail to decrypt.

franziskuskiefer commented 1 month ago

hm, that's weird. That sounds like the subsequent encapsulations/encryptions are done differently for some reason. But looking at your code I don't see how that could happen.

jplock commented 4 weeks ago

I figured this out - I was using a sender context when encrypting and was trying to use one-shot decryption. That's not supported.