attr_vault 0.1.2 → 0.2.0

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: 86139c030cc0889d40ee3b40db69fe0b8b66beee
4
- data.tar.gz: cadcf1eabcf1dc445edc9d8d423f66ff6cb58242
3
+ metadata.gz: 679c27532f9ed523f12ffad0a4ac79b7d70b95a3
4
+ data.tar.gz: 6fb6564391b43997048851d9ab6a706c98b9ee27
5
5
  SHA512:
6
- metadata.gz: 4758d09746a415b03c7b2e187419501f78a3815b67a918e810c35b7cf6d90319f9fdcf32ce100bf8b48b25453a5831c11b8d98c446c80caef47b6cd5d9d07a14
7
- data.tar.gz: bfca3f4fc0f177d8e4244e6e778b7f4d3a4599b9aab9a0f68e225e8ba4bfd22388938d75869a49ef31cc724160340aafb563c8309b2018976c723f094515bc7d
6
+ metadata.gz: 4904567a0e64825c0396ffe7bfe4a6e5915b267ba083091d0e383cbc6226c07e9fa0952d24a231588da7ec1f9243cd2b0e01ec7b4ce73ae4bb490958f22ba2bd
7
+ data.tar.gz: 2979e267a1e776842472f9a8d86fb6125c549ef1fe08c9546b49869309f8afcdbcb62a8288e92ab443fbd10fc94921d547cbe6a5773c5d5eb737301766c71a8c
data/Gemfile.lock CHANGED
@@ -1,7 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- attr_vault (0.1.2)
4
+ attr_vault (0.2.0)
5
+ pg (~> 0.18.3)
6
+ sequel (~> 4.13)
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -32,16 +32,98 @@ encrypted with which key, making it easy to age out keys when needed
32
32
 
33
33
  ### Keyring
34
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.
35
+ Keys are managed through a keyring--a short JSON document describing
36
+ your encryption keys. The keyring must be a JSON object mapping
37
+ numeric ids of the keys to the key values. A keyring must have at
38
+ least one key. For example:
39
+
40
+ ```json
41
+ {
42
+ "1": "PV8+EHgJlHfsVVVstJHgEo+3OCSn4iJDzqJs55U650Q=",
43
+ "2": "0HyJ15am4haRsCyiFCxDdlKwl3G5yPNKTUbadpaIfPI="
44
+ }
45
+ ```
46
+
47
+ The `id` is used to track which key encrypted which piece of data; a
48
+ key with a larger id is assumed to be newer. The `value` is the actual
49
+ bytes of the encryption key, used for encryption and verification: see
50
+ below.
51
+
52
+ #### Legacy keyrings
53
+
54
+ A legacy keyring format is also supported for backwards
55
+ compatibility. The keyring must be a JSON array of objects with the
56
+ fields `id`, `created_at`, and `value`, and also must have at least
57
+ one key:
58
+
59
+ ```json
60
+ [
61
+ {
62
+ "id": "1380e471-038e-459a-801d-10e7988ee6a3",
63
+ "created_at": "2016-02-04 01:55:00+00",
64
+ "value": "PV8+EHgJlHfsVVVstJHgEo+3OCSn4iJDzqJs55U650Q="
65
+ }
66
+ ]
67
+ ```
68
+
69
+ The `id` must be a uuid. The `created_at` must be an ISO-8601
70
+ timestamp indicating the age of a key relative to the other keys. The
71
+ `value` is the same structure as for a normal keyring.
72
+
73
+ #### Legacy keyring migration
74
+
75
+ You can migrate from legacy keyrings to the new format via the
76
+ following process:
77
+
78
+ Add a new key_id column:
39
79
 
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.
80
+ ```ruby
81
+ Sequel.migration do
82
+ change do
83
+ alter_table(:diary_entries) do
84
+ add_column :new_key_id, :integer
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
90
+ Devise new numeric ids for all in-use keys (based on their
91
+ `created_at` dates), and link the ids with sql like the following:
92
+
93
+ ```sql
94
+ WITH key_map(new_key_id, old_key_id) AS (
95
+ VALUES (1, 'first-uuid'),
96
+ (2, 'next-uuid'),
97
+ (3, '...')
98
+ )
99
+ UPDATE
100
+ diary_entries
101
+ SET
102
+ diary_entries.new_key_id = key_map.new_key_id
103
+ FROM
104
+ key_map
105
+ WHERE
106
+ diary_entries.key_id = key_map.old_key_id
107
+ ```
108
+
109
+ Rename the new column to be used as the main key id and drop the old
110
+ id column:
111
+
112
+ ```ruby
113
+ Sequel.migration do
114
+ change do
115
+ alter_table(:diary_entries) do
116
+ rename_column :key_id, :old_key_id
117
+ rename_column :new_key_id, :key_id
118
+ set_column_not_null :key_id
119
+ drop_column :old_key_id, :integer
120
+ end
121
+ end
122
+ end
123
+ ```
44
124
 
125
+ Then change the keyring in your application to use the new numeric
126
+ ids.
45
127
 
46
128
  ### Encryption and verification
47
129
 
@@ -83,7 +165,7 @@ Postgres, where binary data is stored in `bytea` columns:
83
165
  Sequel.migration do
84
166
  change do
85
167
  alter_table(:diary_entries) do
86
- add_column :key_id, :uuid
168
+ add_column :key_id, :integer
87
169
  add_column :secret_stuff, :bytea
88
170
  end
89
171
  end
@@ -174,9 +256,10 @@ It's safe to use the same name as the name of the encrypted attribute.
174
256
 
175
257
  Because AttrVault uses a keyring, with access to multiple keys at
176
258
  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.
259
+ keyring with a higher id than any other key (or more recent
260
+ `created_at` for the legacy keyring format), that key will
261
+ automatically be used for encryption. Any keys that are no longer in
262
+ use can be removed from the keyring.
180
263
 
181
264
  To check if an existing key with id 123 is still in use, run:
182
265
 
data/attr_vault.gemspec CHANGED
@@ -15,7 +15,8 @@ Gem::Specification.new do |gem|
15
15
  gem.version = AttrVault::VERSION
16
16
  gem.license = "MIT"
17
17
 
18
+ gem.add_runtime_dependency "pg", '~> 0.18.3'
19
+ gem.add_runtime_dependency "sequel", '~> 4.13'
20
+
18
21
  gem.add_development_dependency "rspec", '~> 3.0'
19
- gem.add_development_dependency "pg", '~> 0.18.3'
20
- gem.add_development_dependency "sequel", '~> 4.13'
21
22
  end
@@ -2,15 +2,19 @@ module AttrVault
2
2
  class Key
3
3
  attr_reader :id, :value, :created_at
4
4
 
5
- def initialize(id, value, created_at)
5
+ def initialize(id, value, created_at=nil)
6
6
  if id.nil?
7
7
  raise InvalidKey, "key id required"
8
8
  end
9
9
  if value.nil?
10
10
  raise InvalidKey, "key value required"
11
11
  end
12
- if created_at.nil?
13
- raise InvalidKey, "key created_at required"
12
+ begin
13
+ id = Integer(id)
14
+ rescue
15
+ if created_at.nil?
16
+ raise InvalidKey, "key created_at required"
17
+ end
14
18
  end
15
19
 
16
20
  @id = id
@@ -33,15 +37,20 @@ module AttrVault
33
37
  def self.load(keyring_data)
34
38
  keyring = Keyring.new
35
39
  begin
36
- candidate_keys = JSON.parse(keyring_data)
37
- unless candidate_keys.respond_to? :each
38
- raise InvalidKeyring, "does not respond to each"
39
- end
40
- candidate_keys.each_with_index do |k|
41
- created_at = unless k["created_at"].nil?
42
- Time.parse(k["created_at"])
43
- end
44
- keyring.add_key(Key.new(k["id"], k["value"], created_at || Time.now))
40
+ candidate_keys = JSON.parse(keyring_data, symbolize_names: true)
41
+
42
+ case candidate_keys
43
+ when Array
44
+ candidate_keys.each do |k|
45
+ created_at = Time.parse(k[:created_at]) if k.has_key?(:created_at)
46
+ keyring.add_key(Key.new(k[:id], k[:value], created_at || Time.now))
47
+ end
48
+ when Hash
49
+ candidate_keys.each do |key_id, key|
50
+ keyring.add_key(Key.new(key_id.to_s, key))
51
+ end
52
+ else
53
+ raise InvalidKeyring, "Invalid JSON structure"
45
54
  end
46
55
  rescue StandardError => e
47
56
  raise InvalidKeyring, e.message
@@ -87,7 +96,14 @@ module AttrVault
87
96
  end
88
97
 
89
98
  def to_json
90
- @keys.to_json
99
+ if @keys.all? { |k| k.created_at.nil? }
100
+ @keys.each_with_object({}) do |k,obj|
101
+ obj[k.id] = k.value
102
+ end.to_json
103
+ else
104
+ # Assume we are dealing with a legacy keyring
105
+ @keys.to_json
106
+ end
91
107
  end
92
108
  end
93
109
  end
@@ -1,3 +1,3 @@
1
1
  module AttrVault
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -2,447 +2,489 @@ require 'spec_helper'
2
2
  require 'json'
3
3
 
4
4
  describe AttrVault do
5
- context "with a single encrypted column" do
6
- let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
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
13
- k = key_data
14
- Class.new(Sequel::Model(:items)) do
15
- include AttrVault
16
- vault_keyring k
17
- vault_attr :secret
18
- end
19
- end
20
-
21
- context "with a new object" do
22
- it "does not affect other attributes" do
23
- not_secret = 'jimi hendrix was rather talented'
24
- s = item.create(not_secret: not_secret)
25
- s.reload
26
- expect(s.not_secret).to eq(not_secret)
27
- expect(s.this.where(not_secret: not_secret).count).to eq 1
5
+ [ [ 'numeric', :items, [ 1, 2 ] ],
6
+ [ 'uuid', :items_legacy,
7
+ [ 'bf72f2ca-b478-4abf-a077-8eb7e071810f',
8
+ '5a8d7477-6604-4801-9f1d-d4f412890cc3' ] ] ].each do |desc, table, key_ids|
9
+
10
+ describe "with #{desc} key ids" do
11
+ let(:key_values) do
12
+ [ 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
13
+ 'hUL1orBBRckZOuSuptRXYMV9lx5Qp54zwFUVwpwTpdk=' ]
28
14
  end
29
15
 
30
- it "encrypts non-empty values" do
31
- secret = 'lady gaga? also rather talented'
32
- s = item.create(secret: secret)
33
- s.reload
34
- expect(s.secret).to eq(secret)
35
- s.columns.each do |col|
36
- expect(s.this.where(Sequel.cast(Sequel.cast(col, :text), :bytea) => secret).count).to eq 0
16
+ def make_keyring(key_ids)
17
+ paired = key_ids.zip(key_values)
18
+ if key_ids.all? { |id| id.is_a? Integer }
19
+ Hash[paired]
20
+ else
21
+ result = []
22
+ paired.each_with_index do |(id,val), i|
23
+ result << { id: id, created_at: Time.now + (i * 60), value: val }
24
+ end
25
+ result
37
26
  end
38
27
  end
39
28
 
40
- it "stores empty values as empty" do
41
- secret = ''
42
- s = item.create(secret: secret)
43
- s.reload
44
- expect(s.secret).to eq('')
45
- expect(s.secret_encrypted).to eq('')
46
- end
47
-
48
- it "stores nil values as nil" do
49
- s = item.create(secret: nil)
50
- s.reload
51
- expect(s.secret).to be_nil
52
- expect(s.secret_encrypted).to be_nil
53
- end
29
+ let(:key1_id) { key_ids.first }
30
+ let(:key2_id) { key_ids.fetch(1) }
31
+ let(:key_id) { key1_id }
54
32
 
55
- it "sets fields to empty that were previously not empty" do
56
- s = item.create(secret: 'joyce hatto')
57
- s.reload
58
- s.update(secret: '')
59
- s.reload
60
- expect(s.secret).to eq ''
61
- expect(s.secret_encrypted).not_to be_nil
33
+ let(:old_keyring) do
34
+ make_keyring(key_ids.take(1))
62
35
  end
63
-
64
- it "avoids updating existing values when those do not change" do
65
- the_secret = "I'm not saying it was aliens..."
66
- s = item.create(secret: the_secret)
67
- s.reload
68
- s.secret = the_secret
69
- expect(s.save_changes).to be_nil
70
- end
71
-
72
- it "stores the key id" do
73
- secret = 'it was professor plum with the wrench in the library'
74
- s = item.create(secret: secret)
75
- s.reload
76
- expect(s.key_id).to eq(key_id)
77
- end
78
- end
79
-
80
- context "with an existing object" do
81
- it "does not affect other attributes" do
82
- not_secret = 'soylent is not especially tasty'
83
- s = item.create
84
- s.update(not_secret: not_secret)
85
- s.reload
86
- expect(s.not_secret).to eq(not_secret)
87
- expect(s.this.where(not_secret: not_secret).count).to eq 1
36
+ let(:new_keyring) do
37
+ make_keyring(key_ids)
88
38
  end
39
+ let(:keyring) { old_keyring }
89
40
 
90
- it "encrypts non-empty values" do
91
- secret = 'soylent green is made of people'
92
- s = item.create
93
- s.update(secret: secret)
94
- s.reload
95
- expect(s.secret).to eq(secret)
96
- s.columns.each do |col|
97
- expect(s.this.where(Sequel.cast(Sequel.cast(col, :text), :bytea) => secret).count).to eq 0
41
+ let(:key1) do
42
+ if keyring.is_a?(Hash)
43
+ keyring[key1_id]
44
+ else
45
+ keyring.find { |k| k[:id] == key1_id }
98
46
  end
99
47
  end
100
-
101
- it "stores empty values as empty" do
102
- s = item.create(secret: "darth vader is luke's father")
103
- s.update(secret: '')
104
- s.reload
105
- expect(s.secret).to eq('')
106
- expect(s.secret_encrypted).to eq('')
107
- end
108
-
109
- it "leaves nil values as nil" do
110
- s = item.create(secret: "dr. crowe was dead all along")
111
- s.update(secret: nil)
112
- s.reload
113
- expect(s.secret).to be_nil
114
- expect(s.secret_encrypted).to be_nil
115
- end
116
-
117
- it "stores the key id" do
118
- secret = 'animal style'
119
- s = item.create
120
- s.update(secret: secret)
121
- s.reload
122
- expect(s.key_id).to eq(key_id)
123
- end
124
-
125
- it "reads a never-set encrypted field as nil" do
126
- s = item.create
127
- expect(s.secret).to be_nil
128
- end
129
-
130
- it "avoids updating existing values when those do not change" do
131
- the_secret = "Satoshi Nakamoto"
132
- s = item.create
133
- s.update(secret: the_secret)
134
- s.secret = the_secret
135
- expect(s.save_changes).to be_nil
48
+ let(:key2) do
49
+ if keyring.is_a?(Hash)
50
+ keyring[key2_id]
51
+ else
52
+ keyring.find { |k| k[:id] == key2_id }
53
+ end
136
54
  end
55
+ let(:key) { key1 }
56
+
57
+ context "with a single encrypted column" do
58
+ let(:item) do
59
+ k = keyring.to_json
60
+ t = table
61
+ Class.new(Sequel::Model(t)) do
62
+ include AttrVault
63
+ vault_keyring k
64
+ vault_attr :secret
65
+ end
66
+ end
137
67
 
138
- it "reads the correct value for a dirty field before the object is saved" do
139
- s = item.create
140
- secret = 'mcmurphy is lobotomized =('
141
- s.secret = secret
142
- expect(s.secret).to eq secret
143
- end
144
- end
145
- end
68
+ context "with a new object" do
69
+ it "does not affect other attributes" do
70
+ not_secret = 'jimi hendrix was rather talented'
71
+ s = item.create(not_secret: not_secret)
72
+ s.reload
73
+ expect(s.not_secret).to eq(not_secret)
74
+ expect(s.this.where(not_secret: not_secret).count).to eq 1
75
+ end
76
+
77
+ it "encrypts non-empty values" do
78
+ secret = 'lady gaga? also rather talented'
79
+ s = item.create(secret: secret)
80
+ s.reload
81
+ expect(s.secret).to eq(secret)
82
+ s.columns.each do |col|
83
+ expect(s.this.where(Sequel.cast(Sequel.cast(col, :text), :bytea) => secret).count).to eq 0
84
+ end
85
+ end
86
+
87
+ it "stores empty values as empty" do
88
+ secret = ''
89
+ s = item.create(secret: secret)
90
+ s.reload
91
+ expect(s.secret).to eq('')
92
+ expect(s.secret_encrypted).to eq('')
93
+ end
94
+
95
+ it "stores nil values as nil" do
96
+ s = item.create(secret: nil)
97
+ s.reload
98
+ expect(s.secret).to be_nil
99
+ expect(s.secret_encrypted).to be_nil
100
+ end
101
+
102
+ it "sets fields to empty that were previously not empty" do
103
+ s = item.create(secret: 'joyce hatto')
104
+ s.reload
105
+ s.update(secret: '')
106
+ s.reload
107
+ expect(s.secret).to eq ''
108
+ expect(s.secret_encrypted).not_to be_nil
109
+ end
110
+
111
+ it "avoids updating existing values when those do not change" do
112
+ the_secret = "I'm not saying it was aliens..."
113
+ s = item.create(secret: the_secret)
114
+ s.reload
115
+ s.secret = the_secret
116
+ expect(s.save_changes).to be_nil
117
+ end
118
+
119
+ it "stores the key id" do
120
+ secret = 'it was professor plum with the wrench in the library'
121
+ s = item.create(secret: secret)
122
+ s.reload
123
+ expect(s.key_id).to eq(key_id)
124
+ end
125
+ end
146
126
 
147
- context "with multiple encrypted columns" do
148
- let(:key_data) do
149
- [ { id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
150
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
151
- created_at: Time.now } ].to_json
152
- end
153
- let(:item) do
154
- k = key_data
155
- Class.new(Sequel::Model(:items)) do
156
- include AttrVault
157
- vault_keyring k
158
- vault_attr :secret
159
- vault_attr :other
127
+ context "with an existing object" do
128
+ it "does not affect other attributes" do
129
+ not_secret = 'soylent is not especially tasty'
130
+ s = item.create
131
+ s.update(not_secret: not_secret)
132
+ s.reload
133
+ expect(s.not_secret).to eq(not_secret)
134
+ expect(s.this.where(not_secret: not_secret).count).to eq 1
135
+ end
136
+
137
+ it "encrypts non-empty values" do
138
+ secret = 'soylent green is made of people'
139
+ s = item.create
140
+ s.update(secret: secret)
141
+ s.reload
142
+ expect(s.secret).to eq(secret)
143
+ s.columns.each do |col|
144
+ expect(s.this.where(Sequel.cast(Sequel.cast(col, :text), :bytea) => secret).count).to eq 0
145
+ end
146
+ end
147
+
148
+ it "stores empty values as empty" do
149
+ s = item.create(secret: "darth vader is luke's father")
150
+ s.update(secret: '')
151
+ s.reload
152
+ expect(s.secret).to eq('')
153
+ expect(s.secret_encrypted).to eq('')
154
+ end
155
+
156
+ it "leaves nil values as nil" do
157
+ s = item.create(secret: "dr. crowe was dead all along")
158
+ s.update(secret: nil)
159
+ s.reload
160
+ expect(s.secret).to be_nil
161
+ expect(s.secret_encrypted).to be_nil
162
+ end
163
+
164
+ it "stores the key id" do
165
+ secret = 'animal style'
166
+ s = item.create
167
+ s.update(secret: secret)
168
+ s.reload
169
+ expect(s.key_id).to eq(key_id)
170
+ end
171
+
172
+ it "reads a never-set encrypted field as nil" do
173
+ s = item.create
174
+ expect(s.secret).to be_nil
175
+ end
176
+
177
+ it "avoids updating existing values when those do not change" do
178
+ the_secret = "Satoshi Nakamoto"
179
+ s = item.create
180
+ s.update(secret: the_secret)
181
+ s.secret = the_secret
182
+ expect(s.save_changes).to be_nil
183
+ end
184
+
185
+ it "reads the correct value for a dirty field before the object is saved" do
186
+ s = item.create
187
+ secret = 'mcmurphy is lobotomized =('
188
+ s.secret = secret
189
+ expect(s.secret).to eq secret
190
+ end
191
+ end
160
192
  end
161
- end
162
193
 
163
- it "does not clobber other attributes" do
164
- secret1 = "superman is really mild-mannered reporter clark kent"
165
- secret2 = "batman is really millionaire playboy bruce wayne"
166
- s = item.create(secret: secret1)
167
- s.reload
168
- expect(s.secret).to eq secret1
169
- s.update(other: secret2)
170
- s.reload
171
- expect(s.secret).to eq secret1
172
- expect(s.other).to eq secret2
173
- end
174
- end
194
+ context "with multiple encrypted columns" do
195
+ let(:key_data) do
196
+ [ { id: 1,
197
+ value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=' } ].to_json
198
+ end
199
+ let(:item) do
200
+ k = key_data
201
+ Class.new(Sequel::Model(:items)) do
202
+ include AttrVault
203
+ vault_keyring k
204
+ vault_attr :secret
205
+ vault_attr :other
206
+ end
207
+ end
175
208
 
176
- context "with items encrypted with an older key" do
177
- let(:key1_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
178
- let(:key1) do
179
- { id: key1_id,
180
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
181
- created_at: Time.new(2014, 1, 1, 0, 0, 0) }
182
- end
183
- let(:key2_id) { '0a85781b-d8ac-4a4d-89b9-acee874e1ec2' }
184
- let(:key2) do
185
- { id: key2_id,
186
- value: 'hUL1orBBRckZOuSuptRXYMV9lx5Qp54zwFUVwpwTpdk=',
187
- created_at: Time.new(2014, 2, 1, 0, 0, 0) }
188
- end
189
- let(:partial_keyring) { [key1].to_json }
190
- let(:full_keyring) { [key1, key2].to_json }
191
- let(:item1) do
192
- k = partial_keyring
193
- Class.new(Sequel::Model(:items)) do
194
- include AttrVault
195
- vault_keyring k
196
- vault_attr :secret
197
- vault_attr :other
198
- end
199
- end
200
- let(:item2) do
201
- k = full_keyring
202
- Class.new(Sequel::Model(:items)) do
203
- include AttrVault
204
- vault_keyring k
205
- vault_attr :secret
206
- vault_attr :other
209
+ it "does not clobber other attributes" do
210
+ secret1 = "superman is really mild-mannered reporter clark kent"
211
+ secret2 = "batman is really millionaire playboy bruce wayne"
212
+ s = item.create(secret: secret1)
213
+ s.reload
214
+ expect(s.secret).to eq secret1
215
+ s.update(other: secret2)
216
+ s.reload
217
+ expect(s.secret).to eq secret1
218
+ expect(s.other).to eq secret2
219
+ end
207
220
  end
208
- end
209
221
 
210
- it "rewrites the items using the current key" do
211
- secret1 = 'mrs. doubtfire is really a man'
212
- secret2 = 'tootsie? also a man'
213
- record = item1.create(secret: secret1)
214
- expect(record.key_id).to eq key1_id
215
- expect(record.secret).to eq secret1
222
+ context "with items encrypted with an older key" do
223
+ let(:key1_id) { 1 }
224
+ let(:key1) do
225
+ { id: key1_id,
226
+ value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=' }
227
+ end
228
+ let(:key2_id) { 2 }
229
+ let(:key2) do
230
+ { id: key2_id,
231
+ value: 'hUL1orBBRckZOuSuptRXYMV9lx5Qp54zwFUVwpwTpdk=' }
232
+ end
233
+ let(:partial_keyring) { [key1].to_json }
234
+ let(:full_keyring) { [key1, key2].to_json }
235
+ let(:item1) do
236
+ k = partial_keyring
237
+ Class.new(Sequel::Model(:items)) do
238
+ include AttrVault
239
+ vault_keyring k
240
+ vault_attr :secret
241
+ vault_attr :other
242
+ end
243
+ end
244
+ let(:item2) do
245
+ k = full_keyring
246
+ Class.new(Sequel::Model(:items)) do
247
+ include AttrVault
248
+ vault_keyring k
249
+ vault_attr :secret
250
+ vault_attr :other
251
+ end
252
+ end
216
253
 
217
- old_secret_encrypted = record.secret_encrypted
254
+ it "rewrites the items using the current key" do
255
+ secret1 = 'mrs. doubtfire is really a man'
256
+ secret2 = 'tootsie? also a man'
257
+ record = item1.create(secret: secret1)
258
+ expect(record.key_id).to eq key1_id
259
+ expect(record.secret).to eq secret1
218
260
 
219
- new_key_record = item2[record.id]
220
- new_key_record.update(secret: secret2)
221
- new_key_record.reload
261
+ old_secret_encrypted = record.secret_encrypted
222
262
 
223
- expect(new_key_record.key_id).to eq key2_id
224
- expect(new_key_record.secret).to eq secret2
225
- expect(new_key_record.secret_encrypted).not_to eq old_secret_encrypted
226
- end
263
+ new_key_record = item2[record.id]
264
+ new_key_record.update(secret: secret2)
265
+ new_key_record.reload
227
266
 
228
- it "rewrites the items using the current key even if they are not updated" do
229
- secret1 = 'the planet of the apes is really earth'
230
- secret2 = 'the answer is 42'
231
- record = item1.create(secret: secret1)
232
- expect(record.key_id).to eq key1_id
233
- expect(record.secret).to eq secret1
267
+ expect(new_key_record.key_id).to eq key2_id
268
+ expect(new_key_record.secret).to eq secret2
269
+ expect(new_key_record.secret_encrypted).not_to eq old_secret_encrypted
270
+ end
234
271
 
235
- old_secret_encrypted = record.secret_encrypted
272
+ it "rewrites the items using the current key even if they are not updated" do
273
+ secret1 = 'the planet of the apes is really earth'
274
+ secret2 = 'the answer is 42'
275
+ record = item1.create(secret: secret1)
276
+ expect(record.key_id).to eq key1_id
277
+ expect(record.secret).to eq secret1
236
278
 
237
- new_key_record = item2[record.id]
238
- new_key_record.update(other: secret2)
239
- new_key_record.reload
279
+ old_secret_encrypted = record.secret_encrypted
240
280
 
241
- expect(new_key_record.key_id).to eq key2_id
242
- expect(new_key_record.secret).to eq secret1
243
- expect(new_key_record.secret_encrypted).not_to eq old_secret_encrypted
244
- expect(new_key_record.other).to eq secret2
245
- end
246
- end
281
+ new_key_record = item2[record.id]
282
+ new_key_record.update(other: secret2)
283
+ new_key_record.reload
247
284
 
248
- context "with plaintext source fields" do
249
- let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
250
- let(:key_data) do
251
- [ { id: key_id,
252
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
253
- created_at: Time.now } ].to_json
254
- end
255
- let(:item1) do
256
- k = key_data
257
- Class.new(Sequel::Model(:items)) do
258
- include AttrVault
259
- vault_keyring k
260
- vault_attr :secret
261
- vault_attr :other
262
- end
263
- end
264
- let(:item2) do
265
- k = key_data
266
- Class.new(Sequel::Model(:items)) do
267
- include AttrVault
268
- vault_keyring k
269
- vault_attr :secret, migrate_from_field: :not_secret
270
- vault_attr :other, migrate_from_field: :other_not_secret
285
+ expect(new_key_record.key_id).to eq key2_id
286
+ expect(new_key_record.secret).to eq secret1
287
+ expect(new_key_record.secret_encrypted).not_to eq old_secret_encrypted
288
+ expect(new_key_record.other).to eq secret2
289
+ end
271
290
  end
272
- end
273
291
 
274
- it "copies a plaintext field to an encrypted field when saving the object" do
275
- becomes_secret = 'the location of the lost continent of atlantis'
276
- s = item1.create(not_secret: becomes_secret)
277
- reloaded = item2[s.id]
278
- expect(reloaded.not_secret).to eq becomes_secret
279
- reloaded.save
280
- reloaded.reload
281
- expect(reloaded.not_secret).to be_nil
282
- expect(reloaded.secret).to eq becomes_secret
283
- end
284
-
285
- it "supports converting multiple fields" do
286
- becomes_secret1 = 'the location of the fountain of youth'
287
- becomes_secret2 = 'the location of the lost city of el dorado'
288
- s = item1.create(not_secret: becomes_secret1, other_not_secret: becomes_secret2)
289
- reloaded = item2[s.id]
290
- expect(reloaded.not_secret).to eq becomes_secret1
291
- expect(reloaded.other_not_secret).to eq becomes_secret2
292
- reloaded.save
293
- reloaded.reload
294
- expect(reloaded.not_secret).to be_nil
295
- expect(reloaded.secret).to eq becomes_secret1
296
- expect(reloaded.other_not_secret).to be_nil
297
- expect(reloaded.other).to eq becomes_secret2
298
- end
292
+ context "with plaintext source fields" do
293
+ let(:key_id) { 1 }
294
+ let(:key_data) do
295
+ [ { id: key_id,
296
+ value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=' } ].to_json
297
+ end
298
+ let(:item1) do
299
+ k = key_data
300
+ Class.new(Sequel::Model(:items)) do
301
+ include AttrVault
302
+ vault_keyring k
303
+ vault_attr :secret
304
+ vault_attr :other
305
+ end
306
+ end
307
+ let(:item2) do
308
+ k = key_data
309
+ Class.new(Sequel::Model(:items)) do
310
+ include AttrVault
311
+ vault_keyring k
312
+ vault_attr :secret, migrate_from_field: :not_secret
313
+ vault_attr :other, migrate_from_field: :other_not_secret
314
+ end
315
+ end
299
316
 
300
- it "nils out the plaintext field and persists the encrypted field on save" do
301
- becomes_secret = 'the location of all those socks that disappear from the dryer'
302
- new_secret = 'the location of pliny the younger drafts'
303
- s = item1.create(not_secret: becomes_secret)
304
- reloaded = item2[s.id]
305
- expect(reloaded.secret).to eq(becomes_secret)
306
- reloaded.secret = new_secret
307
- expect(reloaded.secret).to eq(new_secret)
308
- reloaded.save
309
- expect(reloaded.secret).to eq(new_secret)
310
- expect(reloaded.not_secret).to be_nil
311
- end
312
- end
317
+ it "copies a plaintext field to an encrypted field when saving the object" do
318
+ becomes_secret = 'the location of the lost continent of atlantis'
319
+ s = item1.create(not_secret: becomes_secret)
320
+ reloaded = item2[s.id]
321
+ expect(reloaded.not_secret).to eq becomes_secret
322
+ reloaded.save
323
+ reloaded.reload
324
+ expect(reloaded.not_secret).to be_nil
325
+ expect(reloaded.secret).to eq becomes_secret
326
+ end
313
327
 
314
- context "with renamed database fields" do
315
- let(:key_data) do
316
- [ { id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
317
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
318
- created_at: Time.now } ].to_json
319
- end
328
+ it "supports converting multiple fields" do
329
+ becomes_secret1 = 'the location of the fountain of youth'
330
+ becomes_secret2 = 'the location of the lost city of el dorado'
331
+ s = item1.create(not_secret: becomes_secret1, other_not_secret: becomes_secret2)
332
+ reloaded = item2[s.id]
333
+ expect(reloaded.not_secret).to eq becomes_secret1
334
+ expect(reloaded.other_not_secret).to eq becomes_secret2
335
+ reloaded.save
336
+ reloaded.reload
337
+ expect(reloaded.not_secret).to be_nil
338
+ expect(reloaded.secret).to eq becomes_secret1
339
+ expect(reloaded.other_not_secret).to be_nil
340
+ expect(reloaded.other).to eq becomes_secret2
341
+ end
320
342
 
321
- it "supports renaming the encrypted field" do
322
- k = key_data
323
- item = Class.new(Sequel::Model(:items)) do
324
- include AttrVault
325
- vault_keyring k
326
- vault_attr :classified_info,
327
- encrypted_field: :secret_encrypted
343
+ it "nils out the plaintext field and persists the encrypted field on save" do
344
+ becomes_secret = 'the location of all those socks that disappear from the dryer'
345
+ new_secret = 'the location of pliny the younger drafts'
346
+ s = item1.create(not_secret: becomes_secret)
347
+ reloaded = item2[s.id]
348
+ expect(reloaded.secret).to eq(becomes_secret)
349
+ reloaded.secret = new_secret
350
+ expect(reloaded.secret).to eq(new_secret)
351
+ reloaded.save
352
+ expect(reloaded.secret).to eq(new_secret)
353
+ expect(reloaded.not_secret).to be_nil
354
+ end
328
355
  end
329
356
 
330
- secret = "we've secretly replaced the fine coffee they usually serve with Folgers Crystals"
331
- s = item.create(classified_info: secret)
332
- s.reload
333
- expect(s.classified_info).to eq secret
334
- expect(s.secret_encrypted).not_to eq secret
335
- end
357
+ context "with renamed database fields" do
358
+ let(:key_data) do
359
+ [ { id: 1,
360
+ value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=' } ].to_json
361
+ end
336
362
 
337
- it "supports renaming the key id field" do
338
- k = key_data
339
- item = Class.new(Sequel::Model(:items)) do
340
- include AttrVault
341
- vault_keyring k, key_field: :alt_key_id
342
- vault_attr :secret
343
- end
363
+ it "supports renaming the encrypted field" do
364
+ k = key_data
365
+ item = Class.new(Sequel::Model(:items)) do
366
+ include AttrVault
367
+ vault_keyring k
368
+ vault_attr :classified_info,
369
+ encrypted_field: :secret_encrypted
370
+ end
371
+
372
+ secret = "we've secretly replaced the fine coffee they usually serve with Folgers Crystals"
373
+ s = item.create(classified_info: secret)
374
+ s.reload
375
+ expect(s.classified_info).to eq secret
376
+ expect(s.secret_encrypted).not_to eq secret
377
+ end
344
378
 
345
- secret = "up up down down left right left right b a"
346
- s = item.create(secret: secret)
347
- s.reload
348
- expect(s.secret).to eq secret
349
- expect(s.secret_encrypted).not_to eq secret
350
- expect(s.alt_key_id).not_to be_nil
351
- expect(s.key_id).to be_nil
352
- end
353
- end
379
+ it "supports renaming the key id field" do
380
+ k = key_data
381
+ item = Class.new(Sequel::Model(:items)) do
382
+ include AttrVault
383
+ vault_keyring k, key_field: :alt_key_id
384
+ vault_attr :secret
385
+ end
354
386
 
355
- context "with a digest field" do
356
- let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
357
- let(:key) do
358
- [ { id: key_id,
359
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
360
- created_at: Time.now } ]
361
- end
362
- let(:item) do
363
- k = key.to_json
364
- Class.new(Sequel::Model(:items)) do
365
- include AttrVault
366
- vault_keyring k
367
- vault_attr :secret, digest_field: :secret_digest
368
- vault_attr :other, digest_field: :other_digest
387
+ secret = "up up down down left right left right b a"
388
+ s = item.create(secret: secret)
389
+ s.reload
390
+ expect(s.secret).to eq secret
391
+ expect(s.secret_encrypted).not_to eq secret
392
+ expect(s.alt_key_id).not_to be_nil
393
+ expect(s.key_id).to be_nil
394
+ end
369
395
  end
370
- end
371
396
 
372
- def test_digest(key, data)
373
- OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
374
- key.first.fetch(:value), data)
375
- end
397
+ context "with a digest field" do
398
+ let(:key_id) { 1 }
399
+ let(:key) do
400
+ [ { id: key_id,
401
+ value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=' } ]
402
+ end
403
+ let(:item) do
404
+ k = key.to_json
405
+ Class.new(Sequel::Model(:items)) do
406
+ include AttrVault
407
+ vault_keyring k
408
+ vault_attr :secret, digest_field: :secret_digest
409
+ vault_attr :other, digest_field: :other_digest
410
+ end
411
+ end
376
412
 
377
- def count_matching_digests(item_class, digest_field, secret)
378
- item.where({digest_field => item_class.vault_digests(secret)}).count
379
- end
413
+ def test_digest(key, data)
414
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
415
+ key.first.fetch(:value), data)
416
+ end
380
417
 
381
- it "records the hmac of the plaintext value" do
382
- secret = 'snape kills dumbledore'
383
- s = item.create(secret: secret)
384
- expect(s.secret_digest).to eq(test_digest(key, secret))
385
- expect(count_matching_digests(item, :secret_digest, secret)).to eq(1)
386
- end
418
+ def count_matching_digests(item_class, digest_field, secret)
419
+ item.where({digest_field => item_class.vault_digests(secret)}).count
420
+ end
387
421
 
388
- it "can record multiple digest fields" do
389
- secret = 'joffrey kills ned'
390
- other_secret = '"gomer pyle" lawrence kills himself'
391
- s = item.create(secret: secret, other: other_secret)
392
- expect(s.secret_digest).to eq(test_digest(key, secret))
393
- expect(s.other_digest).to eq(test_digest(key, other_secret))
394
-
395
- # Check vault_digests feature matching against the database.
396
- expect(count_matching_digests(item, :secret_digest, secret)).to eq(1)
397
- expect(count_matching_digests(item, :other_digest, other_secret)).to eq(1)
398
-
399
- # Negative tests for mismatched digesting.
400
- expect(count_matching_digests(item, :secret_digest, other_secret))
401
- .to eq(0)
402
- expect(count_matching_digests(item, :other_digest, secret)).to eq(0)
403
- end
422
+ it "records the hmac of the plaintext value" do
423
+ secret = 'snape kills dumbledore'
424
+ s = item.create(secret: secret)
425
+ expect(s.secret_digest).to eq(test_digest(key, secret))
426
+ expect(count_matching_digests(item, :secret_digest, secret)).to eq(1)
427
+ end
404
428
 
405
- it "records the digest for an empty field" do
406
- s = item.create(secret: '', other: '')
407
- expect(s.secret_digest).to eq(test_digest(key, ''))
408
- expect(s.other_digest).to eq(test_digest(key, ''))
409
- end
429
+ it "can record multiple digest fields" do
430
+ secret = 'joffrey kills ned'
431
+ other_secret = '"gomer pyle" lawrence kills himself'
432
+ s = item.create(secret: secret, other: other_secret)
433
+ expect(s.secret_digest).to eq(test_digest(key, secret))
434
+ expect(s.other_digest).to eq(test_digest(key, other_secret))
435
+
436
+ # Check vault_digests feature matching against the database.
437
+ expect(count_matching_digests(item, :secret_digest, secret)).to eq(1)
438
+ expect(count_matching_digests(item, :other_digest, other_secret)).to eq(1)
439
+
440
+ # Negative tests for mismatched digesting.
441
+ expect(count_matching_digests(item, :secret_digest, other_secret))
442
+ .to eq(0)
443
+ expect(count_matching_digests(item, :other_digest, secret)).to eq(0)
444
+ end
410
445
 
411
- it "records the digest of a nil field" do
412
- s = item.create
413
- expect(s.secret_digest).to be_nil
414
- expect(s.other_digest).to be_nil
415
- end
416
- end
417
- end
446
+ it "records the digest for an empty field" do
447
+ s = item.create(secret: '', other: '')
448
+ expect(s.secret_digest).to eq(test_digest(key, ''))
449
+ expect(s.other_digest).to eq(test_digest(key, ''))
450
+ end
418
451
 
419
- describe "stress test" do
420
- let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
421
- let(:key_data) do
422
- [ { id: key_id,
423
- value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
424
- created_at: Time.now } ].to_json
425
- end
426
- let(:item) do
427
- k = key_data
428
- Class.new(Sequel::Model(:items)) do
429
- include AttrVault
430
- vault_keyring k
431
- vault_attr :secret
452
+ it "records the digest of a nil field" do
453
+ s = item.create
454
+ expect(s.secret_digest).to be_nil
455
+ expect(s.other_digest).to be_nil
456
+ end
457
+ end
432
458
  end
433
- end
434
459
 
435
- it "works" do
436
- 3.times.map do
437
- Thread.new do
438
- s = item.create(secret: 'that Commander Keen level in DOOM II')
439
- 1_000.times do
440
- new_secret = [ nil, '', 36.times.map { (0..255).to_a.sample.chr }.join('') ].sample
441
- s.update(secret: new_secret)
442
- s.reload
443
- expect(s.secret).to eq new_secret
460
+ describe "stress test" do
461
+ let(:key_id) { 1 }
462
+ let(:key_data) do
463
+ [ { id: key_id,
464
+ value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=' } ].to_json
465
+ end
466
+ let(:item) do
467
+ k = key_data
468
+ Class.new(Sequel::Model(:items)) do
469
+ include AttrVault
470
+ vault_keyring k
471
+ vault_attr :secret
444
472
  end
445
473
  end
446
- end.map(&:join)
474
+
475
+ it "works" do
476
+ 3.times.map do
477
+ Thread.new do
478
+ s = item.create(secret: 'that Commander Keen level in DOOM II')
479
+ 1_000.times do
480
+ new_secret = [ nil, '', 36.times.map { (0..255).to_a.sample.chr }.join('') ].sample
481
+ s.update(secret: new_secret)
482
+ s.reload
483
+ expect(s.secret).to eq new_secret
484
+ end
485
+ end
486
+ end.map(&:join)
487
+ end
488
+ end
447
489
  end
448
490
  end
data/spec/spec_helper.rb CHANGED
@@ -12,21 +12,36 @@ require 'pg'
12
12
  require 'sequel'
13
13
 
14
14
  conn = Sequel.connect(ENV['DATABASE_URL'])
15
- conn.run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
16
- conn.run 'DROP TABLE IF EXISTS items'
17
15
  conn.run <<-EOF
18
- CREATE TABLE items(
19
- id serial primary key,
20
- key_id uuid,
21
- alt_key_id uuid,
22
- secret_encrypted bytea,
23
- secret_digest bytea,
24
- other_encrypted bytea,
25
- other_digest bytea,
26
- not_secret text,
27
- other_not_secret text
28
- )
29
- EOF
16
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
17
+
18
+ DROP TABLE IF EXISTS items;
19
+ DROP TABLE IF EXISTS items_legacy;
20
+
21
+ CREATE TABLE items(
22
+ id serial primary key,
23
+ key_id integer,
24
+ alt_key_id integer,
25
+ secret_encrypted bytea,
26
+ secret_digest bytea,
27
+ other_encrypted bytea,
28
+ other_digest bytea,
29
+ not_secret text,
30
+ other_not_secret text
31
+ );
32
+
33
+ CREATE TABLE items_legacy(
34
+ id serial primary key,
35
+ key_id uuid,
36
+ alt_key_id uuid,
37
+ secret_encrypted bytea,
38
+ secret_digest bytea,
39
+ other_encrypted bytea,
40
+ other_digest bytea,
41
+ not_secret text,
42
+ other_not_secret text
43
+ );
44
+ EOF
30
45
 
31
46
  RSpec.configure do |config|
32
47
  config.run_all_when_everything_filtered = true
@@ -34,6 +49,7 @@ RSpec.configure do |config|
34
49
 
35
50
  config.before(:example) do
36
51
  conn.run 'TRUNCATE items'
52
+ conn.run 'TRUNCATE items_legacy'
37
53
  end
38
54
 
39
55
  # Run specs in random order to surface order dependencies. If you find an
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr_vault
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciek Sakrejda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-22 00:00:00.000000000 Z
11
+ date: 2016-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec
14
+ name: pg
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
20
- type: :development
19
+ version: 0.18.3
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: 0.18.3
27
27
  - !ruby/object:Gem::Dependency
28
- name: pg
28
+ name: sequel
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.18.3
34
- type: :development
33
+ version: '4.13'
34
+ type: :runtime
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.18.3
40
+ version: '4.13'
41
41
  - !ruby/object:Gem::Dependency
42
- name: sequel
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '4.13'
47
+ version: '3.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '4.13'
54
+ version: '3.0'
55
55
  description: Encryption at rest made easy
56
56
  email:
57
57
  - m.sakrejda@gmail.com