Skip to main content

seq_runtime/crypto/
aes.rs

1//! AES-256-GCM authenticated encryption.
2
3use crate::seqstring::global_string;
4use crate::stack::{Stack, pop, push};
5use crate::value::Value;
6
7use aes_gcm::{
8    Aes256Gcm, Nonce,
9    aead::{Aead, KeyInit as AesKeyInit, OsRng, rand_core::RngCore as AeadRngCore},
10};
11use base64::{Engine, engine::general_purpose::STANDARD};
12
13use super::{AES_GCM_TAG_SIZE, AES_KEY_SIZE, AES_NONCE_SIZE};
14
15/// Encrypt plaintext using AES-256-GCM
16///
17/// Stack effect: ( String String -- String Bool )
18///
19/// Arguments:
20/// - plaintext: The string to encrypt
21/// - key: Hex-encoded 32-byte key (64 hex characters)
22///
23/// Returns:
24/// - ciphertext: base64(nonce || ciphertext || tag)
25/// - success: Bool indicating success
26///
27/// # Safety
28/// Stack must have two String values on top
29#[unsafe(no_mangle)]
30pub unsafe extern "C" fn patch_seq_crypto_aes_gcm_encrypt(stack: Stack) -> Stack {
31    assert!(!stack.is_null(), "crypto.aes-gcm-encrypt: stack is null");
32
33    let (stack, key_val) = unsafe { pop(stack) };
34    let (stack, plaintext_val) = unsafe { pop(stack) };
35
36    match (plaintext_val, key_val) {
37        (Value::String(plaintext), Value::String(key_hex)) => {
38            match aes_gcm_encrypt(plaintext.as_str(), key_hex.as_str()) {
39                Some(ciphertext) => {
40                    let stack = unsafe { push(stack, Value::String(global_string(ciphertext))) };
41                    unsafe { push(stack, Value::Bool(true)) }
42                }
43                None => {
44                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
45                    unsafe { push(stack, Value::Bool(false)) }
46                }
47            }
48        }
49        _ => panic!("crypto.aes-gcm-encrypt: expected two Strings on stack"),
50    }
51}
52
53/// Decrypt ciphertext using AES-256-GCM
54///
55/// Stack effect: ( String String -- String Bool )
56///
57/// Arguments:
58/// - ciphertext: base64(nonce || ciphertext || tag)
59/// - key: Hex-encoded 32-byte key (64 hex characters)
60///
61/// Returns:
62/// - plaintext: The decrypted string
63/// - success: Bool indicating success
64///
65/// # Safety
66/// Stack must have two String values on top
67#[unsafe(no_mangle)]
68pub unsafe extern "C" fn patch_seq_crypto_aes_gcm_decrypt(stack: Stack) -> Stack {
69    assert!(!stack.is_null(), "crypto.aes-gcm-decrypt: stack is null");
70
71    let (stack, key_val) = unsafe { pop(stack) };
72    let (stack, ciphertext_val) = unsafe { pop(stack) };
73
74    match (ciphertext_val, key_val) {
75        (Value::String(ciphertext), Value::String(key_hex)) => {
76            match aes_gcm_decrypt(ciphertext.as_str(), key_hex.as_str()) {
77                Some(plaintext) => {
78                    let stack = unsafe { push(stack, Value::String(global_string(plaintext))) };
79                    unsafe { push(stack, Value::Bool(true)) }
80                }
81                None => {
82                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
83                    unsafe { push(stack, Value::Bool(false)) }
84                }
85            }
86        }
87        _ => panic!("crypto.aes-gcm-decrypt: expected two Strings on stack"),
88    }
89}
90
91pub(super) fn aes_gcm_encrypt(plaintext: &str, key_hex: &str) -> Option<String> {
92    // Decode hex key
93    let key_bytes = hex::decode(key_hex).ok()?;
94    if key_bytes.len() != AES_KEY_SIZE {
95        return None;
96    }
97
98    // Create cipher
99    let cipher = Aes256Gcm::new_from_slice(&key_bytes).ok()?;
100
101    // Generate random nonce
102    let mut nonce_bytes = [0u8; AES_NONCE_SIZE];
103    OsRng.fill_bytes(&mut nonce_bytes);
104    let nonce = Nonce::from_slice(&nonce_bytes);
105
106    // Encrypt
107    let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes()).ok()?;
108
109    // Combine: nonce || ciphertext (tag is appended by aes-gcm)
110    let mut combined = Vec::with_capacity(AES_NONCE_SIZE + ciphertext.len());
111    combined.extend_from_slice(&nonce_bytes);
112    combined.extend_from_slice(&ciphertext);
113
114    Some(STANDARD.encode(&combined))
115}
116
117pub(super) fn aes_gcm_decrypt(ciphertext_b64: &str, key_hex: &str) -> Option<String> {
118    // Decode base64
119    let combined = STANDARD.decode(ciphertext_b64).ok()?;
120    if combined.len() < AES_NONCE_SIZE + AES_GCM_TAG_SIZE {
121        // At minimum: nonce + tag
122        return None;
123    }
124
125    // Decode hex key
126    let key_bytes = hex::decode(key_hex).ok()?;
127    if key_bytes.len() != AES_KEY_SIZE {
128        return None;
129    }
130
131    // Split nonce and ciphertext
132    let (nonce_bytes, ciphertext) = combined.split_at(AES_NONCE_SIZE);
133    let nonce = Nonce::from_slice(nonce_bytes);
134
135    // Create cipher and decrypt
136    let cipher = Aes256Gcm::new_from_slice(&key_bytes).ok()?;
137    let plaintext_bytes = cipher.decrypt(nonce, ciphertext).ok()?;
138
139    String::from_utf8(plaintext_bytes).ok()
140}