cloak-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2398d4e9c6598ab9bc282e3a4ac5d84bd4a7c6ac168b02018432aebdd1ef59f0
4
+ data.tar.gz: 0d5ccd2205af4e50a089f36a1e93b78825f18fa6945aa2202d9b361ccf33d093
5
+ SHA512:
6
+ metadata.gz: 5e0f4ea4ccd8f082390ea2111e7788fab4961038bcfce70f647b7585a0806733c38266d282f0366f9315aa6cbc9ceb5bcfd91dbd162f0239250823ac9cf2f54c
7
+ data.tar.gz: debb76ae8864205d3a1363dbb56dfc24d09bb20242184a76a0541c82a24446b2f63eb3e27f0c9fe578b22cfb8c56f39a41b5cd2e164b968a882821d027e1ca69
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2020-12-14)
2
+
3
+ - First release
@@ -0,0 +1,46 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Andrew Kane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
23
+ ---
24
+
25
+ Code snippets from redis-rb
26
+
27
+ Copyright (c) 2009 Ezra Zygmuntowicz
28
+
29
+ Permission is hereby granted, free of charge, to any person obtaining
30
+ a copy of this software and associated documentation files (the
31
+ "Software"), to deal in the Software without restriction, including
32
+ without limitation the rights to use, copy, modify, merge, publish,
33
+ distribute, sublicense, and/or sell copies of the Software, and to
34
+ permit persons to whom the Software is furnished to do so, subject to
35
+ the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be
38
+ included in all copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
41
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
42
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
43
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
44
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
45
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
46
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,113 @@
1
+ # Cloak
2
+
3
+ :fire: Application-level encryption for Redis and Memcached
4
+
5
+ Encrypts keys, values, list elements, set members, and hash fields while still being able to perform a majority of operations :tada:
6
+
7
+ See [technical details](#technical-details) for more info.
8
+
9
+ [![Build Status](https://github.com/ankane/cloak/workflows/build/badge.svg?branch=master)](https://github.com/ankane/cloak/actions)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application’s Gemfile:
14
+
15
+ ```ruby
16
+ gem 'cloak-rb'
17
+ ```
18
+
19
+ ## Getting Started
20
+
21
+ Generate a key
22
+
23
+ ```ruby
24
+ Cloak.generate_key
25
+ ```
26
+
27
+ Store the key with your other secrets. This is typically an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this) or Rails credentials. Be sure to use different keys in development and production. Set the following environment variable with your key (you can use this one in development)
28
+
29
+ ```sh
30
+ CLOAK_KEY=0000000000000000000000000000000000000000000000000000000000000000
31
+ ```
32
+
33
+ or add it to your credentials for each environment (`rails credentials:edit --environment <env>` for Rails 6+)
34
+
35
+ ```yml
36
+ cloak_key: "0000000000000000000000000000000000000000000000000000000000000000"
37
+ ```
38
+
39
+ Then follow the instructions for your key-value store.
40
+
41
+ - [Redis](#redis)
42
+ - [Memcached](#memcached)
43
+
44
+ ## Redis
45
+
46
+ *Requires the [redis](https://github.com/redis/redis-rb) gem*
47
+
48
+ Create a client
49
+
50
+ ```ruby
51
+ redis = Cloak::Redis.new(key: key)
52
+ ```
53
+
54
+ And use it in place of a `Redis` instance. A few methods aren’t supported:
55
+
56
+ - `lrem` since encrypted list elements aren’t comparable
57
+ - `setrange`, `setbit`, `append`, and `bitop` since encrypted strings can’t be modified in-place
58
+
59
+ Also, for sorted sets, members having the same score are not guaranteed to be returned in lexographical order.
60
+
61
+ ## Memcached
62
+
63
+ *Requires the [dalli](https://github.com/petergoldstein/dalli) gem*
64
+
65
+ Create a client
66
+
67
+ ```ruby
68
+ dalli = Cloak::Dalli.new(key: key)
69
+ ```
70
+
71
+ And use it in place a `Dalli::Client` instance.
72
+
73
+ ## Technical Details
74
+
75
+ Cloak uses [AES-SIV](https://github.com/miscreant/meta/wiki/AES-SIV), which supports deterministic encryption. Unlike most encryption algorithms, AES-SIV supports nonce reuse without catastrophic failure (like AES-GCM) or leaking prefix information (like AES-CBC).
76
+
77
+ - Items that need to be comparable across keys use a fixed nonce (keys, set members, HyperLogLog elements)
78
+ - Items that need to be comparable within a key use a key-specific nonce (hash fields)
79
+ - Other items use a random nonce (string values, list elements, hash values)
80
+
81
+ The fixed nonces are `\x00` bytes for keys, `\x01` bytes for set members, and `\x02` bytes for HyperLogLog elements. Key-specific nonces for hash fields are the first 16 bytes of encrypted key.
82
+
83
+ Commands, expiration times, increment/decrement values, and sorted set scores are not encrypted.
84
+
85
+ ## Key Rotation
86
+
87
+ Key rotation is not supported right now, but may be possible in a limited capacity in the future.
88
+
89
+ ## Credits
90
+
91
+ Thanks to [Miscreant](https://github.com/miscreant/miscreant.rb) for AES-SIV encryption.
92
+
93
+ ## History
94
+
95
+ View the [changelog](https://github.com/ankane/cloak/blob/master/CHANGELOG.md)
96
+
97
+ ## Contributing
98
+
99
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
100
+
101
+ - [Report bugs](https://github.com/ankane/cloak/issues)
102
+ - Fix bugs and [submit pull requests](https://github.com/ankane/cloak/pulls)
103
+ - Write, clarify, or fix documentation
104
+ - Suggest or add new features
105
+
106
+ To get started with development:
107
+
108
+ ```sh
109
+ git clone https://github.com/ankane/cloak.git
110
+ cd cloak
111
+ bundle install
112
+ bundle exec rake test
113
+ ```
@@ -0,0 +1,20 @@
1
+ # dependencies
2
+ require "miscreant"
3
+
4
+ # stdlib
5
+ require "forwardable"
6
+
7
+ # modules
8
+ require "cloak/utils"
9
+ require "cloak/version"
10
+
11
+ module Cloak
12
+ class Error < StandardError; end
13
+
14
+ autoload :Dalli, "cloak/dalli"
15
+ autoload :Redis, "cloak/redis"
16
+
17
+ def self.generate_key
18
+ Miscreant::AEAD.generate_key.unpack("H*").first
19
+ end
20
+ end
@@ -0,0 +1,62 @@
1
+ require "dalli"
2
+
3
+ module Cloak
4
+ # don't extend Dalli::Client so we can confirm operations are safe before adding
5
+ class Dalli
6
+ extend Forwardable
7
+ include Utils
8
+
9
+ def_delegators :@dalli, :flush, :flush_all, :stats, :reset_stats, :alive!, :version, :reset, :close
10
+
11
+ # need to use servers = nil instead of *args for Ruby < 2.7
12
+ def initialize(servers = nil, key: nil, **options)
13
+ @dalli = ::Dalli::Client.new(servers, options)
14
+ create_encryptor(key)
15
+ end
16
+
17
+ def get(key, options = nil)
18
+ decrypt_value(@dalli.get(encrypt_key(key), options))
19
+ end
20
+
21
+ def get_multi(*keys)
22
+ res = {}
23
+ @dalli.get_multi(*keys.map { |k| encrypt_key(k) }).each do |k, v|
24
+ res[decrypt_key(k)] = decrypt_value(v)
25
+ end
26
+ res
27
+ end
28
+
29
+ def fetch(key, ttl = nil, options = nil, &blk)
30
+ wrapped_blk = proc { encrypt_value(blk.call) } if blk
31
+ decrypt_value(@dalli.fetch(encrypt_key(key), ttl, options, &wrapped_blk))
32
+ end
33
+
34
+ def set(key, value, ttl = nil, options = nil)
35
+ @dalli.set(encrypt_key(key), encrypt_value(value), ttl, options)
36
+ end
37
+
38
+ def add(key, value, ttl = nil, options = nil)
39
+ @dalli.add(encrypt_key(key), encrypt_value(value), ttl, options)
40
+ end
41
+
42
+ def replace(key, value, ttl = nil, options = nil)
43
+ @dalli.replace(encrypt_key(key), encrypt_value(value), ttl, options)
44
+ end
45
+
46
+ def delete(key)
47
+ @dalli.delete(encrypt_key(key))
48
+ end
49
+
50
+ def incr(key, amt = 1, ttl = nil, default = nil)
51
+ @dalli.incr(encrypt_key(key), amt, ttl, default)
52
+ end
53
+
54
+ def decr(key, amt = 1, ttl = nil, default = nil)
55
+ @dalli.decr(encrypt_key(key), amt, ttl, default)
56
+ end
57
+
58
+ def touch(key, ttl = nil)
59
+ @dalli.touch(encrypt_key(key), ttl)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,757 @@
1
+ require "redis"
2
+
3
+ module Cloak
4
+ # don't extend Redis so we can confirm operations are safe before adding
5
+ class Redis
6
+ extend Forwardable
7
+ include Utils
8
+
9
+ # client setname and getname not encrypted
10
+ def_delegators :@redis, :auth, :select, :quit, :bgrewriteaof, :bgsave,
11
+ :config, :client, :dbsize, :flushall, :flushdb, :info, :lastsave,
12
+ :monitor, :save, :shutdown, :slaveof, :slowlog, :sync, :time,
13
+ :unwatch, :pipelined, :multi, :exec, :discard
14
+
15
+ def initialize(key: nil, **options)
16
+ @redis = ::Redis.new(**options)
17
+ create_encryptor(key)
18
+ end
19
+
20
+ def debug(*args)
21
+ args[1] = encrypt_key(args[1]) if args[0] == "object"
22
+ @redis.debug(*args)
23
+ end
24
+
25
+ def ping(message = nil)
26
+ if message.nil?
27
+ @redis.ping
28
+ else
29
+ on_result(@redis.ping(encrypt_value(message))) do |res|
30
+ decrypt_value(res)
31
+ end
32
+ end
33
+ end
34
+
35
+ def echo(value)
36
+ on_result(@redis.echo(encrypt_value(value))) do |res|
37
+ decrypt_value(res)
38
+ end
39
+ end
40
+
41
+ def persist(key)
42
+ @redis.persist(encrypt_key(key))
43
+ end
44
+
45
+ def expire(key, seconds)
46
+ @redis.expire(encrypt_key(key), seconds)
47
+ end
48
+
49
+ def expireat(key, unix_time)
50
+ @redis.expireat(encrypt_key(key), unix_time)
51
+ end
52
+
53
+ def ttl(key)
54
+ @redis.ttl(encrypt_key(key))
55
+ end
56
+
57
+ def pexpire(key, milliseconds)
58
+ @redis.pexpire(encrypt_key(key), milliseconds)
59
+ end
60
+
61
+ def pexpireat(key, ms_unix_time)
62
+ @redis.pexpireat(encrypt_key(key), ms_unix_time)
63
+ end
64
+
65
+ def pttl(key)
66
+ @redis.pttl(encrypt_key(key))
67
+ end
68
+
69
+ def dump(key)
70
+ @redis.dump(encrypt_key(key))
71
+ end
72
+
73
+ def restore(key, ttl, serialized_value, replace: nil)
74
+ @redis.restore(encrypt_key(key), ttl, serialized_value, replace: replace)
75
+ end
76
+
77
+ def del(*keys)
78
+ @redis.del(*keys.map { |k| encrypt_key(k) })
79
+ end
80
+
81
+ def unlink(*keys)
82
+ @redis.unlink(*keys.map { |k| encrypt_key(k) })
83
+ end
84
+
85
+ def exists(*keys)
86
+ @redis.exists(*keys.map { |k| encrypt_key(k) })
87
+ end
88
+
89
+ def exists?(*keys)
90
+ @redis.exists?(*keys.map { |k| encrypt_key(k) })
91
+ end
92
+
93
+ # could match in-memory
94
+ def keys(pattern = "*")
95
+ raise "Only * pattern supported" if pattern != "*"
96
+ on_result(@redis.keys(pattern)) do |res|
97
+ res.map { |k| decrypt_key(k) }
98
+ end
99
+ end
100
+
101
+ def move(key, db)
102
+ @redis.move(encrypt_key(key), db)
103
+ end
104
+
105
+ def object(*args)
106
+ args[1] = encrypt_key(args[1]) if args.size > 1
107
+ @redis.object(*args)
108
+ end
109
+
110
+ def randomkey
111
+ on_result(@redis.randomkey) do |res|
112
+ res.nil? ? res : decrypt_key(res)
113
+ end
114
+ end
115
+
116
+ def rename(old_name, new_name)
117
+ @redis.rename(encrypt_key(old_name), encrypt_key(new_name))
118
+ end
119
+
120
+ def renamenx(old_name, new_name)
121
+ @redis.renamenx(encrypt_key(old_name), encrypt_key(new_name))
122
+ end
123
+
124
+ # sort not supported
125
+
126
+ def type(key)
127
+ @redis.type(encrypt_key(key))
128
+ end
129
+
130
+ def decr(key)
131
+ @redis.decr(encrypt_key(key))
132
+ end
133
+
134
+ def decrby(key, decrement)
135
+ @redis.decrby(encrypt_key(key), decrement)
136
+ end
137
+
138
+ def incr(key)
139
+ @redis.incr(encrypt_key(key))
140
+ end
141
+
142
+ def incrby(key, increment)
143
+ @redis.incrby(encrypt_key(key), increment)
144
+ end
145
+
146
+ def incrbyfloat(key, increment)
147
+ @redis.incrbyfloat(encrypt_key(key), increment)
148
+ end
149
+
150
+ def set(key, value, **options)
151
+ @redis.set(encrypt_key(key), encrypt_value(value), **options)
152
+ end
153
+
154
+ def setex(key, ttl, value)
155
+ @redis.setex(encrypt_key(key), ttl, encrypt_value(value))
156
+ end
157
+
158
+ def psetex(key, ttl, value)
159
+ @redis.psetex(encrypt_key(key), ttl, encrypt_value(value))
160
+ end
161
+
162
+ def setnx(key, value)
163
+ @redis.setnx(encrypt_key(key), ttl, encrypt_value(value))
164
+ end
165
+
166
+ def mset(*args)
167
+ @redis.mset(args.map.with_index { |v, i| i % 2 == 0 ? encrypt_key(v) : encrypt_value(v) })
168
+ end
169
+
170
+ # match redis
171
+ def mapped_mset(hash)
172
+ mset(hash.to_a.flatten)
173
+ end
174
+
175
+ def msetnx(*args)
176
+ @redis.msetnx(args.map.with_index { |v, i| i % 2 == 0 ? encrypt_key(v) : encrypt_value(v) })
177
+ end
178
+
179
+ # match redis
180
+ def mapped_msetnx(hash)
181
+ msetnx(hash.to_a.flatten)
182
+ end
183
+
184
+ def get(key)
185
+ on_result(@redis.get(encrypt_key(key))) do |res|
186
+ decrypt_value(res)
187
+ end
188
+ end
189
+
190
+ def mget(*keys, &blk)
191
+ on_result(@redis.mget(*keys.map { |k| encrypt_key(k) }, &blk)) do |res|
192
+ res.map { |v| decrypt_value(v) }
193
+ end
194
+ end
195
+
196
+ def mapped_mget(*keys)
197
+ on_result(@redis.mapped_mget(*keys.map { |k| encrypt_key(k) })) do |res|
198
+ res.map { |k, v| [decrypt_key(k), decrypt_value(v)] }.to_h
199
+ end
200
+ end
201
+
202
+ # setrange not supported
203
+
204
+ def getrange(key, start, stop)
205
+ on_result(@redis.get(encrypt_key(key))) do |res|
206
+ decrypt_value(res)[start..stop]
207
+ end
208
+ end
209
+
210
+ # setbit not supported
211
+
212
+ # TODO raise "ERR bit offset is not an integer or out of range" when needed
213
+ def getbit(key, offset)
214
+ on_result(@redis.get(encrypt_key(key))) do |res|
215
+ v = decrypt_value(res)
216
+ v.nil? ? 0 : v.unpack1("B*")[offset].to_i
217
+ end
218
+ end
219
+
220
+ # append not supported
221
+
222
+ def bitcount(key, start = 0, stop = -1)
223
+ on_result(@redis.get(encrypt_key(key))) do |res|
224
+ decrypt_value(res)[start..stop].unpack1("B*").count("1")
225
+ end
226
+ end
227
+
228
+ # bitop not supported
229
+
230
+ def bitpos(key, bit, start = nil, stop = nil)
231
+ on_result(@redis.get(encrypt_key(key))) do |res|
232
+ pos = decrypt_value(res)[(start || 0)..(stop || -1)].unpack1("B*").index(bit.to_s)
233
+ pos ? pos + (start.to_i * 8) : -1
234
+ end
235
+ end
236
+
237
+ def getset(key, value)
238
+ on_result(@redis.getset(encrypt_key(key), encrypt_value(value))) do |res|
239
+ decrypt_value(res)
240
+ end
241
+ end
242
+
243
+ # subtract nonce size (16) and auth tag (16)
244
+ def strlen(key)
245
+ on_result(@redis.strlen(encrypt_key(key))) do |res|
246
+ res == 0 ? 0 : res - 32
247
+ end
248
+ end
249
+
250
+ def llen(key)
251
+ @redis.llen(encrypt_key(key))
252
+ end
253
+
254
+ def lpush(key, value)
255
+ @redis.lpush(encrypt_key(key), value.is_a?(Array) ? value.map { |v| encrypt_element(v) } : encrypt_element(value))
256
+ end
257
+
258
+ def lpushx(key, value)
259
+ @redis.lpushx(encrypt_key(key), encrypt_element(value))
260
+ end
261
+
262
+ def rpush(key, value)
263
+ @redis.rpush(encrypt_key(key), value.is_a?(Array) ? value.map { |v| encrypt_element(v) } : encrypt_element(value))
264
+ end
265
+
266
+ def rpushx(key, value)
267
+ @redis.rpushx(encrypt_key(key), encrypt_element(value))
268
+ end
269
+
270
+ def lpop(key)
271
+ @redis.lpop(encrypt_key(key))
272
+ end
273
+
274
+ def rpop(key)
275
+ @redis.rpop(encrypt_key(key))
276
+ end
277
+
278
+ def rpoplpush(source, destination)
279
+ @redis.rpoplpush(encrypt_key(source), encrypt_key(destination))
280
+ end
281
+
282
+ def blpop(*args)
283
+ _bpop(:blpop, args)
284
+ end
285
+
286
+ def brpop(*args)
287
+ _bpop(:brpop, args)
288
+ end
289
+
290
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
291
+ @redis.brpoplpush(encrypt_key(source), encrypt_key(destination), timeout: timeout)
292
+ end
293
+
294
+ def lindex(key, index)
295
+ on_result(@redis.lindex(encrypt_key(key), index)) do |res|
296
+ decrypt_element(res)
297
+ end
298
+ end
299
+
300
+ def linsert(key, where, pivot, value)
301
+ @redis.linsert(encrypt_key(key), where, pivot, encrypt_element(value))
302
+ end
303
+
304
+ def lrange(key, start, stop)
305
+ @redis.lrange(encrypt_key(key), start, stop)
306
+ end
307
+
308
+ # lrem not possible with random nonce
309
+
310
+ def lset(key, index, value)
311
+ @redis.lset(encrypt_key(key), index, encrypt_element(value))
312
+ end
313
+
314
+ def ltrim(key, start, stop)
315
+ @redis.ltrim(encrypt_key(key), start, stop)
316
+ end
317
+
318
+ def scard(key)
319
+ @redis.scard(encrypt_key(key))
320
+ end
321
+
322
+ def sadd(key, member)
323
+ @redis.sadd(encrypt_key(key), encrypt_member(member))
324
+ end
325
+
326
+ def srem(key, member)
327
+ @redis.srem(encrypt_key(key), encrypt_member(member))
328
+ end
329
+
330
+ def spop(key, count = nil)
331
+ on_result(@redis.spop(encrypt_key(key))) do |res|
332
+ if count.nil?
333
+ decrypt_member(res)
334
+ else
335
+ res.map { |v| decrypt_member(v) }
336
+ end
337
+ end
338
+ end
339
+
340
+ def srandmember(key, count = nil)
341
+ on_result(@redis.srandmember(encrypt_key(key))) do |res|
342
+ if count.nil?
343
+ decrypt_member(res)
344
+ else
345
+ res.map { |v| decrypt_member(v) }
346
+ end
347
+ end
348
+ end
349
+
350
+ def smove(source, destination, member)
351
+ @redis.smove(encrypt_key(source), encrypt_key(destination), encrypt_member(member))
352
+ end
353
+
354
+ def sismember(key, member)
355
+ @redis.sismember(encrypt_key(key), encrypt_member(member))
356
+ end
357
+
358
+ def smembers(key)
359
+ on_result(@redis.smembers(encrypt_key(key))) do |res|
360
+ res.map { |v| decrypt_member(v) }
361
+ end
362
+ end
363
+
364
+ def sdiff(*keys)
365
+ on_result(@redis.sdiff(*keys.map { |k| encrypt_key(k) })) do |res|
366
+ res.map { |v| decrypt_member(v) }
367
+ end
368
+ end
369
+
370
+ def sdiffstore(destination, *keys)
371
+ @redis.sdiffstore(encrypt_key(destination), *keys.map { |k| encrypt_key(k) })
372
+ end
373
+
374
+ def sinter(*keys)
375
+ on_result(@redis.sinter(*keys.map { |k| encrypt_key(k) })) do |res|
376
+ res.map { |v| decrypt_member(v) }
377
+ end
378
+ end
379
+
380
+ def sinterstore(destination, *keys)
381
+ @redis.sinterstore(encrypt_key(destination), *keys.map { |k| encrypt_key(k) })
382
+ end
383
+
384
+ def sunion(*keys)
385
+ on_result(@redis.sunion(*keys.map { |k| encrypt_key(k) })) do |res|
386
+ res.map { |v| decrypt_member(v) }
387
+ end
388
+ end
389
+
390
+ def sunionstore(destination, *keys)
391
+ @redis.sunionstore(encrypt_key(destination), *keys.map { |k| encrypt_key(k) })
392
+ end
393
+
394
+ def zcard(key)
395
+ @redis.zcard(encrypt_key(key))
396
+ end
397
+
398
+ def zadd(key, *args, **options)
399
+ if args.size == 1 && args[0].is_a?(Array)
400
+ args = args[0]
401
+ elsif args.size == 2
402
+ args = [args]
403
+ else
404
+ raise ArgumentError, "wrong number of arguments"
405
+ end
406
+
407
+ # convert score to numeric to avoid data leakage
408
+ # if there's an issue with arguments
409
+ @redis.zadd(encrypt_key(key), args.map { |v| [to_score(v[0]), encrypt_member(v[1])] }, **options)
410
+ end
411
+
412
+ def zincrby(key, increment, member)
413
+ @redis.zincrby(encrypt_key(key), increment, encrypt_member(member))
414
+ end
415
+
416
+ def zrem(key, member)
417
+ @redis.zrem(encrypt_key(key), member.is_a?(Array) ? member.map { |v| encrypt_member(v) } : encrypt_member(member))
418
+ end
419
+
420
+ def zpopmax(key, count = nil)
421
+ on_result(@redis.zpopmax(encrypt_key(key), count)) do |res|
422
+ if count.to_i > 1
423
+ res.map { |v, s| [decrypt_member(v), s] }
424
+ else
425
+ [decrypt_member(res[0]), res[1]]
426
+ end
427
+ end
428
+ end
429
+
430
+ def zpopmin(key, count = nil)
431
+ on_result(@redis.zpopmin(encrypt_key(key), count)) do |res|
432
+ if count.to_i > 1
433
+ res.map { |v, s| [decrypt_member(v), s] }
434
+ else
435
+ [decrypt_member(res[0]), res[1]]
436
+ end
437
+ end
438
+ end
439
+
440
+ def bzpopmax(*args)
441
+ _bpop(:bzpopmax, args, zset: true)
442
+ end
443
+
444
+ def bzpopmin(*args)
445
+ _bpop(:bzpopmin, args, zset: true)
446
+ end
447
+
448
+ def zscore(key, member)
449
+ @redis.zscore(encrypt_key(key), encrypt_member(member))
450
+ end
451
+
452
+ # can't guarantee lexographical order without potentially fetching all elements
453
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
454
+ on_result(@redis.zrange(encrypt_key(key), start, stop, with_scores: with_scores)) do |res|
455
+ if with_scores
456
+ res.map { |v, s| [decrypt_member(v), s] }
457
+ else
458
+ res.map { |v| decrypt_member(v) }
459
+ end
460
+ end
461
+ end
462
+
463
+ # can't guarantee lexographical order without potentially fetching all elements
464
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
465
+ on_result(@redis.zrevrange(encrypt_key(key), start, stop, with_scores: with_scores)) do |res|
466
+ if with_scores
467
+ res.map { |v, s| [decrypt_member(v), s] }
468
+ else
469
+ res.map { |v| decrypt_member(v) }
470
+ end
471
+ end
472
+ end
473
+
474
+ def zrank(key, member)
475
+ @redis.zrank(encrypt_key(key), encrypt_member(member))
476
+ end
477
+
478
+ def zrevrank(key, member)
479
+ @redis.zrevrank(encrypt_key(key), encrypt_member(member))
480
+ end
481
+
482
+ def zremrangebyrank(key, start, stop)
483
+ @redis.zremrangebyrank(encrypt_key(key), start, stop)
484
+ end
485
+
486
+ # zlexcount not supported (could support - + range)
487
+ # zrangebylex not supported
488
+ # zrevrangebylex not supported
489
+
490
+ # could guarantee lexographical order when limit not used
491
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
492
+ on_result(@redis.zrangebyscore(encrypt_key(key), min, max, with_scores: with_scores, limit: limit)) do |res|
493
+ if with_scores
494
+ res.map { |v, s| [decrypt_member(v), s] }
495
+ else
496
+ res.map { |v| decrypt_member(v) }
497
+ end
498
+ end
499
+ end
500
+
501
+ # could guarantee lexographical order when limit not used
502
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
503
+ on_result(@redis.zrevrangebyscore(encrypt_key(key), max, min, with_scores: with_scores, limit: limit)) do |res|
504
+ if with_scores
505
+ res.map { |v, s| [decrypt_member(v), s] }
506
+ else
507
+ res.map { |v| decrypt_member(v) }
508
+ end
509
+ end
510
+ end
511
+
512
+ def zremrangebyscore(key, min, max)
513
+ @redis.zremrangebyscore(encrypt_key(key), min, max)
514
+ end
515
+
516
+ def zcount(key, min, max)
517
+ @redis.zcount(encrypt_key(key), min, max)
518
+ end
519
+
520
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
521
+ @redis.zinterstore(encrypt_key(destination), keys.map { |k| encrypt_key(k) }, weights: weights, aggregate: aggregate)
522
+ end
523
+
524
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
525
+ @redis.zunionstore(encrypt_key(destination), keys.map { |k| encrypt_key(k) }, weights: weights, aggregate: aggregate)
526
+ end
527
+
528
+ def hlen(key)
529
+ @redis.hlen(encrypt_key(key))
530
+ end
531
+
532
+ def hset(key, *attrs)
533
+ attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash)
534
+
535
+ ek = encrypt_key(key)
536
+ @redis.hset(ek, attrs.map.with_index { |v, i| i % 2 == 0 ? encrypt_field(ek, v) : encrypt_value(v) })
537
+ end
538
+
539
+ def hsetnx(key, field, value)
540
+ ek = encrypt_key(key)
541
+ @redis.hsetnx(ek, encrypt_field(ek, field), encrypt_value(value))
542
+ end
543
+
544
+ def hmset(key, *attrs)
545
+ ek = encrypt_key(key)
546
+ @redis.hset(ek, attrs.map.with_index { |v, i| i % 2 == 0 ? encrypt_field(ek, v) : encrypt_value(v) })
547
+ end
548
+
549
+ # match redis
550
+ def mapped_hmset(key, hash)
551
+ hmset(key, hash.to_a.flatten)
552
+ end
553
+
554
+ def hget(key, field)
555
+ ek = encrypt_key(key)
556
+ on_result(@redis.hget(ek, encrypt_field(ek, field))) do |res|
557
+ decrypt_value(res)
558
+ end
559
+ end
560
+
561
+ def hmget(key, *fields, &blk)
562
+ ek = encrypt_key(key)
563
+ on_result(@redis.hmget(ek, *fields.map { |f| encrypt_field(ek, f) }, &blk)) do |res|
564
+ res.map { |v| decrypt_value(v) }
565
+ end
566
+ end
567
+
568
+ def mapped_hmget(key, *fields)
569
+ ek = encrypt_key(key)
570
+ on_result(@redis.mapped_hmget(ek, *fields.map { |f| encrypt_field(ek, f) })) do |res|
571
+ res.map { |f, v| [decrypt_field(ek, f), decrypt_value(v)] }.to_h
572
+ end
573
+ end
574
+
575
+ def hdel(key, *fields)
576
+ ek = encrypt_key(key)
577
+ @redis.hdel(ek, *fields.map { |v| encrypt_field(ek, v) })
578
+ end
579
+
580
+ def hexists(key, field)
581
+ ek = encrypt_key(key)
582
+ @redis.hexists(ek, encrypt_field(ek, field))
583
+ end
584
+
585
+ def hincrby(key, field, increment)
586
+ ek = encrypt_key(key)
587
+ @redis.hincrby(ek, encrypt_field(ek, field), increment)
588
+ end
589
+
590
+ def hincrbyfloat(key, field, increment)
591
+ ek = encrypt_key(key)
592
+ @redis.hincrbyfloat(ek, encrypt_field(ek, field), increment)
593
+ end
594
+
595
+ def hkeys(key)
596
+ ek = encrypt_key(key)
597
+ on_result(@redis.hkeys(ek)) do |res|
598
+ res.map { |v| decrypt_field(ek, v) }
599
+ end
600
+ end
601
+
602
+ def hvals(key)
603
+ ek = encrypt_key(key)
604
+ on_result(@redis.hvals(ek)) do |res|
605
+ res.map { |v| decrypt_value(v) }
606
+ end
607
+ end
608
+
609
+ def hgetall(key)
610
+ ek = encrypt_key(key)
611
+ on_result(@redis.hgetall(ek)) do |res|
612
+ res.map { |f, v| [decrypt_field(ek, f), decrypt_value(v)] }.to_h
613
+ end
614
+ end
615
+
616
+ # TODO pubsub
617
+ # TODO watch
618
+
619
+ # match option not supported
620
+ def scan(cursor, count: nil)
621
+ on_result(@redis.scan(cursor, count: count)) do |res|
622
+ [res[0], res[1].map { |v| decrypt_key(v) }]
623
+ end
624
+ end
625
+
626
+ # match redis
627
+ def scan_each(**options, &block)
628
+ return to_enum(:scan_each, **options) unless block_given?
629
+
630
+ cursor = 0
631
+ loop do
632
+ cursor, keys = scan(cursor, **options)
633
+ keys.each(&block)
634
+ break if cursor == "0"
635
+ end
636
+ end
637
+
638
+ # match option not supported
639
+ def hscan(key, cursor, count: nil)
640
+ ek = encrypt_key(key)
641
+ on_result(@redis.hscan(ek, cursor, count: count)) do |res|
642
+ [res[0], res[1].map { |v| [decrypt_field(ek, v[0]), decrypt_value(v[1])] }]
643
+ end
644
+ end
645
+
646
+ # match redis
647
+ def hscan_each(key, **options, &block)
648
+ return to_enum(:hscan_each, key, **options) unless block_given?
649
+
650
+ cursor = 0
651
+ loop do
652
+ # hscan encrypts key
653
+ cursor, values = hscan(key, cursor, **options)
654
+ values.each(&block)
655
+ break if cursor == "0"
656
+ end
657
+ end
658
+
659
+ # match option not supported
660
+ def zscan(key, cursor, count: nil)
661
+ on_result(@redis.zscan(encrypt_key(key), cursor, count: count)) do |res|
662
+ [res[0], res[1].map { |v| [decrypt_member(v[0]), v[1]] }]
663
+ end
664
+ end
665
+
666
+ # match redis
667
+ def zscan_each(key, **options, &block)
668
+ return to_enum(:zscan_each, key, **options) unless block_given?
669
+
670
+ cursor = 0
671
+ loop do
672
+ # zscan encrypts key
673
+ cursor, values = zscan(key, cursor, **options)
674
+ values.each(&block)
675
+ break if cursor == "0"
676
+ end
677
+ end
678
+
679
+ # match option not supported
680
+ def sscan(key, cursor, count: nil)
681
+ on_result(@redis.sscan(encrypt_key(key), cursor, count: count)) do |res|
682
+ [res[0], res[1].map { |v| decrypt_member(v) }]
683
+ end
684
+ end
685
+
686
+ # match redis
687
+ def sscan_each(key, **options, &block)
688
+ return to_enum(:sscan_each, key, **options) unless block_given?
689
+
690
+ cursor = 0
691
+ loop do
692
+ # sscan encrypts key
693
+ cursor, keys = sscan(key, cursor, **options)
694
+ keys.each(&block)
695
+ break if cursor == "0"
696
+ end
697
+ end
698
+
699
+ def pfadd(key, member)
700
+ @redis.pfadd(encrypt_key(key), member.is_a?(Array) ? member.map { |v| encrypt_hll_element(v) } : encrypt_hll_element(member))
701
+ end
702
+
703
+ def pfcount(*keys)
704
+ @redis.pfcount(*keys.map { |k| encrypt_key(k) })
705
+ end
706
+
707
+ def pfmerge(dest_key, *source_key)
708
+ @redis.pfmerge(encrypt_key(dest_key), *source_key.map { |k| encrypt_key(k) })
709
+ end
710
+
711
+ # geo not supported
712
+ # streams not supported
713
+
714
+ private
715
+
716
+ def on_result(res, &block)
717
+ if res.is_a?(::Redis::Future)
718
+ res.instance_exec do
719
+ if @transformation
720
+ raise "Not implemented yet. Please create an issue."
721
+ else
722
+ @transformation = block
723
+ end
724
+ end
725
+ res
726
+ else
727
+ block.call(res)
728
+ end
729
+ end
730
+
731
+ def _bpop(cmd, args, zset: false, &blk)
732
+ # match redis
733
+ timeout = if args.last.is_a?(Hash)
734
+ options = args.pop
735
+ options[:timeout]
736
+ elsif args.last.respond_to?(:to_int)
737
+ args.pop.to_int
738
+ end
739
+
740
+ keys = args.flatten.map { |k| encrypt_key(k) }
741
+
742
+ on_result(@redis._bpop(cmd, [keys, {timeout: timeout}], &blk)) do |res|
743
+ if res.nil?
744
+ res
745
+ elsif zset
746
+ [decrypt_key(res[0]), decrypt_member(res[1]), res[2]]
747
+ else
748
+ [decrypt_key(res[0]), decrypt_element(res[1])]
749
+ end
750
+ end
751
+ end
752
+
753
+ def to_score(v)
754
+ v.is_a?(Numeric) ? v : v.to_f
755
+ end
756
+ end
757
+ end
@@ -0,0 +1,77 @@
1
+ module Cloak
2
+ module Utils
3
+ KEY_NONCE = "\x00".b*16
4
+ MEMBER_NONCE = "\x01".b*16
5
+ HLL_ELEMENT_NONCE = "\x02".b*16
6
+
7
+ private
8
+
9
+ def create_encryptor(key)
10
+ @encryptor = Miscreant::AEAD.new("AES-SIV", [key].pack("H*"))
11
+ end
12
+
13
+ def encrypt_value(value)
14
+ if value.nil?
15
+ value
16
+ else
17
+ nonce = Miscreant::AEAD.generate_nonce
18
+ nonce + @encryptor.seal(to_binary(value), nonce: nonce)
19
+ end
20
+ end
21
+
22
+ def decrypt_value(value)
23
+ if value.nil? || value.empty?
24
+ value
25
+ else
26
+ value = to_binary(value)
27
+ nonce = value.slice(0, 16)
28
+ value = value.slice(16..-1)
29
+ raise Error, "Decryption failed" if nonce.bytesize != 16 || value.nil?
30
+ value = @encryptor.open(value, nonce: nonce)
31
+ value.force_encoding(Encoding::UTF_8)
32
+ value
33
+ end
34
+ end
35
+
36
+ alias_method :encrypt_element, :encrypt_value
37
+ alias_method :decrypt_element, :decrypt_value
38
+
39
+ def encrypt_key(key)
40
+ @encryptor.seal(to_binary(key), nonce: KEY_NONCE)
41
+ end
42
+
43
+ def decrypt_key(key)
44
+ @encryptor.open(to_binary(key), nonce: KEY_NONCE)
45
+ end
46
+
47
+ def encrypt_field(key, field)
48
+ @encryptor.seal(to_binary(field), nonce: key.slice(0, 16))
49
+ end
50
+
51
+ def decrypt_field(key, field)
52
+ @encryptor.open(to_binary(field), nonce: key.slice(0, 16))
53
+ end
54
+
55
+ def encrypt_member(value)
56
+ @encryptor.seal(to_binary(value), nonce: MEMBER_NONCE)
57
+ end
58
+
59
+ def decrypt_member(value)
60
+ @encryptor.open(to_binary(value), nonce: MEMBER_NONCE)
61
+ end
62
+
63
+ def encrypt_hll_element(value)
64
+ @encryptor.seal(to_binary(value), nonce: HLL_ELEMENT_NONCE)
65
+ end
66
+
67
+ def decrypt_hll_element(value)
68
+ @encryptor.open(to_binary(value), nonce: HLL_ELEMENT_NONCE)
69
+ end
70
+
71
+ def to_binary(value)
72
+ value = value.to_s
73
+ value = value.dup.force_encoding(Encoding::BINARY) unless value.encoding == Encoding::BINARY
74
+ value
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Cloak
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloak-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Kane
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: miscreant
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email: andrew@chartkick.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - LICENSE.txt
35
+ - README.md
36
+ - lib/cloak-rb.rb
37
+ - lib/cloak/dalli.rb
38
+ - lib/cloak/redis.rb
39
+ - lib/cloak/utils.rb
40
+ - lib/cloak/version.rb
41
+ homepage: https://github.com/ankane/cloak
42
+ licenses:
43
+ - MIT
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '2.5'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.1.4
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Application-level encryption for Redis and Memcached
64
+ test_files: []