Skip to main content

acp_runtime/
crypto.rs

1// Copyright 2026 ACP Project
2// Licensed under the Apache License, Version 2.0
3// See LICENSE file for details.
4
5use aes_gcm::aead::{Aead, KeyInit, Payload};
6use aes_gcm::{Aes256Gcm, Nonce};
7use base64::Engine;
8use base64::engine::general_purpose::URL_SAFE;
9use ed25519_dalek::{Signer, Verifier};
10use hkdf::Hkdf;
11use rand::RngCore;
12use serde_json::{Map, Value};
13use sha2::{Digest, Sha256};
14use std::collections::HashMap;
15use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
16
17use crate::errors::{AcpError, AcpResult};
18use crate::json_support;
19use crate::messages::{Envelope, ProtectedPayload, WrappedContentKey};
20
21pub fn b64_encode(value: &[u8]) -> String {
22    URL_SAFE.encode(value)
23}
24
25pub fn b64_decode(value: &str) -> AcpResult<Vec<u8>> {
26    URL_SAFE
27        .decode(value.as_bytes())
28        .map_err(|e| AcpError::Crypto(format!("invalid base64 value: {e}")))
29}
30
31pub fn canonical_json(value: &Value) -> AcpResult<String> {
32    json_support::canonical_json_string(value)
33}
34
35pub fn sha256_hex(value: &[u8]) -> String {
36    let mut hasher = Sha256::new();
37    hasher.update(value);
38    hex::encode(hasher.finalize())
39}
40
41pub fn generate_ed25519_keypair() -> (String, String) {
42    let mut secret = [0u8; 32];
43    rand::thread_rng().fill_bytes(&mut secret);
44    let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
45    let verify_key = signing_key.verifying_key();
46    (
47        b64_encode(&signing_key.to_bytes()),
48        b64_encode(&verify_key.to_bytes()),
49    )
50}
51
52pub fn generate_x25519_keypair() -> (String, String) {
53    let mut secret = [0u8; 32];
54    rand::thread_rng().fill_bytes(&mut secret);
55    let private_key = StaticSecret::from(secret);
56    let public_key = X25519PublicKey::from(&private_key);
57    (
58        b64_encode(&private_key.to_bytes()),
59        b64_encode(public_key.as_bytes()),
60    )
61}
62
63pub fn sign_bytes(data: &[u8], signing_private_key_b64: &str) -> AcpResult<String> {
64    let key_bytes = b64_decode(signing_private_key_b64)?;
65    let key_bytes: [u8; 32] = key_bytes
66        .try_into()
67        .map_err(|_| AcpError::Crypto("invalid Ed25519 private key length".to_string()))?;
68    let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
69    Ok(b64_encode(&signing_key.sign(data).to_bytes()))
70}
71
72pub fn verify_signature(data: &[u8], signature_b64: &str, signing_public_key_b64: &str) -> bool {
73    let Ok(signature_bytes) = b64_decode(signature_b64) else {
74        return false;
75    };
76    let Ok(signature_bytes) = <[u8; 64]>::try_from(signature_bytes) else {
77        return false;
78    };
79    let Ok(public_key_bytes) = b64_decode(signing_public_key_b64) else {
80        return false;
81    };
82    let Ok(public_key_bytes) = <[u8; 32]>::try_from(public_key_bytes) else {
83        return false;
84    };
85    let Ok(signature) = ed25519_dalek::Signature::try_from(signature_bytes.as_slice()) else {
86        return false;
87    };
88    let Ok(verifying_key) = ed25519_dalek::VerifyingKey::from_bytes(&public_key_bytes) else {
89        return false;
90    };
91    verifying_key.verify(data, &signature).is_ok()
92}
93
94pub fn envelope_aad(envelope: &Envelope) -> AcpResult<Vec<u8>> {
95    let mut aad = Map::new();
96    aad.insert(
97        "acp_version".to_string(),
98        Value::String(envelope.acp_version.clone()),
99    );
100    aad.insert(
101        "message_id".to_string(),
102        Value::String(envelope.message_id.clone()),
103    );
104    aad.insert(
105        "operation_id".to_string(),
106        Value::String(envelope.operation_id.clone()),
107    );
108    aad.insert("sender".to_string(), Value::String(envelope.sender.clone()));
109    aad.insert(
110        "recipients".to_string(),
111        Value::Array(
112            envelope
113                .recipients
114                .iter()
115                .map(|recipient| Value::String(recipient.clone()))
116                .collect(),
117        ),
118    );
119    if let Some(tenant) = envelope.tenant.as_ref() {
120        aad.insert("tenant".to_string(), Value::String(tenant.clone()));
121    }
122    json_support::canonical_json_bytes(&Value::Object(aad))
123}
124
125pub fn encrypt_for_recipients(
126    payload: &Map<String, Value>,
127    envelope: &Envelope,
128    recipient_encryption_public_keys: &HashMap<String, String>,
129) -> AcpResult<ProtectedPayload> {
130    let plaintext = canonical_json(&Value::Object(payload.clone()))?.into_bytes();
131    let mut content_key = [0u8; 32];
132    rand::thread_rng().fill_bytes(&mut content_key);
133    let mut nonce = [0u8; 12];
134    rand::thread_rng().fill_bytes(&mut nonce);
135
136    let payload_aad = envelope_aad(envelope)?;
137    let payload_cipher = Aes256Gcm::new_from_slice(&content_key)
138        .map_err(|e| AcpError::Crypto(format!("unable to initialize payload cipher: {e}")))?;
139    let ciphertext = payload_cipher
140        .encrypt(
141            Nonce::from_slice(&nonce),
142            Payload {
143                msg: &plaintext,
144                aad: &payload_aad,
145            },
146        )
147        .map_err(|e| AcpError::Crypto(format!("payload encryption failed: {e}")))?;
148
149    let mut ephemeral_secret_bytes = [0u8; 32];
150    rand::thread_rng().fill_bytes(&mut ephemeral_secret_bytes);
151    let ephemeral_private = StaticSecret::from(ephemeral_secret_bytes);
152    let ephemeral_public = X25519PublicKey::from(&ephemeral_private);
153
154    let mut wrapped_content_keys = Vec::new();
155    for (recipient, recipient_public_key_b64) in recipient_encryption_public_keys {
156        let recipient_public = decode_x25519_public(recipient_public_key_b64)?;
157        let shared_secret = ephemeral_private.diffie_hellman(&recipient_public);
158        let wrap_key = derive_wrap_key(shared_secret.as_bytes(), recipient)?;
159        let mut wrap_nonce = [0u8; 12];
160        rand::thread_rng().fill_bytes(&mut wrap_nonce);
161        let wrap_cipher = Aes256Gcm::new_from_slice(&wrap_key)
162            .map_err(|e| AcpError::Crypto(format!("unable to initialize wrap cipher: {e}")))?;
163        let wrapped_cek = wrap_cipher
164            .encrypt(
165                Nonce::from_slice(&wrap_nonce),
166                Payload {
167                    msg: &content_key,
168                    aad: envelope.message_id.as_bytes(),
169                },
170            )
171            .map_err(|e| AcpError::Crypto(format!("content key wrap failed: {e}")))?;
172        wrapped_content_keys.push(WrappedContentKey {
173            recipient: recipient.to_string(),
174            ephemeral_public_key: b64_encode(ephemeral_public.as_bytes()),
175            nonce: b64_encode(&wrap_nonce),
176            ciphertext: b64_encode(&wrapped_cek),
177        });
178    }
179
180    Ok(ProtectedPayload {
181        nonce: b64_encode(&nonce),
182        ciphertext: b64_encode(&ciphertext),
183        wrapped_content_keys,
184        payload_hash: sha256_hex(&ciphertext),
185        signature_kid: String::new(),
186        signature: String::new(),
187    })
188}
189
190pub fn sign_protected_payload(
191    envelope: &Envelope,
192    protected_payload: &mut ProtectedPayload,
193    signing_private_key_b64: &str,
194    signature_kid: &str,
195) -> AcpResult<()> {
196    protected_payload.signature_kid = signature_kid.to_string();
197    let input = message_signature_input(envelope, protected_payload)?;
198    protected_payload.signature = sign_bytes(&input, signing_private_key_b64)?;
199    Ok(())
200}
201
202pub fn verify_protected_payload_signature(
203    envelope: &Envelope,
204    protected_payload: &ProtectedPayload,
205    sender_signing_public_key_b64: &str,
206) -> bool {
207    if protected_payload.signature.trim().is_empty() {
208        return false;
209    }
210    let Ok(input) = message_signature_input(envelope, protected_payload) else {
211        return false;
212    };
213    verify_signature(
214        &input,
215        &protected_payload.signature,
216        sender_signing_public_key_b64,
217    )
218}
219
220pub fn decrypt_for_recipient(
221    envelope: &Envelope,
222    protected_payload: &ProtectedPayload,
223    recipient_id: &str,
224    recipient_encryption_private_key_b64: &str,
225) -> AcpResult<Map<String, Value>> {
226    let matching = protected_payload
227        .wrapped_content_keys
228        .iter()
229        .find(|item| item.recipient == recipient_id)
230        .ok_or_else(|| {
231            AcpError::Crypto(format!(
232                "No wrapped content key available for recipient {recipient_id}"
233            ))
234        })?;
235
236    let recipient_private = decode_x25519_private(recipient_encryption_private_key_b64)?;
237    let ephemeral_public = decode_x25519_public(&matching.ephemeral_public_key)?;
238    let shared_secret = recipient_private.diffie_hellman(&ephemeral_public);
239    let wrap_key = derive_wrap_key(shared_secret.as_bytes(), recipient_id)?;
240
241    let wrap_cipher = Aes256Gcm::new_from_slice(&wrap_key)
242        .map_err(|e| AcpError::Crypto(format!("unable to initialize wrap cipher: {e}")))?;
243    let wrapped_nonce = b64_decode(&matching.nonce)?;
244    let wrapped_nonce: [u8; 12] = wrapped_nonce
245        .try_into()
246        .map_err(|_| AcpError::Crypto("invalid wrapped nonce length".to_string()))?;
247    let content_key = wrap_cipher
248        .decrypt(
249            Nonce::from_slice(&wrapped_nonce),
250            Payload {
251                msg: &b64_decode(&matching.ciphertext)?,
252                aad: envelope.message_id.as_bytes(),
253            },
254        )
255        .map_err(|e| AcpError::Crypto(format!("failed to unwrap content key: {e}")))?;
256
257    let payload_cipher = Aes256Gcm::new_from_slice(&content_key)
258        .map_err(|e| AcpError::Crypto(format!("unable to initialize payload cipher: {e}")))?;
259    let payload_nonce = b64_decode(&protected_payload.nonce)?;
260    let payload_nonce: [u8; 12] = payload_nonce
261        .try_into()
262        .map_err(|_| AcpError::Crypto("invalid payload nonce length".to_string()))?;
263    let plaintext = payload_cipher
264        .decrypt(
265            Nonce::from_slice(&payload_nonce),
266            Payload {
267                msg: &b64_decode(&protected_payload.ciphertext)?,
268                aad: &envelope_aad(envelope)?,
269            },
270        )
271        .map_err(|e| AcpError::Crypto(format!("failed to decrypt message payload: {e}")))?;
272    let payload_value: Value = serde_json::from_slice(&plaintext)?;
273    match payload_value {
274        Value::Object(map) => Ok(map),
275        _ => Err(AcpError::Crypto(
276            "decrypted payload is not a JSON object".to_string(),
277        )),
278    }
279}
280
281fn message_signature_input(
282    envelope: &Envelope,
283    protected: &ProtectedPayload,
284) -> AcpResult<Vec<u8>> {
285    let value = serde_json::json!({
286        "envelope": envelope,
287        "protected": protected.to_signable_value(),
288    });
289    json_support::canonical_json_bytes(&value)
290}
291
292fn decode_x25519_public(value: &str) -> AcpResult<X25519PublicKey> {
293    let bytes = b64_decode(value)?;
294    let bytes: [u8; 32] = bytes
295        .try_into()
296        .map_err(|_| AcpError::Crypto("invalid X25519 public key length".to_string()))?;
297    Ok(X25519PublicKey::from(bytes))
298}
299
300fn decode_x25519_private(value: &str) -> AcpResult<StaticSecret> {
301    let bytes = b64_decode(value)?;
302    let bytes: [u8; 32] = bytes
303        .try_into()
304        .map_err(|_| AcpError::Crypto("invalid X25519 private key length".to_string()))?;
305    Ok(StaticSecret::from(bytes))
306}
307
308fn derive_wrap_key(shared_secret: &[u8], recipient: &str) -> AcpResult<[u8; 32]> {
309    let hkdf = Hkdf::<Sha256>::new(None, shared_secret);
310    let mut out = [0u8; 32];
311    hkdf.expand(format!("acp-v1-wrap:{recipient}").as_bytes(), &mut out)
312        .map_err(|e| AcpError::Crypto(format!("hkdf expand failed: {e}")))?;
313    Ok(out)
314}