-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgoeql.go
323 lines (270 loc) · 10.2 KB
/
goeql.go
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package goeql
// goeql is a collection of helpers for serializing and deserializing values
// into the shape EQL and the CipherStash Proxy needs to enable encryption and
// decryption of values, and search of those encrypted values while keeping them
// encrypted at all times.
// EQL expects a json format that looks like this:
//
// '{"k":"pt","p":"a string representation of the plaintext that is being encrypted","i":{"t":"table","c":"column"},"v":1}'
//
// More documentation on this format can be found at https://github.com/cipherstash/encrypt-query-language#data-format
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
)
// TableColumn represents the table and column an encrypted value belongs to
type TableColumn struct {
T string `json:"t"`
C string `json:"c"`
}
// EncryptedColumn represents the plaintext value sent by a database client
type EncryptedColumn struct {
K string `json:"k"`
P string `json:"p"`
I TableColumn `json:"i"`
V int `json:"v"`
Q any `json:"q"`
}
// EncryptedText is a string value to be encrypted
type EncryptedText string
// EncryptedJsonb is a jsonb value to be encrypted
type EncryptedJsonb map[string]interface{}
// EncryptedJsonb array value to be encrypted/decrypted
type EncryptedJsonbArray []interface{}
// EncryptedInt is a int value to be encrypted
type EncryptedInt int
// EncryptedBool is a bool value to be encrypted
type EncryptedBool bool
// Serialize turns a EncryptedText value into a jsonb payload for CipherStash Proxy
func (et EncryptedText) Serialize(table string, column string) ([]byte, error) {
// Adding a check based on the go zero value for a string https://go.dev/ref/spec#The_zero_value
if len(et) == 0 {
return nil, nil
}
val, err := ToEncryptedColumn(string(et), table, column, nil)
if err != nil {
return nil, fmt.Errorf("error serializing: %v", err)
}
return json.Marshal(val)
}
// Deserialize turns a jsonb payload from CipherStash Proxy into an EncryptedText value
func (et *EncryptedText) Deserialize(data []byte) (EncryptedText, error) {
if len(data) == 0 {
var EncryptedText EncryptedText
return EncryptedText, nil
}
var jsonData map[string]interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
return "", err
}
if pValue, ok := jsonData["p"].(string); ok {
return EncryptedText(pValue), nil
}
return "", fmt.Errorf("invalid format: missing 'p' field in JSONB")
}
// Serialize turns a EncryptedJsonb value into a jsonb payload for CipherStash Proxy
func (ej EncryptedJsonb) Serialize(table string, column string) ([]byte, error) {
// When setting a jsonb field in xorm to nil || an empty map || not including the field,
// the value that comes through here is map[]/
// Adding a check based on the go zero value https://go.dev/ref/spec#The_zero_value
if len(ej) == 0 {
return nil, nil
}
val, err := ToEncryptedColumn(map[string]any(ej), table, column, nil)
if err != nil {
return nil, fmt.Errorf("error serializing: %v", err)
}
return json.Marshal(val)
}
// Deserialize turns a jsonb payload from CipherStash Proxy into an EncryptedJsonb value
func (ej *EncryptedJsonb) Deserialize(data []byte) (EncryptedJsonb, error) {
if len(data) == 0 {
return nil, nil
}
var jsonData map[string]interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
return nil, err
}
if pValue, ok := jsonData["p"].(string); ok {
var pData map[string]interface{}
if err := json.Unmarshal([]byte(pValue), &pData); err != nil {
return nil, fmt.Errorf("error unmarshaling 'p' JSON string: %v", err)
}
return EncryptedJsonb(pData), nil
}
return nil, fmt.Errorf("invalid format: missing 'p' field in JSONB")
}
// Serialize turns a EncryptedJsonbArray value into a jsonb payload for CipherStash Proxy
func (eja EncryptedJsonbArray) Serialize(table string, column string) ([]byte, error) {
// When setting a jsonb field in xorm to nil || an empty map || not including the field,
// the value that comes through here is map[]/
// Adding a check based on the go zero value https://go.dev/ref/spec#The_zero_value
if len(eja) == 0 {
return nil, nil
}
val, err := ToEncryptedColumn([]interface{}(eja), table, column, nil)
if err != nil {
return nil, fmt.Errorf("error serializing: %v", err)
}
return json.Marshal(val)
}
// Deserialize turns a jsonb payload from CipherStash Proxy into an EncryptedJsonbArray value
func (ej *EncryptedJsonbArray) Deserialize(data []byte) (EncryptedJsonbArray, error) {
if len(data) == 0 {
return nil, nil
}
var jsonData map[string]interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
return nil, err
}
if pValue, ok := jsonData["p"].(string); ok {
var pData []interface{}
if err := json.Unmarshal([]byte(pValue), &pData); err != nil {
return nil, fmt.Errorf("error unmarshaling 'p' JSON string: %v", err)
}
return EncryptedJsonbArray(pData), nil
}
return nil, fmt.Errorf("invalid format: missing 'p' field in JSONB")
}
// Serialize turns a EncryptedInt value into a jsonb payload for CipherStash Proxy
func (ei EncryptedInt) Serialize(table string, column string) ([]byte, error) {
val, err := ToEncryptedColumn(int(ei), table, column, nil)
if err != nil {
return nil, fmt.Errorf("error serializing: %v", err)
}
return json.Marshal(val)
}
// Deserialize turns a jsonb payload from CipherStash Proxy into an EncryptedInt value
func (ei *EncryptedInt) Deserialize(data []byte) (EncryptedInt, error) {
var jsonData map[string]interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
return 0, fmt.Errorf("error unmarshaling 'p' JSON string: %v", err)
}
if pValue, ok := jsonData["p"].(string); ok {
parsedValue, err := strconv.Atoi(pValue) // Convert string to int
if err != nil {
return 0, fmt.Errorf("invalid number format in 'p' field: %v", err)
}
return EncryptedInt(parsedValue), nil
}
return 0, fmt.Errorf("invalid format: missing 'p' field")
}
// Serialize turns a EncryptedBool value into a jsonb payload for CipherStash Proxy
func (eb EncryptedBool) Serialize(table string, column string) ([]byte, error) {
// https: //go.dev/ref/spec#The_zero_value
// The zero value for an boolean is false
if !eb {
return nil, nil
}
val, err := ToEncryptedColumn(bool(eb), table, column, nil)
if err != nil {
return nil, fmt.Errorf("error serializing: %v", err)
}
return json.Marshal(val)
}
// Deserialize turns a jsonb payload from CipherStash Proxy into an EncryptedBool value
func (eb *EncryptedBool) Deserialize(data []byte) (EncryptedBool, error) {
var jsonData map[string]interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
// TODO: Check the best return values for these.
return false, err
}
if pValue, ok := jsonData["p"].(string); ok {
parsedValue, err := strconv.ParseBool(pValue)
if err != nil {
return false, fmt.Errorf("invalid boolean format in 'p' field: %v", err)
}
return EncryptedBool(parsedValue), nil
}
return false, fmt.Errorf("invalid format: missing 'p' field")
}
// MatchQuery serializes a plaintext value used in a match query
func MatchQuery(value any, table string, column string) ([]byte, error) {
return serializeQuery(value, table, column, "match")
}
// OreQuery serializes a plaintext value used in an ore query
func OreQuery(value any, table string, column string) ([]byte, error) {
return serializeQuery(value, table, column, "ore")
}
// UniqueQuery serializes a plaintext value used in a unique query
func UniqueQuery(value any, table string, column string) ([]byte, error) {
return serializeQuery(value, table, column, "unique")
}
// JsonbQuery serializes a plaintext value used in a jsonb query
func JsonbQuery(value any, table string, column string) ([]byte, error) {
return serializeQuery(value, table, column, "ste_vec")
}
// EJsonPathQuery serializes an ejson path to be used in an ejson path query
func EJsonPathQuery(value any, table string, column string) ([]byte, error) {
return serializeQuery(value, table, column, "ejson_path")
}
// serializeQuery produces a jsonb payload used by EQL query functions to perform search operations like equality checks, range queries, and unique constraints.
func serializeQuery(value any, table string, column string, queryType any) ([]byte, error) {
query, err := ToEncryptedColumn(value, table, column, queryType)
if err != nil {
return nil, fmt.Errorf("error converting to EncryptedColumn: %v", err)
}
serializedQuery, errMarshal := json.Marshal(query)
if errMarshal != nil {
return nil, fmt.Errorf("error marshalling EncryptedColumn: %v", errMarshal)
}
return serializedQuery, nil
}
// ToEncryptedColumn converts a plaintext value to a string, and returns the EncryptedColumn struct for inserting into a database.
func ToEncryptedColumn(value any, table string, column string, queryType any) (EncryptedColumn, error) {
if queryType == nil {
str, err := convertToString(value)
if err != nil {
return EncryptedColumn{}, fmt.Errorf("error: %v", err)
}
data := EncryptedColumn{K: "pt", P: str, I: TableColumn{T: table, C: column}, V: 1, Q: nil}
return data, nil
}
str, err := convertToString(value)
if err != nil {
return EncryptedColumn{}, fmt.Errorf("error: %v", err)
}
data := EncryptedColumn{K: "pt", P: str, I: TableColumn{T: table, C: column}, V: 1, Q: queryType}
return data, nil
}
func convertToString(value any) (string, error) {
// Check for slice types
val := reflect.ValueOf(value)
// reflect.Slice will return true if it is a slice
if val.Kind() == reflect.Slice {
strSlice := make([]string, val.Len())
for i := 0; i < val.Len(); i++ {
elem, err := convertToString(val.Index(i).Interface())
if err != nil {
return "", err
}
strSlice[i] = elem
}
return "[" + strings.Join(strSlice, ", ") + "]", nil
}
switch v := value.(type) {
case fmt.Stringer:
return v.String(), nil
case string:
return v, nil
case int, int8, int16, int32, int64:
return fmt.Sprintf("%d", v), nil
case uint, uint8, uint16, uint32, uint64, uintptr:
return fmt.Sprintf("%d", v), nil
case float32, float64:
return fmt.Sprintf("%f", v), nil
case map[string]any:
jsonData, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("error marshaling JSON: %v", err)
}
return string(jsonData), nil
case bool:
return strconv.FormatBool(v), nil
default:
return "", fmt.Errorf("unsupported type: %T", v)
}
}