attr_vault 0.0.9 → 0.1.1
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/.travis.yml +12 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/LICENSE +1 -1
- data/README.md +215 -0
- data/attr_vault.gemspec +2 -2
- data/lib/attr_vault/cryptor.rb +0 -11
- data/lib/attr_vault/keyring.rb +8 -0
- data/lib/attr_vault/version.rb +1 -1
- data/lib/attr_vault.rb +22 -12
- data/spec/attr_vault_spec.rb +110 -81
- data/spec/spec_helper.rb +2 -2
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 357be9f5ce89b5473ebde27f6b12821149b72c69
|
4
|
+
data.tar.gz: 548d448526fb2c375bd77d88876c2b1317df45b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cf7d0c6455857d0c15b7aff5bff86884223e660708767faae36ccadb0d06cf098845123609cb65f9e71baf008542fdb4d50b1b2efc459efc9491a97ebfa7d0e
|
7
|
+
data.tar.gz: f3108dfbe9dd22231243a971f12c5162d785e959a26f668a6b904e32a5ea856fa1e87afc600a408efccc5dd9d978270cc22ab07fced09631547c72999740cfd9
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
attr_vault (0.
|
4
|
+
attr_vault (0.1.1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
diff-lcs (1.2.5)
|
10
|
-
pg (0.
|
10
|
+
pg (0.18.3)
|
11
11
|
rspec (3.1.0)
|
12
12
|
rspec-core (~> 3.1.0)
|
13
13
|
rspec-expectations (~> 3.1.0)
|
@@ -27,6 +27,6 @@ PLATFORMS
|
|
27
27
|
|
28
28
|
DEPENDENCIES
|
29
29
|
attr_vault!
|
30
|
-
pg (~> 0)
|
30
|
+
pg (~> 0.18.3)
|
31
31
|
rspec (~> 3.0)
|
32
32
|
sequel (~> 4.13)
|
data/LICENSE
CHANGED
data/README.md
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
[](https://travis-ci.org/uhoh-itsmaciek/attr_vault)
|
2
|
+
|
3
|
+
# AttrVault
|
4
|
+
|
5
|
+
Simple encryption-at-rest plugin for
|
6
|
+
[Sequel](https://github.com/jeremyevans/sequel.git).
|
7
|
+
|
8
|
+
|
9
|
+
N.B.: AttrVault is *not* for encrypting passwords--for that, you
|
10
|
+
should use something like
|
11
|
+
[bcrypt](https://github.com/codahale/bcrypt-ruby). It's meant for
|
12
|
+
encrypting sensitive data you will need to access in
|
13
|
+
plaintext. Passwords do not fall in that category.
|
14
|
+
|
15
|
+
|
16
|
+
### Philosophy
|
17
|
+
|
18
|
+
Sensitive data should be encrypted at rest. Data breaches are common,
|
19
|
+
and while preventing the breach in the first place is preferable,
|
20
|
+
defense in depth is a wise strategy.
|
21
|
+
|
22
|
+
AttrVault encrypts your data in-application, so your encryption keys
|
23
|
+
are never sent to the database. It includes an HMAC, so you can be
|
24
|
+
confident the data was not tampered with.
|
25
|
+
|
26
|
+
AttrVault is also designed with key rotation in mind. It relies on a
|
27
|
+
keyring of keys, always using the newest one for encryption, and
|
28
|
+
supporting decryption with any available key. It tracks which data was
|
29
|
+
encrypted with which key, making it easy to age out keys when needed
|
30
|
+
(e.g., on an employee departure).
|
31
|
+
|
32
|
+
|
33
|
+
### Keyring
|
34
|
+
|
35
|
+
Keys are managed through a keyring--a short JSON document containing
|
36
|
+
information about your encryption keys. The keyring must be a JSON
|
37
|
+
array of objects with the fields `id`, `created_at`, and `value`. A
|
38
|
+
keyring must have at least one key.
|
39
|
+
|
40
|
+
The `id` can be numeric or a uuid. The `created_at` must be an
|
41
|
+
ISO-8601 timestamp indicating the age of a key relative to the other
|
42
|
+
keys. The `value` is the actual bytes of the encryption key, used for
|
43
|
+
encryption and verification: see below.
|
44
|
+
|
45
|
+
|
46
|
+
### Encryption and verification
|
47
|
+
|
48
|
+
The encryption mechanism in AttrVault is borrowed from another
|
49
|
+
encryption library, [Fernet](https://github.com/fernet). The encrypted
|
50
|
+
payload format is slightly different: AttrVault drops the TTL (almost
|
51
|
+
never useful for data at rest) and the Base64 encoding (since most
|
52
|
+
modern databases can deal with binary data natively).
|
53
|
+
|
54
|
+
The key should be 32 bytes of random data, base64-encoded. A simple
|
55
|
+
way to generate that is:
|
56
|
+
|
57
|
+
```console
|
58
|
+
$ dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64
|
59
|
+
```
|
60
|
+
|
61
|
+
Include the result of this in the `value` section of the key
|
62
|
+
description in the keyring. Half this key is used for encryption, and
|
63
|
+
half for the HMAC.
|
64
|
+
|
65
|
+
|
66
|
+
### Usage
|
67
|
+
|
68
|
+
N.B.: AttrVault depends on the `Sequel::Model#before_save` hook. If
|
69
|
+
you use this in your model, be sure to call `super`!
|
70
|
+
|
71
|
+
First generate a key as above.
|
72
|
+
|
73
|
+
#### General schema changes
|
74
|
+
|
75
|
+
AttrVault needs some small changes to your database schema. It
|
76
|
+
requires a key identifier column for each model that uses encrypted
|
77
|
+
fields, and a binary data column for each field.
|
78
|
+
|
79
|
+
Here is a sample Sequel migration for adding encrypted fields to
|
80
|
+
Postgres, where binary data is stored in `bytea` columns:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
Sequel.migration do
|
84
|
+
change do
|
85
|
+
alter_table(:diary_entries) do
|
86
|
+
add_column :key_id, :uuid
|
87
|
+
add_column :secret_stuff, :bytea
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
|
94
|
+
#### Encrypted fields
|
95
|
+
|
96
|
+
AttrVault needs some configuration in models as well. A
|
97
|
+
`vault_keyring` attribute specifies a keyring in JSON (see the
|
98
|
+
expected format above). Then, for each field to be encrypted, include
|
99
|
+
a `vault_attr` attribute with its desired attribute name. You can
|
100
|
+
optionally specify the name of the encrypted column as well (by
|
101
|
+
default, it will be the field name suffixed with `_encrypted`):
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class DiaryEntry < Sequel::Model
|
105
|
+
vault_keyring ENV['ATTR_VAULT_KEYRING']
|
106
|
+
vault_attr :body, encrypted_field: :secret_stuff
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
AttrVault will generate getters and setters for any `vault_attr`s
|
111
|
+
specified.
|
112
|
+
|
113
|
+
|
114
|
+
#### Lookups
|
115
|
+
|
116
|
+
One tricky aspect of encryption is looking up records by known secret.
|
117
|
+
E.g.,
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
DiaryEntry.where(body: '@SwiftOnSecurity is dreamy')
|
121
|
+
```
|
122
|
+
|
123
|
+
is trivial with plaintext fields, but impossible with the model
|
124
|
+
defined as above.
|
125
|
+
|
126
|
+
AttrVault includes a way to mitigate this. Another small schema change:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
Sequel.migration do
|
130
|
+
change do
|
131
|
+
alter_table(:diary_entries) do
|
132
|
+
add_column :secret_digest, :bytea
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
Another small model definition change:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class DiaryEntry < Sequel::Model
|
142
|
+
vault_keyring ENV['ATTR_VAULT_KEYRING']
|
143
|
+
vault_attr :body, encrypted_field: :secret_stuff,
|
144
|
+
digest_field: :secret_digest
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
To be continued...
|
149
|
+
|
150
|
+
(storing digests is implemented, easy lookup by digest is not)
|
151
|
+
|
152
|
+
#### Migrating unencrypted data
|
153
|
+
|
154
|
+
If you have plaintext data that you'd like to start encrypting, doing
|
155
|
+
so in one shot can require a maintenance window if your data volume is
|
156
|
+
large enough. To avoid this, AttrVault supports online migration via
|
157
|
+
an "encrypt-on-write" mechanism: models will be read as normal, but
|
158
|
+
their fields will be encrypted whenever the models are saved. To
|
159
|
+
enable this behavior, just specify where the unencrypted data is
|
160
|
+
coming from:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class DiaryEntry < Sequel::Model
|
164
|
+
vault_keyring ENV['ATTR_VAULT_KEYRING']
|
165
|
+
vault_attr :body, encrypted_field: :secret_stuff,
|
166
|
+
migrate_from_field: :please_no_snooping
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
It's safe to use the same name as the name of the encrypted attribute.
|
171
|
+
|
172
|
+
|
173
|
+
#### Key rotation
|
174
|
+
|
175
|
+
Because AttrVault uses a keyring, with access to multiple keys at
|
176
|
+
once, key rotation is fairly straightforward: if you add a key to the
|
177
|
+
keyring with a more recent `created_at` than any other key, that key
|
178
|
+
will automatically be used for encryption. Any keys that are no longer
|
179
|
+
in use can be removed from the keyring.
|
180
|
+
|
181
|
+
To check if an existing key with id 123 is still in use, run:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
DiaryEntry.where(key_id: 123).empty?
|
185
|
+
```
|
186
|
+
|
187
|
+
If this is true, the key with that id can be safely removed.
|
188
|
+
|
189
|
+
For a large dataset, you may want to index the `key_id` column.
|
190
|
+
|
191
|
+
|
192
|
+
### Contributing
|
193
|
+
|
194
|
+
Patches are warmly welcome.
|
195
|
+
|
196
|
+
To run tests locally, you'll need a `DATABASE_URL` environment
|
197
|
+
variable pointing to a database AttrVault may use for testing. E.g.,
|
198
|
+
|
199
|
+
```console
|
200
|
+
$ createdb attr_vault_test
|
201
|
+
$ DATABASE_URL=postgres:///attr_vault_test bundle exec rspec
|
202
|
+
```
|
203
|
+
|
204
|
+
Please follow the project's general coding style and open issues for
|
205
|
+
any significant behavior or API changes.
|
206
|
+
|
207
|
+
A pull request is understood to mean you are offering your code to the
|
208
|
+
project under the MIT License.
|
209
|
+
|
210
|
+
|
211
|
+
### License
|
212
|
+
|
213
|
+
Copyright (c) 2014-2015 AttrVault Contributors
|
214
|
+
|
215
|
+
MIT License. See LICENSE for full text.
|
data/attr_vault.gemspec
CHANGED
@@ -5,7 +5,7 @@ Gem::Specification.new do |gem|
|
|
5
5
|
gem.email = ["m.sakrejda@gmail.com"]
|
6
6
|
gem.description = %q{Encryption at rest made easy}
|
7
7
|
gem.summary = %q{Sequel plugin for encryption at rest}
|
8
|
-
gem.homepage = "https://github.com/
|
8
|
+
gem.homepage = "https://github.com/uhoh-itsmaciek/attr_vault"
|
9
9
|
|
10
10
|
gem.files = `git ls-files`.split($\)
|
11
11
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
@@ -16,6 +16,6 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.license = "MIT"
|
17
17
|
|
18
18
|
gem.add_development_dependency "rspec", '~> 3.0'
|
19
|
-
gem.add_development_dependency "pg", '~> 0'
|
19
|
+
gem.add_development_dependency "pg", '~> 0.18.3'
|
20
20
|
gem.add_development_dependency "sequel", '~> 4.13'
|
21
21
|
end
|
data/lib/attr_vault/cryptor.rb
CHANGED
@@ -3,8 +3,6 @@ require 'base64'
|
|
3
3
|
module AttrVault
|
4
4
|
module Cryptor
|
5
5
|
|
6
|
-
PARANOID = true
|
7
|
-
|
8
6
|
def self.encrypt(value, key)
|
9
7
|
return value if value.nil? || value.empty?
|
10
8
|
|
@@ -14,15 +12,6 @@ module AttrVault
|
|
14
12
|
encrypted_payload = iv + encrypted_message
|
15
13
|
mac = Encryption.hmac_digest(secret.signing_key, encrypted_payload)
|
16
14
|
|
17
|
-
if PARANOID
|
18
|
-
mac_again = Encryption.hmac_digest(secret.signing_key, encrypted_payload)
|
19
|
-
unless verify_signature(mac, mac_again)
|
20
|
-
raise InvalidCiphertext, "Could not reliably calculate HMAC; " +
|
21
|
-
"got #{Base64.encode64(mac)} and #{Base64.encode64(mac_again)} " +
|
22
|
-
"for the same values"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
15
|
Sequel.blob(mac + encrypted_payload)
|
27
16
|
end
|
28
17
|
|
data/lib/attr_vault/keyring.rb
CHANGED
@@ -18,6 +18,10 @@ module AttrVault
|
|
18
18
|
@created_at = created_at
|
19
19
|
end
|
20
20
|
|
21
|
+
def digest(data)
|
22
|
+
AttrVault::Encryption::hmac_digest(value, data)
|
23
|
+
end
|
24
|
+
|
21
25
|
def to_json(*args)
|
22
26
|
{ id: id, value: value, created_at: created_at }.to_json
|
23
27
|
end
|
@@ -78,6 +82,10 @@ module AttrVault
|
|
78
82
|
k
|
79
83
|
end
|
80
84
|
|
85
|
+
def digests(data)
|
86
|
+
keys.map { |k| k.digest(data) }
|
87
|
+
end
|
88
|
+
|
81
89
|
def to_json
|
82
90
|
@keys.to_json
|
83
91
|
end
|
data/lib/attr_vault/version.rb
CHANGED
data/lib/attr_vault.rb
CHANGED
@@ -3,7 +3,6 @@ require 'attr_vault/keyring'
|
|
3
3
|
require 'attr_vault/secret'
|
4
4
|
require 'attr_vault/encryption'
|
5
5
|
require 'attr_vault/cryptor'
|
6
|
-
require 'digest/sha1'
|
7
6
|
|
8
7
|
module AttrVault
|
9
8
|
def self.included(base)
|
@@ -31,14 +30,14 @@ module AttrVault
|
|
31
30
|
@vault_dirty_attrs[attr.name] ||= self.send(attr.name)
|
32
31
|
end
|
33
32
|
end
|
34
|
-
# If any attr has
|
33
|
+
# If any attr has migrate_from_field and the plaintext field
|
35
34
|
# has a value set, flag the attr as dirty using the plaintext
|
36
35
|
# source value, then nil out the plaintext field. Skip any
|
37
36
|
# attributes that are already dirty.
|
38
|
-
self.class.vault_attrs.reject { |attr| attr.
|
39
|
-
unless self[attr.
|
40
|
-
@vault_dirty_attrs[attr.name] ||= self[attr.
|
41
|
-
self[attr.
|
37
|
+
self.class.vault_attrs.reject { |attr| attr.migrate_from_field.nil? }.each do |attr|
|
38
|
+
unless self[attr.migrate_from_field].nil?
|
39
|
+
@vault_dirty_attrs[attr.name] ||= self[attr.migrate_from_field]
|
40
|
+
self[attr.migrate_from_field] = nil
|
42
41
|
end
|
43
42
|
end
|
44
43
|
self.class.vault_attrs.each do |attr|
|
@@ -52,7 +51,8 @@ module AttrVault
|
|
52
51
|
if value.nil?
|
53
52
|
self[attr.digest_field] = nil
|
54
53
|
else
|
55
|
-
self[attr.digest_field] =
|
54
|
+
self[attr.digest_field] =
|
55
|
+
Sequel.blob(Encryption.hmac_digest(current_key.value, value))
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
@@ -68,6 +68,10 @@ module AttrVault
|
|
68
68
|
@keyring = Keyring.load(keyring_data)
|
69
69
|
end
|
70
70
|
|
71
|
+
def vault_digests(data)
|
72
|
+
@keyring.digests(data).map { |d| Sequel.blob(d) }
|
73
|
+
end
|
74
|
+
|
71
75
|
def vault_attr(name, opts={})
|
72
76
|
attr = VaultAttr.new(name, opts)
|
73
77
|
self.vault_attrs << attr
|
@@ -79,8 +83,8 @@ module AttrVault
|
|
79
83
|
end
|
80
84
|
# if there is a plaintext source field, use that and ignore
|
81
85
|
# the encrypted field
|
82
|
-
if !attr.
|
83
|
-
return self[attr.
|
86
|
+
if !attr.migrate_from_field.nil? && !self[attr.migrate_from_field].nil?
|
87
|
+
return self[attr.migrate_from_field]
|
84
88
|
end
|
85
89
|
|
86
90
|
keyring = self.class.vault_keys
|
@@ -124,15 +128,21 @@ module AttrVault
|
|
124
128
|
end
|
125
129
|
|
126
130
|
class VaultAttr
|
127
|
-
attr_reader :name, :encrypted_field, :
|
131
|
+
attr_reader :name, :encrypted_field, :migrate_from_field,
|
132
|
+
:migrate_from_kind, :digest_field
|
128
133
|
|
129
134
|
def initialize(name,
|
130
135
|
encrypted_field: "#{name}_encrypted",
|
131
|
-
|
136
|
+
migrate_from_field: nil,
|
137
|
+
migrate_from_kind: :plaintext,
|
132
138
|
digest_field: nil)
|
133
139
|
@name = name
|
134
140
|
@encrypted_field = encrypted_field.to_sym
|
135
|
-
@
|
141
|
+
@migrate_from_field = migrate_from_field.to_sym unless migrate_from_field.nil?
|
142
|
+
unless migrate_from_kind == :plaintext
|
143
|
+
raise ArgumentError, "Unknown migration kind: #{migrate_from_kind}"
|
144
|
+
end
|
145
|
+
@migrate_from_kind = migrate_from_kind
|
136
146
|
@digest_field = digest_field.to_sym unless digest_field.nil?
|
137
147
|
end
|
138
148
|
end
|
data/spec/attr_vault_spec.rb
CHANGED
@@ -4,23 +4,19 @@ require 'json'
|
|
4
4
|
describe AttrVault do
|
5
5
|
context "with a single encrypted column" do
|
6
6
|
let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
7
|
-
let(:key_data)
|
8
|
-
[{
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
let(:item) {
|
14
|
-
# the let form can't be evaluated inside the class definition
|
15
|
-
# because Ruby scoping rules were written by H.P. Lovecraft, so
|
16
|
-
# we create a local here to work around that
|
7
|
+
let(:key_data) do
|
8
|
+
[ { id: key_id,
|
9
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
10
|
+
created_at: Time.now } ].to_json
|
11
|
+
end
|
12
|
+
let(:item) do
|
17
13
|
k = key_data
|
18
14
|
Class.new(Sequel::Model(:items)) do
|
19
15
|
include AttrVault
|
20
16
|
vault_keyring k
|
21
17
|
vault_attr :secret
|
22
18
|
end
|
23
|
-
|
19
|
+
end
|
24
20
|
|
25
21
|
context "with a new object" do
|
26
22
|
it "does not affect other attributes" do
|
@@ -133,13 +129,12 @@ describe AttrVault do
|
|
133
129
|
end
|
134
130
|
|
135
131
|
context "with multiple encrypted columns" do
|
136
|
-
let(:key_data)
|
137
|
-
[{
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
let(:item) {
|
132
|
+
let(:key_data) do
|
133
|
+
[ { id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
|
134
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
135
|
+
created_at: Time.now } ].to_json
|
136
|
+
end
|
137
|
+
let(:item) do
|
143
138
|
k = key_data
|
144
139
|
Class.new(Sequel::Model(:items)) do
|
145
140
|
include AttrVault
|
@@ -147,7 +142,7 @@ describe AttrVault do
|
|
147
142
|
vault_attr :secret
|
148
143
|
vault_attr :other
|
149
144
|
end
|
150
|
-
|
145
|
+
end
|
151
146
|
|
152
147
|
it "does not clobber other attributes" do
|
153
148
|
secret1 = "superman is really mild-mannered reporter clark kent"
|
@@ -164,30 +159,20 @@ describe AttrVault do
|
|
164
159
|
|
165
160
|
context "with items encrypted with an older key" do
|
166
161
|
let(:key1_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
167
|
-
let(:key1)
|
168
|
-
{
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
}
|
173
|
-
}
|
174
|
-
|
162
|
+
let(:key1) do
|
163
|
+
{ id: key1_id,
|
164
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
165
|
+
created_at: Time.new(2014, 1, 1, 0, 0, 0) }
|
166
|
+
end
|
175
167
|
let(:key2_id) { '0a85781b-d8ac-4a4d-89b9-acee874e1ec2' }
|
176
|
-
let(:key2)
|
177
|
-
{
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
}
|
183
|
-
let(:
|
184
|
-
[key1].to_json
|
185
|
-
}
|
186
|
-
|
187
|
-
let(:full_keyring) {
|
188
|
-
[key1, key2].to_json
|
189
|
-
}
|
190
|
-
let(:item1) {
|
168
|
+
let(:key2) do
|
169
|
+
{ id: key2_id,
|
170
|
+
value: 'hUL1orBBRckZOuSuptRXYMV9lx5Qp54zwFUVwpwTpdk=',
|
171
|
+
created_at: Time.new(2014, 2, 1, 0, 0, 0) }
|
172
|
+
end
|
173
|
+
let(:partial_keyring) { [key1].to_json }
|
174
|
+
let(:full_keyring) { [key1, key2].to_json }
|
175
|
+
let(:item1) do
|
191
176
|
k = partial_keyring
|
192
177
|
Class.new(Sequel::Model(:items)) do
|
193
178
|
include AttrVault
|
@@ -195,8 +180,8 @@ describe AttrVault do
|
|
195
180
|
vault_attr :secret
|
196
181
|
vault_attr :other
|
197
182
|
end
|
198
|
-
|
199
|
-
let(:item2)
|
183
|
+
end
|
184
|
+
let(:item2) do
|
200
185
|
k = full_keyring
|
201
186
|
Class.new(Sequel::Model(:items)) do
|
202
187
|
include AttrVault
|
@@ -204,7 +189,7 @@ describe AttrVault do
|
|
204
189
|
vault_attr :secret
|
205
190
|
vault_attr :other
|
206
191
|
end
|
207
|
-
|
192
|
+
end
|
208
193
|
|
209
194
|
it "rewrites the items using the current key" do
|
210
195
|
secret1 = 'mrs. doubtfire is really a man'
|
@@ -246,13 +231,12 @@ describe AttrVault do
|
|
246
231
|
|
247
232
|
context "with plaintext source fields" do
|
248
233
|
let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
249
|
-
let(:key_data)
|
250
|
-
[{
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
let(:item1) {
|
234
|
+
let(:key_data) do
|
235
|
+
[ { id: key_id,
|
236
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
237
|
+
created_at: Time.now } ].to_json
|
238
|
+
end
|
239
|
+
let(:item1) do
|
256
240
|
k = key_data
|
257
241
|
Class.new(Sequel::Model(:items)) do
|
258
242
|
include AttrVault
|
@@ -260,16 +244,16 @@ describe AttrVault do
|
|
260
244
|
vault_attr :secret
|
261
245
|
vault_attr :other
|
262
246
|
end
|
263
|
-
|
264
|
-
let(:item2)
|
247
|
+
end
|
248
|
+
let(:item2) do
|
265
249
|
k = key_data
|
266
250
|
Class.new(Sequel::Model(:items)) do
|
267
251
|
include AttrVault
|
268
252
|
vault_keyring k
|
269
|
-
vault_attr :secret,
|
270
|
-
vault_attr :other,
|
253
|
+
vault_attr :secret, migrate_from_field: :not_secret
|
254
|
+
vault_attr :other, migrate_from_field: :other_not_secret
|
271
255
|
end
|
272
|
-
|
256
|
+
end
|
273
257
|
|
274
258
|
it "copies a plaintext field to an encrypted field when saving the object" do
|
275
259
|
becomes_secret = 'the location of the lost continent of atlantis'
|
@@ -312,12 +296,11 @@ describe AttrVault do
|
|
312
296
|
end
|
313
297
|
|
314
298
|
context "with renamed database fields" do
|
315
|
-
let(:key_data)
|
316
|
-
[{
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
}
|
299
|
+
let(:key_data) do
|
300
|
+
[ { id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
|
301
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
302
|
+
created_at: Time.now } ].to_json
|
303
|
+
end
|
321
304
|
|
322
305
|
it "supports renaming the encrypted field" do
|
323
306
|
k = key_data
|
@@ -355,43 +338,58 @@ describe AttrVault do
|
|
355
338
|
|
356
339
|
context "with a digest field" do
|
357
340
|
let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
358
|
-
let(:
|
359
|
-
[{
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
# the let form can't be evaluated inside the class definition
|
366
|
-
# because Ruby scoping rules were written by H.P. Lovecraft, so
|
367
|
-
# we create a local here to work around that
|
368
|
-
k = key_data
|
341
|
+
let(:key) do
|
342
|
+
[ { id: key_id,
|
343
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
344
|
+
created_at: Time.now } ]
|
345
|
+
end
|
346
|
+
let(:item) do
|
347
|
+
k = key.to_json
|
369
348
|
Class.new(Sequel::Model(:items)) do
|
370
349
|
include AttrVault
|
371
350
|
vault_keyring k
|
372
351
|
vault_attr :secret, digest_field: :secret_digest
|
373
352
|
vault_attr :other, digest_field: :other_digest
|
374
353
|
end
|
375
|
-
|
354
|
+
end
|
355
|
+
|
356
|
+
def test_digest(key, data)
|
357
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
|
358
|
+
key.first.fetch(:value), data)
|
359
|
+
end
|
376
360
|
|
377
|
-
|
361
|
+
def count_matching_digests(item_class, digest_field, secret)
|
362
|
+
item.where({digest_field => item_class.vault_digests(secret)}).count
|
363
|
+
end
|
364
|
+
|
365
|
+
it "records the hmac of the plaintext value" do
|
378
366
|
secret = 'snape kills dumbledore'
|
379
367
|
s = item.create(secret: secret)
|
380
|
-
expect(s.secret_digest).to eq(
|
368
|
+
expect(s.secret_digest).to eq(test_digest(key, secret))
|
369
|
+
expect(count_matching_digests(item, :secret_digest, secret)).to eq(1)
|
381
370
|
end
|
382
371
|
|
383
372
|
it "can record multiple digest fields" do
|
384
373
|
secret = 'joffrey kills ned'
|
385
374
|
other_secret = '"gomer pyle" lawrence kills himself'
|
386
375
|
s = item.create(secret: secret, other: other_secret)
|
387
|
-
expect(s.secret_digest).to eq(
|
388
|
-
expect(s.other_digest).to eq(
|
376
|
+
expect(s.secret_digest).to eq(test_digest(key, secret))
|
377
|
+
expect(s.other_digest).to eq(test_digest(key, other_secret))
|
378
|
+
|
379
|
+
# Check vault_digests feature matching against the database.
|
380
|
+
expect(count_matching_digests(item, :secret_digest, secret)).to eq(1)
|
381
|
+
expect(count_matching_digests(item, :other_digest, other_secret)).to eq(1)
|
382
|
+
|
383
|
+
# Negative tests for mismatched digesting.
|
384
|
+
expect(count_matching_digests(item, :secret_digest, other_secret))
|
385
|
+
.to eq(0)
|
386
|
+
expect(count_matching_digests(item, :other_digest, secret)).to eq(0)
|
389
387
|
end
|
390
388
|
|
391
389
|
it "records the digest for an empty field" do
|
392
390
|
s = item.create(secret: '', other: '')
|
393
|
-
expect(s.secret_digest).to eq(
|
394
|
-
expect(s.other_digest).to eq(
|
391
|
+
expect(s.secret_digest).to eq(test_digest(key, ''))
|
392
|
+
expect(s.other_digest).to eq(test_digest(key, ''))
|
395
393
|
end
|
396
394
|
|
397
395
|
it "records the digest of a nil field" do
|
@@ -401,3 +399,34 @@ describe AttrVault do
|
|
401
399
|
end
|
402
400
|
end
|
403
401
|
end
|
402
|
+
|
403
|
+
describe "stress test" do
|
404
|
+
let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
405
|
+
let(:key_data) do
|
406
|
+
[ { id: key_id,
|
407
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
408
|
+
created_at: Time.now } ].to_json
|
409
|
+
end
|
410
|
+
let(:item) do
|
411
|
+
k = key_data
|
412
|
+
Class.new(Sequel::Model(:items)) do
|
413
|
+
include AttrVault
|
414
|
+
vault_keyring k
|
415
|
+
vault_attr :secret
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
it "works" do
|
420
|
+
3.times.map do
|
421
|
+
Thread.new do
|
422
|
+
s = item.create(secret: 'that Commander Keen level in DOOM II')
|
423
|
+
1_000.times do
|
424
|
+
new_secret = [ nil, '', 36.times.map { (0..255).to_a.sample.chr }.join('') ].sample
|
425
|
+
s.update(secret: new_secret)
|
426
|
+
s.reload
|
427
|
+
expect(s.secret).to eq new_secret
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end.map(&:join)
|
431
|
+
end
|
432
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_vault
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maciek Sakrejda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.18.3
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.18.3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: sequel
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,9 +59,11 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
+
- ".travis.yml"
|
62
63
|
- Gemfile
|
63
64
|
- Gemfile.lock
|
64
65
|
- LICENSE
|
66
|
+
- README.md
|
65
67
|
- attr_vault.gemspec
|
66
68
|
- lib/attr_vault.rb
|
67
69
|
- lib/attr_vault/cryptor.rb
|
@@ -74,7 +76,7 @@ files:
|
|
74
76
|
- spec/attr_vault/secret_spec.rb
|
75
77
|
- spec/attr_vault_spec.rb
|
76
78
|
- spec/spec_helper.rb
|
77
|
-
homepage: https://github.com/
|
79
|
+
homepage: https://github.com/uhoh-itsmaciek/attr_vault
|
78
80
|
licenses:
|
79
81
|
- MIT
|
80
82
|
metadata: {}
|
@@ -94,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
96
|
version: '0'
|
95
97
|
requirements: []
|
96
98
|
rubyforge_project:
|
97
|
-
rubygems_version: 2.
|
99
|
+
rubygems_version: 2.4.5
|
98
100
|
signing_key:
|
99
101
|
specification_version: 4
|
100
102
|
summary: Sequel plugin for encryption at rest
|