bzync-nextsql 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +16 -0
- data/lib/nextsql/client.rb +85 -9
- data/lib/nextsql/errors.rb +9 -1
- data/lib/nextsql/field_encryption.rb +823 -0
- data/lib/nextsql/pool.rb +215 -0
- data/lib/nextsql/protocol.rb +26 -5
- data/lib/nextsql/version.rb +1 -1
- data/lib/nextsql.rb +7 -0
- metadata +6 -4
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "bigdecimal"
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
require_relative "protocol"
|
|
10
|
+
require_relative "errors"
|
|
11
|
+
|
|
12
|
+
module NextSQL
|
|
13
|
+
FieldType = Struct.new(:kind, :precision, :scale, :vec_elem, keyword_init: true) do
|
|
14
|
+
def self.decimal(precision, scale)
|
|
15
|
+
if precision < 1 || precision > 38 || scale.negative? || scale > precision
|
|
16
|
+
raise Error.new("invalid_argument", "unsupported client-encrypted type")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
new(kind: Protocol::KIND_DECIMAL, precision: precision, scale: scale, vec_elem: 0)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
FieldType::UUID = FieldType.new(kind: Protocol::KIND_UUID, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
24
|
+
FieldType::STRING = FieldType.new(kind: Protocol::KIND_STRING, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
25
|
+
FieldType::TEXT = FieldType.new(kind: Protocol::KIND_TEXT, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
26
|
+
FieldType::BLOB = FieldType.new(kind: Protocol::KIND_BLOB, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
27
|
+
FieldType::TIMESTAMPTZ = FieldType.new(kind: Protocol::KIND_TIMESTAMPTZ, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
28
|
+
FieldType::JSON = FieldType.new(kind: Protocol::KIND_JSON, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
29
|
+
FieldType::BOOL = FieldType.new(kind: Protocol::KIND_BOOL, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
30
|
+
FieldType::INT8 = FieldType.new(kind: Protocol::KIND_INT8, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
31
|
+
FieldType::INT16 = FieldType.new(kind: Protocol::KIND_INT16, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
32
|
+
FieldType::INT32 = FieldType.new(kind: Protocol::KIND_INT32, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
33
|
+
FieldType::INT64 = FieldType.new(kind: Protocol::KIND_INT64, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
34
|
+
FieldType::UINT8 = FieldType.new(kind: Protocol::KIND_UINT8, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
35
|
+
FieldType::UINT16 = FieldType.new(kind: Protocol::KIND_UINT16, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
36
|
+
FieldType::UINT32 = FieldType.new(kind: Protocol::KIND_UINT32, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
37
|
+
FieldType::UINT64 = FieldType.new(kind: Protocol::KIND_UINT64, precision: 0, scale: 0, vec_elem: 0).freeze
|
|
38
|
+
|
|
39
|
+
FieldKey = Struct.new(:id, :material, keyword_init: true) do
|
|
40
|
+
def initialize(id:, material:)
|
|
41
|
+
FieldEncryption.validate_key_id!(id)
|
|
42
|
+
material_bytes = material.b
|
|
43
|
+
if material_bytes.bytesize != FieldEncryption::KEY_SIZE || material_bytes == ("\x00".b * FieldEncryption::KEY_SIZE)
|
|
44
|
+
raise Error.new("invalid_argument", "invalid field key")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
super(id: id, material: material_bytes)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
FieldKeyInfo = Struct.new(:id, :created, :current, :revoked, keyword_init: true)
|
|
52
|
+
|
|
53
|
+
class MemoryFieldKeyring
|
|
54
|
+
def initialize(current, overlap = [])
|
|
55
|
+
@keys = {}
|
|
56
|
+
@revoked = {}
|
|
57
|
+
all_keys = [current] + (overlap || [])
|
|
58
|
+
if all_keys.length > FieldEncryption::MAX_KEYS
|
|
59
|
+
raise Error.new("invalid_argument", "too many field keys")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
seen = {}
|
|
63
|
+
all_keys.each do |k|
|
|
64
|
+
raise Error.new("invalid_argument", "invalid field key") unless k.is_a?(FieldKey)
|
|
65
|
+
raise Error.new("invalid_argument", "duplicate field key id") if seen[k.id]
|
|
66
|
+
|
|
67
|
+
seen[k.id] = true
|
|
68
|
+
@keys[k.id] = k
|
|
69
|
+
end
|
|
70
|
+
@current_id = current.id
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def current_field_key(_database, _table, _column)
|
|
74
|
+
k = @keys[@current_id]
|
|
75
|
+
raise Error.new("crypto", "current field key unavailable") unless k
|
|
76
|
+
|
|
77
|
+
k
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def field_key(_database, _table, _column, key_id)
|
|
81
|
+
if @revoked[key_id] || !@keys.key?(key_id)
|
|
82
|
+
raise Error.new("crypto", "field key unavailable or revoked")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@keys[key_id]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def rotate(key)
|
|
89
|
+
raise Error.new("invalid_argument", "invalid field key") unless key.is_a?(FieldKey)
|
|
90
|
+
raise Error.new("conflict", "cannot reuse a revoked field key id") if @revoked[key.id]
|
|
91
|
+
|
|
92
|
+
if !@keys.key?(key.id) && @keys.length >= FieldEncryption::MAX_KEYS
|
|
93
|
+
raise Error.new("exhausted", "field key limit reached")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@keys[key.id] = key
|
|
97
|
+
@current_id = key.id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def revoke(key_id)
|
|
101
|
+
if !@keys.key?(key_id) && !@revoked.key?(key_id)
|
|
102
|
+
raise Error.new("not_found", "unknown field key id")
|
|
103
|
+
end
|
|
104
|
+
if key_id == @current_id
|
|
105
|
+
raise Error.new("conflict", "cannot revoke the current field key")
|
|
106
|
+
end
|
|
107
|
+
return if @revoked[key_id]
|
|
108
|
+
|
|
109
|
+
@revoked[key_id] = true
|
|
110
|
+
@keys.delete(key_id)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class FileFieldKeyring
|
|
115
|
+
FileRecord = Struct.new(:id, :created, :current, :revoked, :material, keyword_init: true)
|
|
116
|
+
|
|
117
|
+
attr_reader :path
|
|
118
|
+
|
|
119
|
+
def initialize(path, records)
|
|
120
|
+
@path = path
|
|
121
|
+
@records = records
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class << self
|
|
125
|
+
def create(path, current)
|
|
126
|
+
raise Error.new("already_exists", "keyring file exists") if File.exist?(path)
|
|
127
|
+
raise Error.new("invalid_argument", "invalid field key") unless current.is_a?(FieldKey)
|
|
128
|
+
|
|
129
|
+
rec = FileRecord.new(
|
|
130
|
+
id: current.id,
|
|
131
|
+
created: Time.now.utc,
|
|
132
|
+
current: true,
|
|
133
|
+
revoked: false,
|
|
134
|
+
material: current.material
|
|
135
|
+
)
|
|
136
|
+
kr = new(path, [rec])
|
|
137
|
+
kr.send(:persist!)
|
|
138
|
+
kr
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def open(path)
|
|
142
|
+
raise Error.new("not_found", "keyring file not found") unless File.exist?(path)
|
|
143
|
+
|
|
144
|
+
raw = File.binread(path)
|
|
145
|
+
records = decode_keyring(raw)
|
|
146
|
+
new(path, records)
|
|
147
|
+
rescue Error
|
|
148
|
+
raise
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
raise Error.new("io", "failed to read keyring: #{e.message}")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def decode_keyring(raw)
|
|
154
|
+
raise Error.new("invalid_format", "truncated keyring") if raw.bytesize < 8
|
|
155
|
+
raise Error.new("invalid_format", "bad keyring magic") if raw[0, 4] != FieldEncryption::KEYRING_MAGIC
|
|
156
|
+
|
|
157
|
+
ver = raw[4, 2].unpack1("v")
|
|
158
|
+
count = raw[6, 2].unpack1("v")
|
|
159
|
+
raise Error.new("invalid_format", "unsupported keyring version") if ver != FieldEncryption::KEYRING_VERSION
|
|
160
|
+
raise Error.new("invalid_format", "keyring key count exceeds limit") if count > FieldEncryption::MAX_KEYS
|
|
161
|
+
|
|
162
|
+
off = 8
|
|
163
|
+
records = []
|
|
164
|
+
seen = {}
|
|
165
|
+
current_count = 0
|
|
166
|
+
|
|
167
|
+
count.times do
|
|
168
|
+
raise Error.new("invalid_format", "truncated keyring record") if off >= raw.bytesize
|
|
169
|
+
|
|
170
|
+
id_len = raw.getbyte(off)
|
|
171
|
+
off += 1
|
|
172
|
+
if id_len < 1 || id_len > FieldEncryption::MAX_KEY_ID || off + id_len + 8 + 1 + FieldEncryption::KEY_SIZE > raw.bytesize
|
|
173
|
+
raise Error.new("invalid_format", "invalid field key id length or truncated record")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
id_str = raw[off, id_len].force_encoding("utf-8")
|
|
177
|
+
off += id_len
|
|
178
|
+
unless FieldEncryption.valid_key_id?(id_str) && !seen[id_str]
|
|
179
|
+
raise Error.new("invalid_format", "invalid or duplicate field key id")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
seen[id_str] = true
|
|
183
|
+
|
|
184
|
+
created_sec = raw[off, 8].unpack1("Q<")
|
|
185
|
+
flags = raw.getbyte(off + 8)
|
|
186
|
+
off += 9
|
|
187
|
+
material = raw[off, FieldEncryption::KEY_SIZE]
|
|
188
|
+
off += FieldEncryption::KEY_SIZE
|
|
189
|
+
|
|
190
|
+
is_current = (flags & FieldEncryption::FLAG_CURRENT) != 0
|
|
191
|
+
is_revoked = (flags & FieldEncryption::FLAG_REVOKED) != 0
|
|
192
|
+
|
|
193
|
+
raise Error.new("invalid_format", "current key cannot be revoked") if is_current && is_revoked
|
|
194
|
+
|
|
195
|
+
if is_revoked && material != ("\x00".b * FieldEncryption::KEY_SIZE)
|
|
196
|
+
raise Error.new("invalid_format", "revoked key has non-zero material")
|
|
197
|
+
end
|
|
198
|
+
if !is_revoked && material == ("\x00".b * FieldEncryption::KEY_SIZE)
|
|
199
|
+
raise Error.new("invalid_format", "unrevoked key has all-zero material")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
current_count += 1 if is_current
|
|
203
|
+
|
|
204
|
+
records << FileRecord.new(
|
|
205
|
+
id: id_str,
|
|
206
|
+
created: Time.at(created_sec).utc,
|
|
207
|
+
current: is_current,
|
|
208
|
+
revoked: is_revoked,
|
|
209
|
+
material: material
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if count.positive? && current_count != 1
|
|
214
|
+
raise Error.new("invalid_format", "keyring must contain exactly one current key")
|
|
215
|
+
end
|
|
216
|
+
if off != raw.bytesize
|
|
217
|
+
raise Error.new("invalid_format", "trailing bytes after keyring records")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
records
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def current_field_key(_database, _table, _column)
|
|
225
|
+
rec = @records.find { |r| r.current && !r.revoked }
|
|
226
|
+
raise Error.new("crypto", "current field key unavailable") unless rec
|
|
227
|
+
|
|
228
|
+
FieldKey.new(id: rec.id, material: rec.material)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def field_key(_database, _table, _column, key_id)
|
|
232
|
+
rec = @records.find { |r| r.id == key_id }
|
|
233
|
+
if !rec || rec.revoked
|
|
234
|
+
raise Error.new("crypto", "field key unavailable or revoked")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
FieldKey.new(id: rec.id, material: rec.material)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def rotate(key)
|
|
241
|
+
raise Error.new("invalid_argument", "invalid field key") unless key.is_a?(FieldKey)
|
|
242
|
+
|
|
243
|
+
idx = @records.find_index { |r| r.id == key.id }
|
|
244
|
+
if idx
|
|
245
|
+
raise Error.new("conflict", "cannot reuse a revoked field key id") if @records[idx].revoked
|
|
246
|
+
else
|
|
247
|
+
if @records.length >= FieldEncryption::MAX_KEYS
|
|
248
|
+
raise Error.new("exhausted", "field key limit reached")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
@records << FileRecord.new(
|
|
252
|
+
id: key.id,
|
|
253
|
+
created: Time.now.utc,
|
|
254
|
+
current: false,
|
|
255
|
+
revoked: false,
|
|
256
|
+
material: key.material
|
|
257
|
+
)
|
|
258
|
+
idx = @records.length - 1
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
@records.each { |r| r.current = false }
|
|
262
|
+
@records[idx].current = true
|
|
263
|
+
@records[idx].material = key.material
|
|
264
|
+
persist!
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def revoke(key_id)
|
|
268
|
+
idx = @records.find_index { |r| r.id == key_id }
|
|
269
|
+
raise Error.new("not_found", "unknown field key id") unless idx
|
|
270
|
+
if @records[idx].current
|
|
271
|
+
raise Error.new("conflict", "cannot revoke the current field key")
|
|
272
|
+
end
|
|
273
|
+
return if @records[idx].revoked
|
|
274
|
+
|
|
275
|
+
@records[idx].revoked = true
|
|
276
|
+
@records[idx].material = "\x00".b * FieldEncryption::KEY_SIZE
|
|
277
|
+
persist!
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def reload
|
|
281
|
+
raw = File.binread(@path)
|
|
282
|
+
@records = self.class.decode_keyring(raw)
|
|
283
|
+
rescue Error
|
|
284
|
+
raise
|
|
285
|
+
rescue StandardError => e
|
|
286
|
+
raise Error.new("io", "failed to reload keyring: #{e.message}")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def list
|
|
290
|
+
@records.map do |r|
|
|
291
|
+
FieldKeyInfo.new(id: r.id, created: r.created, current: r.current, revoked: r.revoked)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def persist!
|
|
298
|
+
raw = encode_keyring
|
|
299
|
+
tmp = "#{@path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
|
|
300
|
+
File.open(tmp, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |f|
|
|
301
|
+
f.binmode
|
|
302
|
+
f.write(raw)
|
|
303
|
+
f.flush
|
|
304
|
+
f.fsync
|
|
305
|
+
end
|
|
306
|
+
File.rename(tmp, @path)
|
|
307
|
+
rescue StandardError => e
|
|
308
|
+
File.unlink(tmp) if File.exist?(tmp)
|
|
309
|
+
raise Error.new("io", "failed to persist keyring: #{e.message}")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def encode_keyring
|
|
313
|
+
buf = +"".b
|
|
314
|
+
buf << FieldEncryption::KEYRING_MAGIC
|
|
315
|
+
buf << [FieldEncryption::KEYRING_VERSION].pack("v")
|
|
316
|
+
buf << [@records.length].pack("v")
|
|
317
|
+
|
|
318
|
+
@records.each do |r|
|
|
319
|
+
id_b = r.id.encode("utf-8").b
|
|
320
|
+
buf << [id_b.bytesize].pack("C")
|
|
321
|
+
buf << id_b
|
|
322
|
+
buf << [r.created.to_i].pack("Q<")
|
|
323
|
+
flags = 0
|
|
324
|
+
flags |= FieldEncryption::FLAG_CURRENT if r.current
|
|
325
|
+
flags |= FieldEncryption::FLAG_REVOKED if r.revoked
|
|
326
|
+
buf << [flags].pack("C")
|
|
327
|
+
buf << r.material
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
buf
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
module FieldEncryption
|
|
335
|
+
PREFIX = "NSCE1."
|
|
336
|
+
DETERMINISTIC_PREFIX = "NSCE2."
|
|
337
|
+
VERSION = 1
|
|
338
|
+
DETERMINISTIC_VERSION = 2
|
|
339
|
+
SUITE_AES_256_GCM = 1
|
|
340
|
+
SUITE_AES_SIV = 2
|
|
341
|
+
KEY_SIZE = 32
|
|
342
|
+
MAX_KEY_ID = 64
|
|
343
|
+
MAX_KEYS = 64
|
|
344
|
+
MAX_PLAINTEXT = 1 << 20
|
|
345
|
+
NONCE_SIZE = 12
|
|
346
|
+
TAG_SIZE = 16
|
|
347
|
+
|
|
348
|
+
KEYRING_MAGIC = "NSFK".b
|
|
349
|
+
KEYRING_VERSION = 1
|
|
350
|
+
FLAG_CURRENT = 1 << 0
|
|
351
|
+
FLAG_REVOKED = 1 << 1
|
|
352
|
+
|
|
353
|
+
KEY_ID_RE = /\A[A-Za-z0-9._-]+\z/
|
|
354
|
+
|
|
355
|
+
class << self
|
|
356
|
+
def generate_field_key(id)
|
|
357
|
+
validate_key_id!(id)
|
|
358
|
+
FieldKey.new(id: id, material: SecureRandom.random_bytes(KEY_SIZE))
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def valid_key_id?(id)
|
|
362
|
+
id.is_a?(String) && id.length >= 1 && id.length <= MAX_KEY_ID && KEY_ID_RE.match?(id)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def validate_key_id!(id)
|
|
366
|
+
raise Error.new("invalid_argument", "invalid field key id") unless valid_key_id?(id)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def encrypt(provider, database, table, column, field_type, value)
|
|
370
|
+
return nil if value.nil?
|
|
371
|
+
|
|
372
|
+
validate_field_type!(field_type)
|
|
373
|
+
plain = encode_scalar(field_type, value)
|
|
374
|
+
if plain.bytesize > MAX_PLAINTEXT
|
|
375
|
+
raise Error.new("exhausted", "plaintext exceeds field limit")
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
key = provider.current_field_key(database, table, column)
|
|
379
|
+
public_header = build_public_header(VERSION, SUITE_AES_256_GCM, key.id, field_type)
|
|
380
|
+
nonce = SecureRandom.random_bytes(NONCE_SIZE)
|
|
381
|
+
aad = build_aad(PREFIX, database, table, column, public_header)
|
|
382
|
+
|
|
383
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
384
|
+
cipher.encrypt
|
|
385
|
+
cipher.key = key.material
|
|
386
|
+
cipher.iv = nonce
|
|
387
|
+
cipher.auth_data = aad
|
|
388
|
+
ct = cipher.update(plain) + cipher.final
|
|
389
|
+
tag = cipher.auth_tag
|
|
390
|
+
|
|
391
|
+
body = public_header + nonce + ct + tag
|
|
392
|
+
PREFIX + Base64.urlsafe_encode64(body, padding: false)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def decrypt(provider, database, table, column, expected_type, ciphertext)
|
|
396
|
+
return nil if ciphertext.nil?
|
|
397
|
+
|
|
398
|
+
validate_field_type!(expected_type)
|
|
399
|
+
parsed = inspect_field(ciphertext)
|
|
400
|
+
if parsed[:mode] != "randomized"
|
|
401
|
+
raise Error.new("invalid_format", "deterministic ciphertext requires decrypt_field_deterministic")
|
|
402
|
+
end
|
|
403
|
+
if parsed[:type] != expected_type
|
|
404
|
+
raise Error.new("invalid_format", "encrypted logical type mismatch")
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
key = provider.field_key(database, table, column, parsed[:key_id])
|
|
408
|
+
body = parsed[:body]
|
|
409
|
+
header_len = parsed[:header_length]
|
|
410
|
+
public_header = body[0...(header_len - NONCE_SIZE)]
|
|
411
|
+
nonce = body[(header_len - NONCE_SIZE)...header_len]
|
|
412
|
+
ct_and_tag = body[header_len..]
|
|
413
|
+
tag = ct_and_tag[-TAG_SIZE..]
|
|
414
|
+
ct = ct_and_tag[0...-TAG_SIZE]
|
|
415
|
+
aad = build_aad(PREFIX, database, table, column, public_header)
|
|
416
|
+
|
|
417
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
418
|
+
cipher.decrypt
|
|
419
|
+
cipher.key = key.material
|
|
420
|
+
cipher.iv = nonce
|
|
421
|
+
cipher.auth_tag = tag
|
|
422
|
+
cipher.auth_data = aad
|
|
423
|
+
begin
|
|
424
|
+
plain = cipher.update(ct) + cipher.final
|
|
425
|
+
rescue OpenSSL::Cipher::CipherError
|
|
426
|
+
raise Error.new("crypto", "ciphertext authentication failed")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
if plain.bytesize > MAX_PLAINTEXT
|
|
430
|
+
raise Error.new("invalid_format", "plaintext exceeds field limit")
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
decode_scalar(expected_type, plain)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def encrypt_deterministic(provider, database, table, column, field_type, value)
|
|
437
|
+
return nil if value.nil?
|
|
438
|
+
|
|
439
|
+
validate_field_type!(field_type)
|
|
440
|
+
plain = encode_scalar(field_type, value)
|
|
441
|
+
if plain.bytesize > MAX_PLAINTEXT
|
|
442
|
+
raise Error.new("exhausted", "plaintext exceeds field limit")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
key = provider.current_field_key(database, table, column)
|
|
446
|
+
public_header = build_public_header(DETERMINISTIC_VERSION, SUITE_AES_SIV, key.id, field_type)
|
|
447
|
+
aad = build_aad(DETERMINISTIC_PREFIX, database, table, column, public_header)
|
|
448
|
+
|
|
449
|
+
derived = hkdf_sha256(key.material, "NextSQL NSCE2 AES-SIV v2".b, 32)
|
|
450
|
+
k1 = derived[0, 16]
|
|
451
|
+
k2 = derived[16, 16]
|
|
452
|
+
|
|
453
|
+
tag = s2v(k1, aad, plain)
|
|
454
|
+
cipher_bytes = ctr_crypt(k2, tag, plain)
|
|
455
|
+
|
|
456
|
+
body = public_header + tag + cipher_bytes
|
|
457
|
+
DETERMINISTIC_PREFIX + Base64.urlsafe_encode64(body, padding: false)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def decrypt_deterministic(provider, database, table, column, expected_type, ciphertext)
|
|
461
|
+
return nil if ciphertext.nil?
|
|
462
|
+
|
|
463
|
+
validate_field_type!(expected_type)
|
|
464
|
+
parsed = inspect_field(ciphertext)
|
|
465
|
+
if parsed[:mode] != "deterministic"
|
|
466
|
+
raise Error.new("invalid_format", "randomized ciphertext requires decrypt_field")
|
|
467
|
+
end
|
|
468
|
+
if parsed[:type] != expected_type
|
|
469
|
+
raise Error.new("invalid_format", "encrypted logical type mismatch")
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
key = provider.field_key(database, table, column, parsed[:key_id])
|
|
473
|
+
body = parsed[:body]
|
|
474
|
+
header_len = parsed[:header_length]
|
|
475
|
+
public_header = body[0...header_len]
|
|
476
|
+
tag = body[header_len, 16]
|
|
477
|
+
ct = body[(header_len + 16)..]
|
|
478
|
+
|
|
479
|
+
aad = build_aad(DETERMINISTIC_PREFIX, database, table, column, public_header)
|
|
480
|
+
derived = hkdf_sha256(key.material, "NextSQL NSCE2 AES-SIV v2".b, 32)
|
|
481
|
+
k1 = derived[0, 16]
|
|
482
|
+
k2 = derived[16, 16]
|
|
483
|
+
|
|
484
|
+
plain = ctr_crypt(k2, tag, ct)
|
|
485
|
+
expected_tag = s2v(k1, aad, plain)
|
|
486
|
+
unless OpenSSL.fixed_length_secure_compare(tag, expected_tag)
|
|
487
|
+
raise Error.new("crypto", "ciphertext authentication failed")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
if plain.bytesize > MAX_PLAINTEXT
|
|
491
|
+
raise Error.new("invalid_format", "plaintext exceeds field limit")
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
decode_scalar(expected_type, plain)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def inspect_field(ciphertext)
|
|
498
|
+
raise Error.new("invalid_format", "invalid client ciphertext") unless ciphertext.is_a?(String)
|
|
499
|
+
|
|
500
|
+
if ciphertext.start_with?(PREFIX)
|
|
501
|
+
prefix = PREFIX
|
|
502
|
+
version = VERSION
|
|
503
|
+
suite = SUITE_AES_256_GCM
|
|
504
|
+
nonce_size = NONCE_SIZE
|
|
505
|
+
mode = "randomized"
|
|
506
|
+
elsif ciphertext.start_with?(DETERMINISTIC_PREFIX)
|
|
507
|
+
prefix = DETERMINISTIC_PREFIX
|
|
508
|
+
version = DETERMINISTIC_VERSION
|
|
509
|
+
suite = SUITE_AES_SIV
|
|
510
|
+
nonce_size = 0
|
|
511
|
+
mode = "deterministic"
|
|
512
|
+
else
|
|
513
|
+
raise Error.new("invalid_format", "invalid client ciphertext prefix")
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
encoded = ciphertext[prefix.length..]
|
|
517
|
+
max_encoded = ((MAX_PLAINTEXT + 101) * 4 / 3) + 4
|
|
518
|
+
if encoded.nil? || encoded.empty? || encoded.length > max_encoded || encoded.length % 4 == 1 || !encoded.match?(/\A[A-Za-z0-9_-]+\z/)
|
|
519
|
+
raise Error.new("invalid_format", "client ciphertext length out of range")
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
padded = encoded + ("=" * ((4 - (encoded.length % 4)) % 4))
|
|
523
|
+
begin
|
|
524
|
+
body = Base64.urlsafe_decode64(padded)
|
|
525
|
+
rescue StandardError
|
|
526
|
+
raise Error.new("invalid_format", "invalid client ciphertext encoding")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
min_len = 3 + 1 + 6 + nonce_size + TAG_SIZE
|
|
530
|
+
if body.bytesize < min_len || body.getbyte(0) != version || body.getbyte(1) != suite
|
|
531
|
+
raise Error.new("invalid_format", "unsupported or truncated client ciphertext")
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
key_id_len = body.getbyte(2)
|
|
535
|
+
if key_id_len < 1 || key_id_len > MAX_KEY_ID || body.bytesize < 3 + key_id_len + 6 + nonce_size + TAG_SIZE
|
|
536
|
+
raise Error.new("invalid_format", "invalid field key id length")
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
key_id = body[3, key_id_len].force_encoding("utf-8")
|
|
540
|
+
raise Error.new("invalid_format", "invalid field key id") unless valid_key_id?(key_id)
|
|
541
|
+
|
|
542
|
+
off = 3 + key_id_len
|
|
543
|
+
kind = body.getbyte(off)
|
|
544
|
+
precision = body[off + 1, 2].unpack1("v")
|
|
545
|
+
scale = body[off + 3, 2].unpack1("v")
|
|
546
|
+
vec_elem = body.getbyte(off + 5)
|
|
547
|
+
ft = FieldType.new(kind: kind, precision: precision, scale: scale, vec_elem: vec_elem)
|
|
548
|
+
validate_field_type!(ft)
|
|
549
|
+
|
|
550
|
+
header_len = off + 6 + nonce_size
|
|
551
|
+
{
|
|
552
|
+
key_id: key_id,
|
|
553
|
+
type: ft,
|
|
554
|
+
body: body,
|
|
555
|
+
header_length: header_len,
|
|
556
|
+
mode: mode,
|
|
557
|
+
}
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# --- internal helpers ---
|
|
561
|
+
|
|
562
|
+
def validate_field_type!(ft)
|
|
563
|
+
if ft.kind == Protocol::KIND_DECIMAL
|
|
564
|
+
if ft.precision < 1 || ft.precision > 38 || ft.scale.negative? || ft.scale > ft.precision
|
|
565
|
+
raise Error.new("invalid_argument", "unsupported client-encrypted type")
|
|
566
|
+
end
|
|
567
|
+
return
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
valid = [
|
|
571
|
+
Protocol::KIND_UUID, Protocol::KIND_STRING, Protocol::KIND_TEXT, Protocol::KIND_BLOB,
|
|
572
|
+
Protocol::KIND_TIMESTAMPTZ, Protocol::KIND_JSON, Protocol::KIND_BOOL,
|
|
573
|
+
Protocol::KIND_INT8, Protocol::KIND_INT16, Protocol::KIND_INT32, Protocol::KIND_INT64,
|
|
574
|
+
Protocol::KIND_UINT8, Protocol::KIND_UINT16, Protocol::KIND_UINT32, Protocol::KIND_UINT64,
|
|
575
|
+
]
|
|
576
|
+
if !valid.include?(ft.kind) || ft.precision != 0 || ft.scale != 0 || ft.vec_elem != 0
|
|
577
|
+
raise Error.new("invalid_argument", "unsupported client-encrypted type")
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def build_public_header(version, suite, key_id, field_type)
|
|
582
|
+
key_id_b = key_id.encode("utf-8").b
|
|
583
|
+
[version, suite, key_id_b.bytesize].pack("CCC") +
|
|
584
|
+
key_id_b +
|
|
585
|
+
[field_type.kind, field_type.precision, field_type.scale, field_type.vec_elem].pack("CvvC")
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def build_aad(prefix, database, table, column, public_header)
|
|
589
|
+
db_b = validate_name!(database, "database")
|
|
590
|
+
tbl_b = validate_name!(table, "table")
|
|
591
|
+
col_b = validate_name!(column, "column")
|
|
592
|
+
|
|
593
|
+
out = prefix.encode("utf-8").b
|
|
594
|
+
out << [db_b.bytesize].pack("v") << db_b
|
|
595
|
+
out << [tbl_b.bytesize].pack("v") << tbl_b
|
|
596
|
+
out << [col_b.bytesize].pack("v") << col_b
|
|
597
|
+
out << public_header
|
|
598
|
+
out
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def validate_name!(val, what)
|
|
602
|
+
raise Error.new("invalid_argument", "#{what} must not be empty") unless val.is_a?(String) && !val.empty?
|
|
603
|
+
b = val.encode("utf-8").b
|
|
604
|
+
raise Error.new("invalid_argument", "#{what} exceeds maximum length") if b.bytesize > 0xFFFF
|
|
605
|
+
b
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def encode_scalar(ft, value)
|
|
609
|
+
case ft.kind
|
|
610
|
+
when Protocol::KIND_UUID
|
|
611
|
+
if value.is_a?(String)
|
|
612
|
+
clean = value.delete("-")
|
|
613
|
+
raise Error.new("invalid_argument", "invalid UUID") unless clean.match?(/\A[0-9a-fA-F]{32}\z/)
|
|
614
|
+
[clean].pack("H*")
|
|
615
|
+
elsif value.is_a?(String) && value.bytesize == 16
|
|
616
|
+
value.b
|
|
617
|
+
else
|
|
618
|
+
raise Error.new("invalid_argument", "invalid UUID for encrypted field")
|
|
619
|
+
end
|
|
620
|
+
when Protocol::KIND_STRING, Protocol::KIND_TEXT
|
|
621
|
+
raise Error.new("invalid_argument", "encrypted string value must be a string") unless value.is_a?(String)
|
|
622
|
+
b = value.encode("utf-8").b
|
|
623
|
+
[b.bytesize].pack("V") + b
|
|
624
|
+
when Protocol::KIND_BLOB
|
|
625
|
+
raise Error.new("invalid_argument", "encrypted BLOB value must be bytes") unless value.is_a?(String)
|
|
626
|
+
b = value.b
|
|
627
|
+
[b.bytesize].pack("V") + b
|
|
628
|
+
when Protocol::KIND_BOOL
|
|
629
|
+
raise Error.new("invalid_argument", "encrypted BOOL value must be boolean") unless value == true || value == false
|
|
630
|
+
value ? "\x01".b : "\x00".b
|
|
631
|
+
when Protocol::KIND_INT8
|
|
632
|
+
raise Error.new("invalid_argument", "encrypted int value out of range") if value < -0x80 || value > 0x7F
|
|
633
|
+
[value].pack("c")
|
|
634
|
+
when Protocol::KIND_INT16
|
|
635
|
+
raise Error.new("invalid_argument", "encrypted int value out of range") if value < -0x8000 || value > 0x7FFF
|
|
636
|
+
[value].pack("s<")
|
|
637
|
+
when Protocol::KIND_INT32
|
|
638
|
+
raise Error.new("invalid_argument", "encrypted int value out of range") if value < -0x80000000 || value > 0x7FFFFFFF
|
|
639
|
+
[value].pack("l<")
|
|
640
|
+
when Protocol::KIND_INT64
|
|
641
|
+
raise Error.new("invalid_argument", "encrypted int value out of range") if value < -0x8000000000000000 || value > 0x7FFFFFFFFFFFFFFF
|
|
642
|
+
[value].pack("q<")
|
|
643
|
+
when Protocol::KIND_UINT8
|
|
644
|
+
raise Error.new("invalid_argument", "encrypted uint value out of range") if value.negative? || value > 0xFF
|
|
645
|
+
[value].pack("C")
|
|
646
|
+
when Protocol::KIND_UINT16
|
|
647
|
+
raise Error.new("invalid_argument", "encrypted uint value out of range") if value.negative? || value > 0xFFFF
|
|
648
|
+
[value].pack("S<")
|
|
649
|
+
when Protocol::KIND_UINT32
|
|
650
|
+
raise Error.new("invalid_argument", "encrypted uint value out of range") if value.negative? || value > 0xFFFFFFFF
|
|
651
|
+
[value].pack("L<")
|
|
652
|
+
when Protocol::KIND_UINT64
|
|
653
|
+
raise Error.new("invalid_argument", "encrypted uint value out of range") if value.negative? || value > 0xFFFFFFFFFFFFFFFF
|
|
654
|
+
[value].pack("Q<")
|
|
655
|
+
when Protocol::KIND_TIMESTAMPTZ
|
|
656
|
+
if value.is_a?(Time)
|
|
657
|
+
ns = (value.to_r * 1_000_000_000).to_i
|
|
658
|
+
[ns].pack("q<")
|
|
659
|
+
elsif value.is_a?(Integer)
|
|
660
|
+
[value].pack("q<")
|
|
661
|
+
else
|
|
662
|
+
raise Error.new("invalid_argument", "encrypted TIMESTAMPTZ value must be Time or unix nanoseconds")
|
|
663
|
+
end
|
|
664
|
+
when Protocol::KIND_DECIMAL
|
|
665
|
+
dec_str = value.is_a?(BigDecimal) ? value.to_s("F") : value.to_s
|
|
666
|
+
Protocol.encode_decimal(dec_str)
|
|
667
|
+
when Protocol::KIND_JSON
|
|
668
|
+
json_str = value.is_a?(String) ? value : JSON.generate(value)
|
|
669
|
+
b = json_str.encode("utf-8").b
|
|
670
|
+
[b.bytesize].pack("V") + b
|
|
671
|
+
else
|
|
672
|
+
raise Error.new("invalid_argument", "unsupported client-encrypted type")
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def decode_scalar(ft, raw)
|
|
677
|
+
case ft.kind
|
|
678
|
+
when Protocol::KIND_UUID
|
|
679
|
+
raise Error.new("invalid_format", "invalid encrypted UUID payload") if raw.bytesize != 16
|
|
680
|
+
Protocol.format_uuid(raw)
|
|
681
|
+
when Protocol::KIND_STRING, Protocol::KIND_TEXT
|
|
682
|
+
raise Error.new("invalid_format", "truncated string payload") if raw.bytesize < 4
|
|
683
|
+
len = raw[0, 4].unpack1("V")
|
|
684
|
+
raise Error.new("invalid_format", "string payload length mismatch") if raw.bytesize != 4 + len
|
|
685
|
+
raw[4..].force_encoding("utf-8")
|
|
686
|
+
when Protocol::KIND_BLOB
|
|
687
|
+
raise Error.new("invalid_format", "truncated blob payload") if raw.bytesize < 4
|
|
688
|
+
len = raw[0, 4].unpack1("V")
|
|
689
|
+
raise Error.new("invalid_format", "blob payload length mismatch") if raw.bytesize != 4 + len
|
|
690
|
+
raw[4..]
|
|
691
|
+
when Protocol::KIND_BOOL
|
|
692
|
+
raise Error.new("invalid_format", "invalid encrypted bool payload") if raw.bytesize != 1
|
|
693
|
+
raw.getbyte(0) != 0
|
|
694
|
+
when Protocol::KIND_INT8
|
|
695
|
+
raise Error.new("invalid_format", "invalid encrypted int payload") if raw.bytesize != 1
|
|
696
|
+
raw.unpack1("c")
|
|
697
|
+
when Protocol::KIND_INT16
|
|
698
|
+
raise Error.new("invalid_format", "invalid encrypted int payload") if raw.bytesize != 2
|
|
699
|
+
raw.unpack1("s<")
|
|
700
|
+
when Protocol::KIND_INT32
|
|
701
|
+
raise Error.new("invalid_format", "invalid encrypted int payload") if raw.bytesize != 4
|
|
702
|
+
raw.unpack1("l<")
|
|
703
|
+
when Protocol::KIND_INT64
|
|
704
|
+
raise Error.new("invalid_format", "invalid encrypted int payload") if raw.bytesize != 8
|
|
705
|
+
raw.unpack1("q<")
|
|
706
|
+
when Protocol::KIND_UINT8
|
|
707
|
+
raise Error.new("invalid_format", "invalid encrypted uint payload") if raw.bytesize != 1
|
|
708
|
+
raw.unpack1("C")
|
|
709
|
+
when Protocol::KIND_UINT16
|
|
710
|
+
raise Error.new("invalid_format", "invalid encrypted uint payload") if raw.bytesize != 2
|
|
711
|
+
raw.unpack1("S<")
|
|
712
|
+
when Protocol::KIND_UINT32
|
|
713
|
+
raise Error.new("invalid_format", "invalid encrypted uint payload") if raw.bytesize != 4
|
|
714
|
+
raw.unpack1("L<")
|
|
715
|
+
when Protocol::KIND_UINT64
|
|
716
|
+
raise Error.new("invalid_format", "invalid encrypted uint payload") if raw.bytesize != 8
|
|
717
|
+
raw.unpack1("Q<")
|
|
718
|
+
when Protocol::KIND_TIMESTAMPTZ
|
|
719
|
+
raise Error.new("invalid_format", "invalid encrypted timestamptz payload") if raw.bytesize != 8
|
|
720
|
+
ns = raw.unpack1("q<")
|
|
721
|
+
Time.at(Rational(ns, 1_000_000_000)).utc
|
|
722
|
+
when Protocol::KIND_DECIMAL
|
|
723
|
+
raise Error.new("invalid_format", "truncated decimal payload") if raw.bytesize < 4
|
|
724
|
+
len = raw[0, 4].unpack1("V")
|
|
725
|
+
raise Error.new("invalid_format", "decimal payload length mismatch") if raw.bytesize != 4 + len
|
|
726
|
+
Protocol.decode_decimal(raw[4..])
|
|
727
|
+
when Protocol::KIND_JSON
|
|
728
|
+
raise Error.new("invalid_format", "truncated json payload") if raw.bytesize < 4
|
|
729
|
+
len = raw[0, 4].unpack1("V")
|
|
730
|
+
raise Error.new("invalid_format", "json payload length mismatch") if raw.bytesize != 4 + len
|
|
731
|
+
JSON.parse(raw[4..].force_encoding("utf-8"))
|
|
732
|
+
else
|
|
733
|
+
raise Error.new("invalid_format", "unsupported client-encrypted type")
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# --- RFC 5297 AES-SIV implementation ---
|
|
738
|
+
|
|
739
|
+
def hkdf_sha256(ikm, info, length = 32)
|
|
740
|
+
prk = OpenSSL::HMAC.digest("SHA256", "".b, ikm)
|
|
741
|
+
t = "".b
|
|
742
|
+
okm = +"".b
|
|
743
|
+
i = 1
|
|
744
|
+
while okm.bytesize < length
|
|
745
|
+
t = OpenSSL::HMAC.digest("SHA256", prk, t + info + [i].pack("C"))
|
|
746
|
+
okm << t
|
|
747
|
+
i += 1
|
|
748
|
+
end
|
|
749
|
+
okm[0, length]
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def aes_block(key, block)
|
|
753
|
+
c = OpenSSL::Cipher.new("aes-128-ecb")
|
|
754
|
+
c.encrypt
|
|
755
|
+
c.key = key
|
|
756
|
+
c.padding = 0
|
|
757
|
+
c.update(block) + c.final
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def dbl(block)
|
|
761
|
+
out = +"\x00".b * 16
|
|
762
|
+
carry = 0
|
|
763
|
+
15.downto(0) do |i|
|
|
764
|
+
b = block.getbyte(i)
|
|
765
|
+
out.setbyte(i, ((b << 1) & 0xFF) | carry)
|
|
766
|
+
carry = (b >> 7) & 1
|
|
767
|
+
end
|
|
768
|
+
out.setbyte(15, out.getbyte(15) ^ 0x87) if carry != 0
|
|
769
|
+
out
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def xor_16(a, b)
|
|
773
|
+
res = +"\x00".b * 16
|
|
774
|
+
16.times { |i| res.setbyte(i, a.getbyte(i) ^ b.getbyte(i)) }
|
|
775
|
+
res
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def cmac(key, message)
|
|
779
|
+
k1 = dbl(aes_block(key, "\x00".b * 16))
|
|
780
|
+
k2 = dbl(k1)
|
|
781
|
+
n = message.bytesize
|
|
782
|
+
blocks = [1, (n + 15) / 16].max
|
|
783
|
+
last = if n.positive? && (n % 16).zero?
|
|
784
|
+
xor_16(message[(n - 16)..], k1)
|
|
785
|
+
else
|
|
786
|
+
rem = n % 16
|
|
787
|
+
pad = message[(n - rem)..] + "\x80".b + ("\x00".b * (15 - rem))
|
|
788
|
+
xor_16(pad, k2)
|
|
789
|
+
end
|
|
790
|
+
y = "\x00".b * 16
|
|
791
|
+
(blocks - 1).times do |i|
|
|
792
|
+
blk = message[(i * 16), 16]
|
|
793
|
+
y = aes_block(key, xor_16(y, blk))
|
|
794
|
+
end
|
|
795
|
+
aes_block(key, xor_16(y, last))
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def s2v(key, ad, plain)
|
|
799
|
+
d = dbl(cmac(key, "\x00".b * 16))
|
|
800
|
+
d = xor_16(d, cmac(key, ad))
|
|
801
|
+
t = if plain.bytesize >= 16
|
|
802
|
+
off = plain.bytesize - 16
|
|
803
|
+
plain[0...off] + xor_16(plain[off..], d)
|
|
804
|
+
else
|
|
805
|
+
pad = plain + "\x80".b + ("\x00".b * (15 - plain.bytesize))
|
|
806
|
+
xor_16(dbl(d), pad)
|
|
807
|
+
end
|
|
808
|
+
cmac(key, t)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def ctr_crypt(key, tag, input)
|
|
812
|
+
q = tag.dup
|
|
813
|
+
q.setbyte(8, q.getbyte(8) & 0x7F)
|
|
814
|
+
q.setbyte(12, q.getbyte(12) & 0x7F)
|
|
815
|
+
c = OpenSSL::Cipher.new("aes-128-ctr")
|
|
816
|
+
c.encrypt
|
|
817
|
+
c.key = key
|
|
818
|
+
c.iv = q
|
|
819
|
+
c.update(input) + c.final
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|