1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//! DML crypto / secret-payload helpers extracted from `impl_dml`.
//!
//! Behaviour-preserving move (issue #1634). Names and behaviour are unchanged
//! from `impl_dml`; `resolve_crypto_sentinel` stays `pub(crate)` (called from
//! `impl_dml` and `impl_dml_support`) and `decrypt_secret_payload` stays
//! `pub(crate)` and importable at a stable path — its single external import
//! in `impl_core` is updated to `super::impl_dml_crypto::decrypt_secret_payload`.
use super::*;
/// Sentinel prefix produced by the parser for `PASSWORD('...')` and
/// `SECRET('...')` literals. The runtime strips this marker and
/// applies the actual crypto transform during INSERT execution.
pub(crate) const PLAINTEXT_SENTINEL: &str = "@@plain@@";
impl RedDBRuntime {
/// Strip the plaintext sentinel from a `Value::Password` or
/// `Value::Secret` produced by the parser and apply the real
/// crypto transform. `Password` is always hashed with argon2id.
/// `Secret` is encrypted with AES-256-GCM keyed by the vault
/// when `red.config.secret.auto_encrypt = true` (default).
pub(crate) fn resolve_crypto_sentinel(&self, value: Value) -> RedDBResult<Value> {
match value {
Value::Password(marked) => {
if let Some(plain) = marked.strip_prefix(PLAINTEXT_SENTINEL) {
Ok(Value::Password(crate::auth::store::hash_password(plain)))
} else {
Ok(Value::Password(marked))
}
}
Value::Secret(bytes) => {
if bytes.starts_with(PLAINTEXT_SENTINEL.as_bytes()) {
if !self.secret_auto_encrypt() {
return Err(RedDBError::Query(
"SECRET() literal rejected: red.config.secret.auto_encrypt \
is false. Insert pre-encrypted bytes directly instead."
.to_string(),
));
}
let key = self.secret_aes_key().ok_or_else(|| {
RedDBError::Query(
"SECRET() column encryption requires a bootstrapped \
vault (red.secret.aes_key is missing). Start the server \
with --vault to enable."
.to_string(),
)
})?;
let plain = &bytes[PLAINTEXT_SENTINEL.len()..];
Ok(Value::Secret(encrypt_secret_payload(&key, plain)))
} else {
Ok(Value::Secret(bytes))
}
}
other => Ok(other),
}
}
}
/// Encode an AES-256-GCM ciphertext as `[12-byte nonce][ciphertext||tag]`.
/// This is the on-disk representation of `Value::Secret`.
fn encrypt_secret_payload(key: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
let nonce_bytes = crate::auth::store::random_bytes(12);
let mut nonce = [0u8; 12];
nonce.copy_from_slice(&nonce_bytes[..12]);
let ct = crate::crypto::aes_gcm::aes256_gcm_encrypt(key, &nonce, b"reddb.secret", plaintext);
let mut out = Vec::with_capacity(12 + ct.len());
out.extend_from_slice(&nonce);
out.extend_from_slice(&ct);
out
}
/// Decode a `Value::Secret` payload back to plaintext. Returns
/// `None` when the payload is too short or AES-GCM authentication
/// fails (tampered or wrong key).
pub(crate) fn decrypt_secret_payload(key: &[u8; 32], payload: &[u8]) -> Option<Vec<u8>> {
if payload.len() < 12 {
return None;
}
let mut nonce = [0u8; 12];
nonce.copy_from_slice(&payload[..12]);
crate::crypto::aes_gcm::aes256_gcm_decrypt(key, &nonce, b"reddb.secret", &payload[12..]).ok()
}