reddb_server/runtime/
impl_dml_crypto.rs1use super::*;
10
11pub(crate) const PLAINTEXT_SENTINEL: &str = "@@plain@@";
15
16#[derive(Debug)]
17pub(crate) struct SecretInputReadLimit {
18 pub required_bytes: usize,
19 pub limit_bytes: usize,
20}
21
22impl From<SecretInputReadLimit> for RedDBError {
23 fn from(limit: SecretInputReadLimit) -> Self {
24 crate::storage::unified::StoreError::ReadLimitExceeded {
25 required_bytes: limit.required_bytes,
26 limit_bytes: limit.limit_bytes,
27 }
28 .into()
29 }
30}
31
32impl RedDBRuntime {
33 pub(crate) fn resolve_secret_column_value(
34 &self,
35 value: Value,
36 is_secret_column: bool,
37 ) -> RedDBResult<Value> {
38 let value = if is_secret_column {
39 match value {
40 Value::Text(text) => {
41 let key = self.secret_aes_key().ok_or_else(|| {
42 RedDBError::Query("SECRET column requires an available vault key".into())
43 })?;
44 Value::Secret(encrypt_secret_payload(&key, text.as_bytes()))
45 }
46 Value::Null | Value::Secret(_) => value,
47 _ => {
48 return Err(RedDBError::Query(
49 "SECRET columns accept text or encrypted secrets".into(),
50 ))
51 }
52 }
53 } else {
54 value
55 };
56 self.resolve_crypto_sentinel(value)
57 }
58
59 pub(crate) fn secret_query_input(&self, value: Value) -> Option<Value> {
60 self.decode_secret_query_input(value, usize::MAX)
64 .ok()
65 .flatten()
66 }
67
68 pub(crate) fn try_secret_query_input(
69 &self,
70 value: Value,
71 ) -> Result<Option<Value>, SecretInputReadLimit> {
72 let limit_bytes =
73 usize::try_from(self.memory_accounting().budget().resolved_bytes).unwrap_or(usize::MAX);
74 self.decode_secret_query_input(value, limit_bytes)
75 }
76
77 fn decode_secret_query_input(
78 &self,
79 value: Value,
80 limit_bytes: usize,
81 ) -> Result<Option<Value>, SecretInputReadLimit> {
82 let Value::Secret(payload) = value else {
83 return Ok(Some(value));
84 };
85 let Some(key) = self.secret_aes_key() else {
86 return Ok(None);
87 };
88 let cipher_bytes = payload.len().saturating_mul(2);
89 let crypto_bytes = cipher_bytes.saturating_add(payload.len());
90 if crypto_bytes > limit_bytes {
91 return Err(SecretInputReadLimit {
92 required_bytes: crypto_bytes,
93 limit_bytes,
94 });
95 }
96 if let Some(plain) = decrypt_secret_payload(&key, &payload) {
97 return Ok(String::from_utf8(plain).ok().map(Value::text));
98 }
99 if payload.len() < 12 {
100 return Ok(None);
101 }
102 let nonce: &[u8; 12] = payload[..12].try_into().expect("validated nonce length");
103 let Ok(plain) = crate::crypto::aes_gcm::aes256_gcm_decrypt(
104 &key,
105 nonce,
106 b"reddb.secret.value",
107 &payload[12..],
108 ) else {
109 return Ok(None);
110 };
111 let (value, consumed) =
112 match Value::from_bytes_with_limit(&plain, limit_bytes.saturating_sub(cipher_bytes)) {
113 Ok(decoded) => decoded,
114 Err(reddb_types::ValueError::ReadLimitExceeded { required_bytes, .. }) => {
115 return Err(SecretInputReadLimit {
116 required_bytes: required_bytes.saturating_add(cipher_bytes),
117 limit_bytes,
118 });
119 }
120 Err(_) => return Ok(None),
121 };
122 Ok((consumed == plain.len()).then_some(value))
123 }
124
125 pub(crate) fn secret_query_output(&self, value: Value) -> RedDBResult<Value> {
128 let key = self.secret_aes_key().ok_or_else(|| {
129 RedDBError::Query("secret expression requires an available vault key".into())
130 })?;
131 let nonce_bytes = crate::auth::store::random_bytes(12);
132 let nonce: &[u8; 12] = nonce_bytes
133 .as_slice()
134 .try_into()
135 .expect("invariant: generated nonce has twelve bytes");
136 let ciphertext = crate::crypto::aes_gcm::aes256_gcm_encrypt(
137 &key,
138 nonce,
139 b"reddb.secret.value",
140 &value.to_bytes(),
141 );
142 let mut payload = nonce_bytes;
143 payload.extend_from_slice(&ciphertext);
144 Ok(Value::Secret(payload))
145 }
146 pub(crate) fn resolve_crypto_sentinel(&self, value: Value) -> RedDBResult<Value> {
152 match value {
153 Value::Password(marked) => {
154 if let Some(plain) = marked.strip_prefix(PLAINTEXT_SENTINEL) {
155 Ok(Value::Password(crate::auth::store::hash_password(plain)))
156 } else {
157 Ok(Value::Password(marked))
158 }
159 }
160 Value::Secret(bytes) => {
161 if bytes.starts_with(PLAINTEXT_SENTINEL.as_bytes()) {
162 if !self.secret_auto_encrypt() {
163 return Err(RedDBError::Query(
164 "SECRET() literal rejected: red.config.secret.auto_encrypt \
165 is false. Insert pre-encrypted bytes directly instead."
166 .to_string(),
167 ));
168 }
169 let key = self.secret_aes_key().ok_or_else(|| {
170 RedDBError::Query(
171 "SECRET() column encryption requires a bootstrapped \
172 vault (red.secret.aes_key is missing). Start the server \
173 with --vault to enable."
174 .to_string(),
175 )
176 })?;
177 let plain = &bytes[PLAINTEXT_SENTINEL.len()..];
178 Ok(Value::Secret(encrypt_secret_payload(&key, plain)))
179 } else {
180 Ok(Value::Secret(bytes))
181 }
182 }
183 other => Ok(other),
184 }
185 }
186}
187
188fn encrypt_secret_payload(key: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
191 let nonce_bytes = crate::auth::store::random_bytes(12);
192 let mut nonce = [0u8; 12];
193 nonce.copy_from_slice(&nonce_bytes[..12]);
194 let ct = crate::crypto::aes_gcm::aes256_gcm_encrypt(key, &nonce, b"reddb.secret", plaintext);
195 let mut out = Vec::with_capacity(12 + ct.len());
196 out.extend_from_slice(&nonce);
197 out.extend_from_slice(&ct);
198 out
199}
200
201pub(crate) fn decrypt_secret_payload(key: &[u8; 32], payload: &[u8]) -> Option<Vec<u8>> {
205 if payload.len() < 12 {
206 return None;
207 }
208 let mut nonce = [0u8; 12];
209 nonce.copy_from_slice(&payload[..12]);
210 crate::crypto::aes_gcm::aes256_gcm_decrypt(key, &nonce, b"reddb.secret", &payload[12..]).ok()
211}