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 value = serde_json::json!({
96        "acp_version": envelope.acp_version,
97        "message_id": envelope.message_id,
98        "operation_id": envelope.operation_id,
99        "sender": envelope.sender,
100        "recipients": envelope.recipients,
101    });
102    json_support::canonical_json_bytes(&value)
103}
104
105pub fn encrypt_for_recipients(
106    payload: &Map<String, Value>,
107    envelope: &Envelope,
108    recipient_encryption_public_keys: &HashMap<String, String>,
109) -> AcpResult<ProtectedPayload> {
110    let plaintext = canonical_json(&Value::Object(payload.clone()))?.into_bytes();
111    let mut content_key = [0u8; 32];
112    rand::thread_rng().fill_bytes(&mut content_key);
113    let mut nonce = [0u8; 12];
114    rand::thread_rng().fill_bytes(&mut nonce);
115
116    let payload_aad = envelope_aad(envelope)?;
117    let payload_cipher = Aes256Gcm::new_from_slice(&content_key)
118        .map_err(|e| AcpError::Crypto(format!("unable to initialize payload cipher: {e}")))?;
119    let ciphertext = payload_cipher
120        .encrypt(
121            Nonce::from_slice(&nonce),
122            Payload {
123                msg: &plaintext,
124                aad: &payload_aad,
125            },
126        )
127        .map_err(|e| AcpError::Crypto(format!("payload encryption failed: {e}")))?;
128
129    let mut ephemeral_secret_bytes = [0u8; 32];
130    rand::thread_rng().fill_bytes(&mut ephemeral_secret_bytes);
131    let ephemeral_private = StaticSecret::from(ephemeral_secret_bytes);
132    let ephemeral_public = X25519PublicKey::from(&ephemeral_private);
133
134    let mut wrapped_content_keys = Vec::new();
135    for (recipient, recipient_public_key_b64) in recipient_encryption_public_keys {
136        let recipient_public = decode_x25519_public(recipient_public_key_b64)?;
137        let shared_secret = ephemeral_private.diffie_hellman(&recipient_public);
138        let wrap_key = derive_wrap_key(shared_secret.as_bytes(), recipient)?;
139        let mut wrap_nonce = [0u8; 12];
140        rand::thread_rng().fill_bytes(&mut wrap_nonce);
141        let wrap_cipher = Aes256Gcm::new_from_slice(&wrap_key)
142            .map_err(|e| AcpError::Crypto(format!("unable to initialize wrap cipher: {e}")))?;
143        let wrapped_cek = wrap_cipher
144            .encrypt(
145                Nonce::from_slice(&wrap_nonce),
146                Payload {
147                    msg: &content_key,
148                    aad: envelope.message_id.as_bytes(),
149                },
150            )
151            .map_err(|e| AcpError::Crypto(format!("content key wrap failed: {e}")))?;
152        wrapped_content_keys.push(WrappedContentKey {
153            recipient: recipient.to_string(),
154            ephemeral_public_key: b64_encode(ephemeral_public.as_bytes()),
155            nonce: b64_encode(&wrap_nonce),
156            ciphertext: b64_encode(&wrapped_cek),
157        });
158    }
159
160    Ok(ProtectedPayload {
161        nonce: b64_encode(&nonce),
162        ciphertext: b64_encode(&ciphertext),
163        wrapped_content_keys,
164        payload_hash: sha256_hex(&ciphertext),
165        signature_kid: String::new(),
166        signature: String::new(),
167    })
168}
169
170pub fn sign_protected_payload(
171    envelope: &Envelope,
172    protected_payload: &mut ProtectedPayload,
173    signing_private_key_b64: &str,
174    signature_kid: &str,
175) -> AcpResult<()> {
176    protected_payload.signature_kid = signature_kid.to_string();
177    let input = message_signature_input(envelope, protected_payload)?;
178    protected_payload.signature = sign_bytes(&input, signing_private_key_b64)?;
179    Ok(())
180}
181
182pub fn verify_protected_payload_signature(
183    envelope: &Envelope,
184    protected_payload: &ProtectedPayload,
185    sender_signing_public_key_b64: &str,
186) -> bool {
187    if protected_payload.signature.trim().is_empty() {
188        return false;
189    }
190    let Ok(input) = message_signature_input(envelope, protected_payload) else {
191        return false;
192    };
193    verify_signature(
194        &input,
195        &protected_payload.signature,
196        sender_signing_public_key_b64,
197    )
198}
199
200pub fn decrypt_for_recipient(
201    envelope: &Envelope,
202    protected_payload: &ProtectedPayload,
203    recipient_id: &str,
204    recipient_encryption_private_key_b64: &str,
205) -> AcpResult<Map<String, Value>> {
206    let matching = protected_payload
207        .wrapped_content_keys
208        .iter()
209        .find(|item| item.recipient == recipient_id)
210        .ok_or_else(|| {
211            AcpError::Crypto(format!(
212                "No wrapped content key available for recipient {recipient_id}"
213            ))
214        })?;
215
216    let recipient_private = decode_x25519_private(recipient_encryption_private_key_b64)?;
217    let ephemeral_public = decode_x25519_public(&matching.ephemeral_public_key)?;
218    let shared_secret = recipient_private.diffie_hellman(&ephemeral_public);
219    let wrap_key = derive_wrap_key(shared_secret.as_bytes(), recipient_id)?;
220
221    let wrap_cipher = Aes256Gcm::new_from_slice(&wrap_key)
222        .map_err(|e| AcpError::Crypto(format!("unable to initialize wrap cipher: {e}")))?;
223    let wrapped_nonce = b64_decode(&matching.nonce)?;
224    let wrapped_nonce: [u8; 12] = wrapped_nonce
225        .try_into()
226        .map_err(|_| AcpError::Crypto("invalid wrapped nonce length".to_string()))?;
227    let content_key = wrap_cipher
228        .decrypt(
229            Nonce::from_slice(&wrapped_nonce),
230            Payload {
231                msg: &b64_decode(&matching.ciphertext)?,
232                aad: envelope.message_id.as_bytes(),
233            },
234        )
235        .map_err(|e| AcpError::Crypto(format!("failed to unwrap content key: {e}")))?;
236
237    let payload_cipher = Aes256Gcm::new_from_slice(&content_key)
238        .map_err(|e| AcpError::Crypto(format!("unable to initialize payload cipher: {e}")))?;
239    let payload_nonce = b64_decode(&protected_payload.nonce)?;
240    let payload_nonce: [u8; 12] = payload_nonce
241        .try_into()
242        .map_err(|_| AcpError::Crypto("invalid payload nonce length".to_string()))?;
243    let plaintext = payload_cipher
244        .decrypt(
245            Nonce::from_slice(&payload_nonce),
246            Payload {
247                msg: &b64_decode(&protected_payload.ciphertext)?,
248                aad: &envelope_aad(envelope)?,
249            },
250        )
251        .map_err(|e| AcpError::Crypto(format!("failed to decrypt message payload: {e}")))?;
252    let payload_value: Value = serde_json::from_slice(&plaintext)?;
253    match payload_value {
254        Value::Object(map) => Ok(map),
255        _ => Err(AcpError::Crypto(
256            "decrypted payload is not a JSON object".to_string(),
257        )),
258    }
259}
260
261fn message_signature_input(
262    envelope: &Envelope,
263    protected: &ProtectedPayload,
264) -> AcpResult<Vec<u8>> {
265    let value = serde_json::json!({
266        "envelope": envelope,
267        "protected": protected.to_signable_value(),
268    });
269    json_support::canonical_json_bytes(&value)
270}
271
272fn decode_x25519_public(value: &str) -> AcpResult<X25519PublicKey> {
273    let bytes = b64_decode(value)?;
274    let bytes: [u8; 32] = bytes
275        .try_into()
276        .map_err(|_| AcpError::Crypto("invalid X25519 public key length".to_string()))?;
277    Ok(X25519PublicKey::from(bytes))
278}
279
280fn decode_x25519_private(value: &str) -> AcpResult<StaticSecret> {
281    let bytes = b64_decode(value)?;
282    let bytes: [u8; 32] = bytes
283        .try_into()
284        .map_err(|_| AcpError::Crypto("invalid X25519 private key length".to_string()))?;
285    Ok(StaticSecret::from(bytes))
286}
287
288fn derive_wrap_key(shared_secret: &[u8], recipient: &str) -> AcpResult<[u8; 32]> {
289    let hkdf = Hkdf::<Sha256>::new(None, shared_secret);
290    let mut out = [0u8; 32];
291    hkdf.expand(format!("acp-v1-wrap:{recipient}").as_bytes(), &mut out)
292        .map_err(|e| AcpError::Crypto(format!("hkdf expand failed: {e}")))?;
293    Ok(out)
294}