cloak-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +46 -0
- data/README.md +113 -0
- data/lib/cloak-rb.rb +20 -0
- data/lib/cloak/dalli.rb +62 -0
- data/lib/cloak/redis.rb +757 -0
- data/lib/cloak/utils.rb +77 -0
- data/lib/cloak/version.rb +3 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
```
|
data/lib/cloak-rb.rb
ADDED
@@ -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
|
data/lib/cloak/dalli.rb
ADDED
@@ -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
|
data/lib/cloak/redis.rb
ADDED
@@ -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
|
data/lib/cloak/utils.rb
ADDED
@@ -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
|
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: []
|