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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 79953f549103ea6e7aacb9e6a16202602b97cd63
4
- data.tar.gz: f1ae2035a82fe167289b43756c43d9aa8bce0ce5
3
+ metadata.gz: 357be9f5ce89b5473ebde27f6b12821149b72c69
4
+ data.tar.gz: 548d448526fb2c375bd77d88876c2b1317df45b2
5
5
  SHA512:
6
- metadata.gz: bbfcd6f8aa8df8c3f082f2a6777c593fda49693978626e033c3848c4bdd996e41e5835d44c73b8116a7ecb8e208b4a106e117e517b0d19b9e0a2fe545766c791
7
- data.tar.gz: 7d8b9d5126597a491ab1a5f40299c2b8f4356026b4b27b150675bc0c7183bb4e39648c96b75a80d5c13971d2728ae5d9ae36a0cd1518e24c9f5a6c36f7b99d3d
6
+ metadata.gz: 1cf7d0c6455857d0c15b7aff5bff86884223e660708767faae36ccadb0d06cf098845123609cb65f9e71baf008542fdb4d50b1b2efc459efc9491a97ebfa7d0e
7
+ data.tar.gz: f3108dfbe9dd22231243a971f12c5162d785e959a26f668a6b904e32a5ea856fa1e87afc600a408efccc5dd9d978270cc22ab07fced09631547c72999740cfd9
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ cache: bundler
3
+ before_script: createdb attr_vault
4
+ sudo: false
5
+ script: DATABASE_URL="postgres:///attr_vault" bundle exec rspec
6
+ addons:
7
+ postgresql: "9.3"
8
+ env:
9
+ rvm:
10
+ - 2.2.2
11
+ notifications:
12
+ email: false
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- ruby '2.1.2'
3
+ ruby '2.2.2'
4
4
 
5
5
  # Specify your gem's dependencies in attr_vault.gemspec
6
6
  gemspec
data/Gemfile.lock CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- attr_vault (0.0.8)
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.17.1)
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Maciek Sakrejda
1
+ Copyright (c) 2014 AttrVault Contributors
2
2
 
3
3
  MIT License
4
4
 
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/deafbybeheading/attr_vault"
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
@@ -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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module AttrVault
2
- VERSION = "0.0.9"
2
+ VERSION = "0.1.1"
3
3
  end
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 plaintext_source_field and the plaintext field
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.plaintext_source_field.nil? }.each do |attr|
39
- unless self[attr.plaintext_source_field].nil?
40
- @vault_dirty_attrs[attr.name] ||= self[attr.plaintext_source_field]
41
- self[attr.plaintext_source_field] = nil
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] = Digest::SHA1.hexdigest(value)
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.plaintext_source_field.nil? && !self[attr.plaintext_source_field].nil?
83
- return self[attr.plaintext_source_field]
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, :plaintext_source_field, :digest_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
- plaintext_source_field: nil,
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
- @plaintext_source_field = plaintext_source_field.to_sym unless plaintext_source_field.nil?
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
@@ -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
- id: key_id,
10
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
11
- created_at: Time.now }].to_json
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
- id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
139
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
140
- created_at: Time.now }].to_json
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
- id: key1_id,
170
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
171
- created_at: Time.new(2014, 1, 1, 0, 0, 0)
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
- id: key2_id,
179
- value: 'hUL1orBBRckZOuSuptRXYMV9lx5Qp54zwFUVwpwTpdk=',
180
- created_at: Time.new(2014, 2, 1, 0, 0, 0)
181
- }
182
- }
183
- let(:partial_keyring) {
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
- id: key_id,
252
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
253
- created_at: Time.now }].to_json
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, plaintext_source_field: :not_secret
270
- vault_attr :other, plaintext_source_field: :other_not_secret
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
- id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
318
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
319
- created_at: Time.now }].to_json
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(:key_data) {
359
- [{
360
- id: key_id,
361
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
362
- created_at: Time.now }].to_json
363
- }
364
- let(:item) {
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
- it "records the sha1 of the plaintext value" do
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(Digest::SHA1.hexdigest(secret))
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(Digest::SHA1.hexdigest(secret))
388
- expect(s.other_digest).to eq(Digest::SHA1.hexdigest(other_secret))
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(Digest::SHA1.hexdigest(''))
394
- expect(s.other_digest).to eq(Digest::SHA1.hexdigest(''))
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
@@ -20,9 +20,9 @@ conn.run <<-EOF
20
20
  key_id uuid,
21
21
  alt_key_id uuid,
22
22
  secret_encrypted bytea,
23
- secret_digest text,
23
+ secret_digest bytea,
24
24
  other_encrypted bytea,
25
- other_digest text,
25
+ other_digest bytea,
26
26
  not_secret text,
27
27
  other_not_secret text
28
28
  )
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.0.9
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: 2014-12-22 00:00:00.000000000 Z
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: '0'
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: '0'
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/deafbybeheading/attr_vault
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.2.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