attr_vault 0.0.9 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/uhoh-itsmaciek/attr_vault.svg)](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
|