Skip to main content

beam/sea/
decrypt.rs

1#![allow(deprecated)]
2//! AES-GCM decryption
3//! Reverse of encrypt.rs
4
5use super::encrypt::derive_aes_key_sync;
6use super::{KeyPair, SeaError};
7use aes_gcm::{
8    Aes256Gcm, Nonce,
9    aead::{Aead, KeyInit},
10};
11use base64::prelude::*;
12use serde_json::Value;
13
14/// Decrypt data using AES-256-GCM
15///
16/// Parses {ct: ciphertext, iv: nonce, s: salt} format.
17/// Shared decrypt: their_epub = Some
18/// Self decrypt: their_epub = None
19pub async fn decrypt(
20    encrypted: &Value,
21    pair: &KeyPair,
22    their_epub: Option<&str>,
23) -> Result<Value, SeaError> {
24    // Extract fields
25    let ct = encrypted
26        .get("ct")
27        .and_then(|v| v.as_str())
28        .ok_or_else(|| SeaError::Decryption("missing ct".to_string()))?;
29
30    let iv = encrypted
31        .get("iv")
32        .and_then(|v| v.as_str())
33        .ok_or_else(|| SeaError::Decryption("missing iv".to_string()))?;
34
35    let s = encrypted
36        .get("s")
37        .and_then(|v| v.as_str())
38        .ok_or_else(|| SeaError::Decryption("missing s".to_string()))?;
39
40    // Decode from base64
41    let ciphertext = BASE64_URL_SAFE_NO_PAD
42        .decode(ct)
43        .map_err(|_| SeaError::Decryption("invalid ct base64".to_string()))?;
44
45    let nonce_bytes = BASE64_URL_SAFE_NO_PAD
46        .decode(iv)
47        .map_err(|_| SeaError::Decryption("invalid iv base64".to_string()))?;
48
49    let salt_bytes = BASE64_URL_SAFE_NO_PAD
50        .decode(s)
51        .map_err(|_| SeaError::Decryption("invalid s base64".to_string()))?;
52
53    // Clone data for spawn_blocking closure
54    let pair = pair.clone();
55    let their_epub = their_epub.map(|s| s.to_string());
56    let salt_owned = salt_bytes;
57    let nonce_owned = nonce_bytes;
58
59    // Run SHA-256 KDF + AES-GCM in spawn_blocking
60    let plaintext = tokio::task::spawn_blocking(move || {
61        // Derive AES key
62        let aes_key = if let Some(ref their_pub) = their_epub {
63            // Shared decryption: ECDH → SHA-256
64            let shared_secret = super::secret::secret_sync(their_pub, &pair)?;
65            derive_aes_key_sync(&shared_secret, &salt_owned)?
66        } else {
67            // Self decryption: epriv directly
68            let epriv = pair
69                .epriv_key
70                .as_ref()
71                .ok_or_else(|| SeaError::Decryption("missing epriv key".to_string()))?;
72            derive_aes_key_sync(epriv, &salt_owned)?
73        };
74
75        // Create AES-GCM cipher
76        let cipher = Aes256Gcm::new_from_slice(&aes_key)
77            .map_err(|e| SeaError::Decryption(format!("failed to create cipher: {}", e)))?;
78
79        // Create nonce from IV
80        let nonce = Nonce::from_slice(&nonce_owned);
81
82        // Decrypt
83        let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|_| {
84            SeaError::Decryption("decryption failed — tampered or wrong key".to_string())
85        })?;
86
87        Ok::<Vec<u8>, SeaError>(plaintext)
88    })
89    .await
90    .map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?;
91
92    let plaintext = plaintext?;
93
94    // Parse JSON
95    let plaintext_str = String::from_utf8(plaintext)
96        .map_err(|_| SeaError::Decryption("invalid UTF-8 in plaintext".to_string()))?;
97
98    serde_json::from_str(&plaintext_str)
99        .map_err(|e| SeaError::Decryption(format!("invalid JSON in plaintext: {}", e)))
100}
101
102/// Decrypt data using a raw symmetric key (AES-256-GCM, no ECDH/PBKDF2)
103///
104/// # Requirements
105/// * `key` must be exactly 32 bytes (AES-256 key size)
106/// * `encrypted` must be in `{ct, iv}` format (no `s` field, as no PBKDF2 was used)
107///
108/// Use this when the key material is already derived via ECDH or another KDF.
109pub async fn decrypt_symmetric(encrypted: &Value, key: &[u8]) -> Result<Value, SeaError> {
110    if key.len() != 32 {
111        return Err(SeaError::Decryption(format!(
112            "decrypt_symmetric: key must be 32 bytes, got {}",
113            key.len()
114        )));
115    }
116
117    let ct = encrypted
118        .get("ct")
119        .and_then(|v| v.as_str())
120        .ok_or_else(|| SeaError::Decryption("missing ct".to_string()))?;
121
122    let iv = encrypted
123        .get("iv")
124        .and_then(|v| v.as_str())
125        .ok_or_else(|| SeaError::Decryption("missing iv".to_string()))?;
126
127    let ciphertext = BASE64_URL_SAFE_NO_PAD
128        .decode(ct)
129        .map_err(|_| SeaError::Decryption("invalid ct base64".to_string()))?;
130
131    let nonce_bytes = BASE64_URL_SAFE_NO_PAD
132        .decode(iv)
133        .map_err(|_| SeaError::Decryption("invalid iv base64".to_string()))?;
134
135    let key_owned = key.to_vec();
136    let nonce_owned = nonce_bytes;
137
138    let plaintext = tokio::task::spawn_blocking(move || {
139        let cipher = Aes256Gcm::new_from_slice(&key_owned)
140            .map_err(|e| SeaError::Decryption(format!("failed to create cipher: {}", e)))?;
141
142        let nonce = Nonce::from_slice(&nonce_owned);
143
144        let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|_| {
145            SeaError::Decryption("symmetric decryption failed — tampered or wrong key".to_string())
146        })?;
147
148        Ok::<Vec<u8>, SeaError>(plaintext)
149    })
150    .await
151    .map_err(|e| SeaError::Crypto(format!("task join error: {}", e)))?;
152
153    let plaintext = plaintext?;
154
155    let plaintext_str = String::from_utf8(plaintext)
156        .map_err(|_| SeaError::Decryption("invalid UTF-8 in plaintext".to_string()))?;
157
158    serde_json::from_str(&plaintext_str)
159        .map_err(|e| SeaError::Decryption(format!("invalid JSON in plaintext: {}", e)))
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::sea::generate_pair;
166    use serde_json::json;
167
168    #[tokio::test]
169    async fn test_decrypt_self_encrypted() {
170        let pair = generate_pair().await.unwrap();
171        let data = json!({"secret": "data"});
172        let encrypted = super::super::encrypt::encrypt(&data, &pair, None)
173            .await
174            .unwrap();
175        let decrypted = decrypt(&encrypted, &pair, None).await.unwrap();
176        assert_eq!(decrypted, data);
177    }
178
179    #[tokio::test]
180    async fn test_decrypt_wrong_key_fails() {
181        let alice = generate_pair().await.unwrap();
182        let bob = generate_pair().await.unwrap();
183        let data = json!("secret");
184        let encrypted = super::super::encrypt::encrypt(&data, &alice, None)
185            .await
186            .unwrap();
187        assert!(decrypt(&encrypted, &bob, None).await.is_err());
188    }
189
190    #[tokio::test]
191    async fn test_decrypt_missing_field_fails() {
192        let pair = generate_pair().await.unwrap();
193        let bad_data = json!({"ct": "abc"}); // missing iv and s
194        assert!(decrypt(&bad_data, &pair, None).await.is_err());
195    }
196
197    #[tokio::test]
198    async fn test_decrypt_symmetric_bad_key_length() {
199        let key = [0u8; 16]; // too short
200        let encrypted = json!({"ct": "abc", "iv": "def"});
201        assert!(decrypt_symmetric(&encrypted, &key).await.is_err());
202    }
203}