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 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