1use std::collections::HashMap;
6
7use serde_json::{Map, Value};
8use ssp2::primitives::{RawJson, Reader, Writer};
9use ssp2::segment::{decode_row, encode_row, Column, ColumnType, ColumnValue, Row};
10use ssp2::util::utf16_lt;
11
12use crate::schema::TableSchema;
13
14#[derive(Debug, Clone, Default)]
19pub struct EncryptionConfig {
20 pub keys: HashMap<String, Vec<u8>>,
21}
22
23impl EncryptionConfig {
24 pub fn is_empty(&self) -> bool {
25 self.keys.is_empty()
26 }
27
28 pub fn key_id_for(&self, table: &str, _row_id: &str) -> String {
30 table.to_owned()
31 }
32}
33
34pub fn bytes_to_hex(bytes: &[u8]) -> String {
35 let mut out = String::with_capacity(bytes.len() * 2);
36 for b in bytes {
37 out.push_str(&format!("{b:02x}"));
38 }
39 out
40}
41
42pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
43 if !hex.len().is_multiple_of(2) {
44 return Err("odd-length hex string".to_owned());
45 }
46 let mut out = Vec::with_capacity(hex.len() / 2);
47 let bytes = hex.as_bytes();
48 for pair in bytes.chunks(2) {
49 let s = std::str::from_utf8(pair).map_err(|_| "non-ASCII hex".to_owned())?;
50 out.push(u8::from_str_radix(s, 16).map_err(|e| format!("bad hex: {e}"))?);
51 }
52 Ok(out)
53}
54
55pub fn json_to_column_value(
58 column: &Column,
59 value: Option<&Value>,
60) -> Result<Option<ColumnValue>, String> {
61 let value = match value {
62 None | Some(Value::Null) => return Ok(None),
63 Some(v) => v,
64 };
65 let fail = |expected: &str| {
66 Err(format!(
67 "column {:?}: expected {expected}, got {value}",
68 column.name
69 ))
70 };
71 match column.ty {
72 ColumnType::String => match value.as_str() {
73 Some(s) => Ok(Some(ColumnValue::String(s.to_owned()))),
74 None => fail("a string"),
75 },
76 ColumnType::Integer => match value.as_i64() {
77 Some(i) => Ok(Some(ColumnValue::Integer(i))),
78 None => fail("an integer"),
79 },
80 ColumnType::Float => match value.as_f64() {
81 Some(f) => Ok(Some(ColumnValue::Float(f))),
82 None => fail("a number"),
83 },
84 ColumnType::Boolean => match value.as_bool() {
85 Some(b) => Ok(Some(ColumnValue::Boolean(b))),
86 None => fail("a boolean"),
87 },
88 ColumnType::Json => match value.as_str() {
90 Some(s) => Ok(Some(ColumnValue::Json(RawJson(s.to_owned())))),
91 None => fail("a raw JSON string"),
92 },
93 ColumnType::BlobRef => match value.as_str() {
95 Some(s) => Ok(Some(ColumnValue::BlobRef(RawJson(s.to_owned())))),
96 None => fail("a raw BlobRef JSON string"),
97 },
98 ColumnType::Bytes => match value.get("$bytes").and_then(Value::as_str) {
99 Some(hex) => Ok(Some(ColumnValue::Bytes(hex_to_bytes(hex)?))),
100 None => fail("a {\"$bytes\": hex} object"),
101 },
102 ColumnType::Crdt => match value.get("$bytes").and_then(Value::as_str) {
104 Some(hex) => Ok(Some(ColumnValue::Crdt(hex_to_bytes(hex)?))),
105 None => fail("a {\"$bytes\": hex} object"),
106 },
107 }
108}
109
110pub fn column_value_to_json(value: &Option<ColumnValue>) -> Value {
112 match value {
113 None => Value::Null,
114 Some(ColumnValue::String(s)) => Value::from(s.clone()),
115 Some(ColumnValue::Integer(i)) => Value::from(*i),
116 Some(ColumnValue::Float(f)) => {
117 serde_json::Number::from_f64(*f).map_or(Value::Null, Value::Number)
118 }
119 Some(ColumnValue::Boolean(b)) => Value::from(*b),
120 Some(ColumnValue::Json(raw)) => Value::from(raw.0.clone()),
121 Some(ColumnValue::BlobRef(raw)) => Value::from(raw.0.clone()),
122 Some(ColumnValue::Bytes(bytes)) => {
123 let mut map = Map::new();
124 map.insert("$bytes".to_owned(), Value::from(bytes_to_hex(bytes)));
125 Value::Object(map)
126 }
127 Some(ColumnValue::Crdt(bytes)) => {
128 let mut map = Map::new();
129 map.insert("$bytes".to_owned(), Value::from(bytes_to_hex(bytes)));
130 Value::Object(map)
131 }
132 }
133}
134
135pub fn encode_row_json(
140 table: &TableSchema,
141 row_id: &str,
142 values: &Map<String, Value>,
143 encryption: &EncryptionConfig,
144) -> Result<Vec<u8>, String> {
145 let mut row: Row = Vec::with_capacity(table.columns.len());
147 for column in &table.columns {
148 row.push(json_to_column_value(column, values.get(&column.name))?);
149 }
150 if table.has_encrypted_columns() {
151 encrypt_row(table, row_id, &mut row, encryption)?;
152 }
153 let mut w = Writer::new();
155 encode_row(&mut w, &table.wire_columns, &row);
156 Ok(w.into_bytes())
157}
158
159pub fn decode_row_bytes(
163 table: &TableSchema,
164 payload: &[u8],
165 encryption: &EncryptionConfig,
166) -> Result<Row, String> {
167 let mut r = Reader::new(payload);
168 let mut row = decode_row(&mut r, &table.wire_columns).map_err(|e| e.to_string())?;
170 if !r.is_empty() {
171 return Err("row payload has trailing bytes".to_owned());
172 }
173 if table.has_encrypted_columns() {
174 decrypt_row(table, &mut row, encryption)?;
175 }
176 Ok(row)
177}
178
179pub fn decrypt_segment_row(
183 table: &TableSchema,
184 row: &mut Row,
185 encryption: &EncryptionConfig,
186) -> Result<(), String> {
187 decrypt_row(table, row, encryption)
188}
189
190#[cfg(feature = "e2ee")]
193fn encrypt_row(
194 table: &TableSchema,
195 row_id: &str,
196 row: &mut Row,
197 encryption: &EncryptionConfig,
198) -> Result<(), String> {
199 use rand_core::RngCore;
200 use ssp2::crypto::{encrypt_value, NONCE_LENGTH};
201 for enc in &table.encrypted_columns {
202 let Some(value) = row.get(enc.index).and_then(|v| v.as_ref()) else {
203 continue; };
205 let plain = column_value_to_plain(value)?;
206 let key_id = encryption.key_id_for(&table.name, row_id);
207 let key = encryption
208 .keys
209 .get(&key_id)
210 .ok_or_else(|| format!("client.decrypt_failed: no key for keyId {key_id:?}"))?;
211 let mut nonce = [0u8; NONCE_LENGTH];
212 rand_core::OsRng.fill_bytes(&mut nonce);
213 let envelope = encrypt_value(&plain, &key_id, key, nonce)?;
214 row[enc.index] = Some(ColumnValue::Bytes(envelope));
215 }
216 Ok(())
217}
218
219#[cfg(feature = "e2ee")]
220fn decrypt_row(
221 table: &TableSchema,
222 row: &mut Row,
223 encryption: &EncryptionConfig,
224) -> Result<(), String> {
225 use ssp2::crypto::{decrypt_value, DeclaredType};
226 for enc in &table.encrypted_columns {
227 let Some(value) = row.get(enc.index).and_then(|v| v.as_ref()) else {
228 continue;
229 };
230 let ColumnValue::Bytes(envelope) = value else {
231 return Err(format!(
232 "client.decrypt_failed: encrypted column at index {} is not bytes",
233 enc.index
234 ));
235 };
236 let declared = DeclaredType::from_name(&enc.declared_type)
237 .ok_or_else(|| format!("unknown declaredType {:?}", enc.declared_type))?;
238 let keys = &encryption.keys;
239 let plain = decrypt_value(declared, envelope, |id| keys.get(id).cloned())
240 .map_err(|e| e.to_string())?;
241 row[enc.index] = Some(plain_to_column_value(plain));
242 }
243 Ok(())
244}
245
246#[cfg(not(feature = "e2ee"))]
249fn encrypt_row(
250 table: &TableSchema,
251 _row_id: &str,
252 _row: &mut Row,
253 _encryption: &EncryptionConfig,
254) -> Result<(), String> {
255 Err(format!(
256 "table {:?} has encrypted columns but this build lacks the e2ee feature (§5.11)",
257 table.name
258 ))
259}
260
261#[cfg(not(feature = "e2ee"))]
262fn decrypt_row(
263 table: &TableSchema,
264 _row: &mut Row,
265 _encryption: &EncryptionConfig,
266) -> Result<(), String> {
267 Err(format!(
268 "table {:?} has encrypted columns but this build lacks the e2ee feature (§5.11)",
269 table.name
270 ))
271}
272
273#[cfg(feature = "e2ee")]
274fn column_value_to_plain(value: &ColumnValue) -> Result<ssp2::crypto::PlainValue, String> {
275 use ssp2::crypto::PlainValue;
276 Ok(match value {
277 ColumnValue::String(s) => PlainValue::String(s.clone()),
278 ColumnValue::Integer(i) => PlainValue::Integer(*i),
279 ColumnValue::Float(f) => PlainValue::Float(*f),
280 ColumnValue::Boolean(b) => PlainValue::Boolean(*b),
281 ColumnValue::Json(j) => PlainValue::Json(j.0.clone()),
282 ColumnValue::BlobRef(j) => PlainValue::BlobRef(j.0.clone()),
283 ColumnValue::Bytes(b) => PlainValue::Bytes(b.clone()),
284 ColumnValue::Crdt(_) => return Err("crdt columns cannot be encrypted (§5.11)".to_owned()),
285 })
286}
287
288#[cfg(feature = "e2ee")]
289fn plain_to_column_value(value: ssp2::crypto::PlainValue) -> ColumnValue {
290 use ssp2::crypto::PlainValue;
291 match value {
292 PlainValue::String(s) => ColumnValue::String(s),
293 PlainValue::Integer(i) => ColumnValue::Integer(i),
294 PlainValue::Float(f) => ColumnValue::Float(f),
295 PlainValue::Boolean(b) => ColumnValue::Boolean(b),
296 PlainValue::Json(s) => ColumnValue::Json(RawJson(s)),
297 PlainValue::BlobRef(s) => ColumnValue::BlobRef(RawJson(s)),
298 PlainValue::Bytes(b) => ColumnValue::Bytes(b),
299 }
300}
301
302pub fn render_row_id(value: &Option<ColumnValue>) -> Result<String, String> {
304 match value {
305 Some(ColumnValue::String(s)) => Ok(s.clone()),
306 Some(ColumnValue::Integer(i)) => Ok(i.to_string()),
307 Some(ColumnValue::Float(f)) => Ok(f.to_string()),
308 Some(ColumnValue::Boolean(b)) => Ok(b.to_string()),
309 Some(ColumnValue::Json(raw)) => Ok(raw.0.clone()),
310 Some(ColumnValue::BlobRef(_)) => Err("blob_ref column cannot be a rowId".to_owned()),
311 Some(ColumnValue::Bytes(_)) => Err("bytes column cannot be a rowId".to_owned()),
312 Some(ColumnValue::Crdt(_)) => Err("crdt column cannot be a rowId".to_owned()),
313 None => Err("primary key value is missing".to_owned()),
314 }
315}
316
317pub fn render_row_id_json(value: Option<&Value>) -> Result<String, String> {
319 match value {
320 Some(Value::String(s)) => Ok(s.clone()),
321 Some(Value::Number(n)) => Ok(n.to_string()),
322 Some(Value::Bool(b)) => Ok(b.to_string()),
323 _ => Err("primary key value is missing or not renderable".to_owned()),
324 }
325}
326
327fn sort_utf16(values: &mut [String]) {
328 values.sort_by(|a, b| {
329 if utf16_lt(a, b) {
330 std::cmp::Ordering::Less
331 } else if utf16_lt(b, a) {
332 std::cmp::Ordering::Greater
333 } else {
334 std::cmp::Ordering::Equal
335 }
336 });
337}
338
339pub fn sort_scope_map(map: &mut [(String, Vec<String>)]) {
342 map.sort_by(|a, b| {
343 if utf16_lt(&a.0, &b.0) {
344 std::cmp::Ordering::Less
345 } else if utf16_lt(&b.0, &a.0) {
346 std::cmp::Ordering::Greater
347 } else {
348 std::cmp::Ordering::Equal
349 }
350 });
351}
352
353pub fn canonical_scope_json(scopes: &[(String, Vec<String>)]) -> String {
356 let mut entries: Vec<(String, Vec<String>)> = scopes.to_vec();
357 sort_scope_map(&mut entries);
358 let mut out = String::from("{");
359 for (i, (key, values)) in entries.iter().enumerate() {
360 if i > 0 {
361 out.push(',');
362 }
363 let mut sorted = values.clone();
364 sort_utf16(&mut sorted);
365 sorted.dedup();
366 out.push_str(&serde_json::to_string(key).expect("string serializes"));
367 out.push_str(":[");
368 for (j, value) in sorted.iter().enumerate() {
369 if j > 0 {
370 out.push(',');
371 }
372 out.push_str(&serde_json::to_string(value).expect("string serializes"));
373 }
374 out.push(']');
375 }
376 out.push('}');
377 out
378}
379
380pub fn scope_map_to_json(scopes: &[(String, Vec<String>)]) -> Value {
382 let mut map = Map::new();
383 for (key, values) in scopes {
384 map.insert(
385 key.clone(),
386 Value::Array(values.iter().map(|v| Value::from(v.clone())).collect()),
387 );
388 }
389 Value::Object(map)
390}
391
392pub fn json_to_scope_map(value: &Value) -> Result<Vec<(String, Vec<String>)>, String> {
394 let object = value
395 .as_object()
396 .ok_or_else(|| "scope map must be an object".to_owned())?;
397 let mut out = Vec::with_capacity(object.len());
398 for (key, values) in object {
399 let list = values
400 .as_array()
401 .ok_or_else(|| format!("scope values for {key:?} must be a list (§0)"))?;
402 let mut strings = Vec::with_capacity(list.len());
403 for v in list {
404 strings.push(
405 v.as_str()
406 .ok_or_else(|| format!("scope value for {key:?} is not a string"))?
407 .to_owned(),
408 );
409 }
410 out.push((key.clone(), strings));
411 }
412 Ok(out)
413}