lockbox 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +305 -85
- data/lib/lockbox.rb +72 -3
- data/lib/lockbox/active_storage_extensions.rb +5 -30
- data/lib/lockbox/box.rb +17 -13
- data/lib/lockbox/carrier_wave_extensions.rb +15 -1
- data/lib/lockbox/key_generator.rb +43 -0
- data/lib/lockbox/model.rb +149 -0
- data/lib/lockbox/railtie.rb +0 -1
- data/lib/lockbox/utils.rb +14 -3
- data/lib/lockbox/version.rb +1 -1
- metadata +18 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4e87cf5f769166cd6cf534608a6877e5cd9f11931441307d9eca11a3d2c4d18
|
4
|
+
data.tar.gz: c52a2bded1142c80918f79de4317ec948c576c1168fd699dac0f69c7d2ca9b47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a92f516956fe3d4feb26cc338bf25a5048fc46916bc4c4722a12028b73697e467ceae118733f1dce778a1585e3a4e97fbf9073c3c80fa7d5340d1e0676cb02c
|
7
|
+
data.tar.gz: 60684645d7645b1b66a66a9b5393db04eb690095fc8bfbe16de50e8770225afe46e2236045cf4c0985a9e7b0e86aa7da16294804e0e6cebe2325193761d9965e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# Lockbox
|
2
2
|
|
3
|
-
:lock:
|
3
|
+
:lock: Modern encryption for Rails
|
4
4
|
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
5
|
+
- Uses state-of-the-art algorithms
|
6
|
+
- Works with database fields, files, and strings
|
7
|
+
- Stores encrypted data in a single field
|
8
|
+
- Requires you to only manage a single encryption key
|
9
|
+
- Makes migrating existing data and key rotation easy
|
8
10
|
|
9
|
-
Check out [this post](https://ankane.org/sensitive-data-rails) for more info on securing sensitive data with Rails
|
11
|
+
Check out [this post](https://ankane.org/modern-encryption-rails) for more info on its design, and [this post](https://ankane.org/sensitive-data-rails) for more info on securing sensitive data with Rails
|
10
12
|
|
11
13
|
[](https://travis-ci.org/ankane/lockbox)
|
12
14
|
|
@@ -23,41 +25,63 @@ gem 'lockbox'
|
|
23
25
|
Generate an encryption key
|
24
26
|
|
25
27
|
```ruby
|
26
|
-
|
28
|
+
Lockbox.generate_key
|
27
29
|
```
|
28
30
|
|
29
31
|
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production. Keys don’t need to be hex-encoded, but it’s often easier to store them this way.
|
30
32
|
|
33
|
+
Set the following environment variable with your key (you can use this one in development)
|
34
|
+
|
35
|
+
```sh
|
36
|
+
LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
37
|
+
```
|
38
|
+
|
39
|
+
or create `config/initializers/lockbox.rb` with something like
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
Lockbox.master_key = Rails.application.credentials.lockbox_master_key
|
43
|
+
```
|
44
|
+
|
31
45
|
Alternatively, you can use a [key management service](#key-management) to manage your keys.
|
32
46
|
|
33
|
-
##
|
47
|
+
## Database Fields
|
34
48
|
|
35
|
-
Create a
|
49
|
+
Create a migration with:
|
36
50
|
|
37
51
|
```ruby
|
38
|
-
|
52
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[5.2]
|
53
|
+
def change
|
54
|
+
add_column :users, :email_ciphertext, :text
|
55
|
+
end
|
56
|
+
end
|
39
57
|
```
|
40
58
|
|
41
|
-
|
59
|
+
Add to your model:
|
42
60
|
|
43
61
|
```ruby
|
44
|
-
|
62
|
+
class User < ApplicationRecord
|
63
|
+
encrypts :email
|
64
|
+
end
|
45
65
|
```
|
46
66
|
|
47
|
-
|
67
|
+
You can use `email` just like any other attribute.
|
48
68
|
|
49
69
|
```ruby
|
50
|
-
|
70
|
+
User.create!(email: "hi@example.org")
|
51
71
|
```
|
52
72
|
|
53
|
-
|
73
|
+
If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
|
74
|
+
|
75
|
+
## Files
|
76
|
+
|
77
|
+
### Active Storage
|
54
78
|
|
55
79
|
Add to your model:
|
56
80
|
|
57
81
|
```ruby
|
58
82
|
class User < ApplicationRecord
|
59
83
|
has_one_attached :license
|
60
|
-
|
84
|
+
encrypts_attached :license
|
61
85
|
end
|
62
86
|
```
|
63
87
|
|
@@ -66,7 +90,7 @@ Works with multiple attachments as well.
|
|
66
90
|
```ruby
|
67
91
|
class User < ApplicationRecord
|
68
92
|
has_many_attached :documents
|
69
|
-
|
93
|
+
encrypts_attached :documents
|
70
94
|
end
|
71
95
|
```
|
72
96
|
|
@@ -75,43 +99,130 @@ There are a few limitations to be aware of:
|
|
75
99
|
- Metadata like image width and height are not extracted when encrypted
|
76
100
|
- Direct uploads cannot be encrypted
|
77
101
|
|
78
|
-
|
102
|
+
To serve encrypted files, use a controller action.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
def license
|
106
|
+
send_data @user.license.download, type: @user.license.content_type
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
**Note:** With Rails 6, attachments are not encrypted with:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
User.create!(avatar: params[:avatar])
|
114
|
+
```
|
115
|
+
|
116
|
+
Until this is addressed, use:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
user = User.new
|
120
|
+
user.attach(params[:avatar])
|
121
|
+
user.save!
|
122
|
+
```
|
123
|
+
|
124
|
+
### CarrierWave
|
79
125
|
|
80
126
|
Add to your uploader:
|
81
127
|
|
82
128
|
```ruby
|
83
129
|
class LicenseUploader < CarrierWave::Uploader::Base
|
84
|
-
encrypt
|
130
|
+
encrypt
|
85
131
|
end
|
86
132
|
```
|
87
133
|
|
88
134
|
Encryption is applied to all versions after processing.
|
89
135
|
|
90
|
-
## Serving Files
|
91
|
-
|
92
136
|
To serve encrypted files, use a controller action.
|
93
137
|
|
94
138
|
```ruby
|
95
139
|
def license
|
96
|
-
send_data @user.license.
|
140
|
+
send_data @user.license.read, type: @user.license.content_type
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Local Files
|
145
|
+
|
146
|
+
Read the file as a binary string
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
message = File.binread("file.txt")
|
150
|
+
```
|
151
|
+
|
152
|
+
Then follow the instructions for encrypting a string below.
|
153
|
+
|
154
|
+
## Strings
|
155
|
+
|
156
|
+
Create a box
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
box = Lockbox.new(key: key)
|
160
|
+
```
|
161
|
+
|
162
|
+
Encrypt
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
ciphertext = box.encrypt(message)
|
166
|
+
```
|
167
|
+
|
168
|
+
Decrypt
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
box.decrypt(ciphertext)
|
172
|
+
```
|
173
|
+
|
174
|
+
## Migrating Existing Data
|
175
|
+
|
176
|
+
Lockbox makes it easy to encrypt an existing column. Add a new column for the ciphertext, then add to your model:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class User < ApplicationRecord
|
180
|
+
encrypts :email, migrating: true
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
Backfill the data in the Rails console:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
Lockbox.migrate(User)
|
188
|
+
```
|
189
|
+
|
190
|
+
Then update the model to the desired state:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class User < ApplicationRecord
|
194
|
+
encrypts :email
|
195
|
+
|
196
|
+
# remove this line after dropping email column
|
197
|
+
self.ignored_columns = ["email"]
|
97
198
|
end
|
98
199
|
```
|
99
200
|
|
100
|
-
|
201
|
+
Finally, drop the unencrypted column.
|
101
202
|
|
102
203
|
## Key Rotation
|
103
204
|
|
104
205
|
To make key rotation easy, you can pass previous versions of keys that can decrypt.
|
105
206
|
|
207
|
+
For Active Record, use:
|
208
|
+
|
106
209
|
```ruby
|
107
|
-
|
210
|
+
class User < ApplicationRecord
|
211
|
+
encrypts :email, previous_versions: [{key: previous_key}]
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
To rotate, use:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
user.update!(email: user.email)
|
108
219
|
```
|
109
220
|
|
110
221
|
For Active Storage use:
|
111
222
|
|
112
223
|
```ruby
|
113
224
|
class User < ApplicationRecord
|
114
|
-
|
225
|
+
encrypts_attached :license, previous_versions: [{key: previous_key}]
|
115
226
|
end
|
116
227
|
```
|
117
228
|
|
@@ -125,7 +236,7 @@ For CarrierWave, use:
|
|
125
236
|
|
126
237
|
```ruby
|
127
238
|
class LicenseUploader < CarrierWave::Uploader::Base
|
128
|
-
encrypt
|
239
|
+
encrypt previous_versions: [{key: previous_key}]
|
129
240
|
end
|
130
241
|
```
|
131
242
|
|
@@ -135,55 +246,108 @@ To rotate existing files, use:
|
|
135
246
|
user.license.rotate_encryption!
|
136
247
|
```
|
137
248
|
|
249
|
+
For strings, use:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
Lockbox.new(key: key, previous_versions: [{key: previous_key}])
|
253
|
+
```
|
254
|
+
|
255
|
+
## Fixtures
|
256
|
+
|
257
|
+
You can use encrypted attributes in fixtures with:
|
258
|
+
|
259
|
+
```yml
|
260
|
+
test_user:
|
261
|
+
email_ciphertext: <%= User.generate_email_ciphertext("secret").inspect %>
|
262
|
+
```
|
263
|
+
|
264
|
+
Be sure to include the `inspect` at the end or it won’t be encoded properly in YAML.
|
265
|
+
|
138
266
|
## Algorithms
|
139
267
|
|
140
268
|
### AES-GCM
|
141
269
|
|
142
|
-
|
270
|
+
This is the default algorithm. Rotate the key every 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the key.
|
271
|
+
|
272
|
+
### XSalsa20
|
273
|
+
|
274
|
+
You can also use XSalsa20, which uses an extended nonce so you don’t have to worry about nonce collisions. First, [install Libsodium](https://github.com/crypto-rb/rbnacl/wiki/Installing-libsodium). For Homebrew, use:
|
143
275
|
|
144
|
-
|
276
|
+
```sh
|
277
|
+
brew install libsodium
|
278
|
+
```
|
145
279
|
|
146
|
-
|
280
|
+
And add to your Gemfile:
|
147
281
|
|
148
282
|
```ruby
|
149
283
|
gem 'rbnacl'
|
150
284
|
```
|
151
285
|
|
152
|
-
Then
|
286
|
+
Then add to your model:
|
153
287
|
|
154
|
-
```ruby
|
155
|
-
# files
|
156
|
-
box = Lockbox.new(key: key, algorithm: "xchacha20")
|
157
288
|
|
158
|
-
|
289
|
+
```ruby
|
159
290
|
class User < ApplicationRecord
|
160
|
-
|
161
|
-
end
|
162
|
-
|
163
|
-
# CarrierWave
|
164
|
-
class LicenseUploader < CarrierWave::Uploader::Base
|
165
|
-
encrypt key: key, algorithm: "xchacha20"
|
291
|
+
encrypts :email, algorithm: "xsalsa20"
|
166
292
|
end
|
167
293
|
```
|
168
294
|
|
169
295
|
Make it the default with:
|
170
296
|
|
171
297
|
```ruby
|
172
|
-
Lockbox.default_options = {algorithm: "
|
298
|
+
Lockbox.default_options = {algorithm: "xsalsa20"}
|
173
299
|
```
|
174
300
|
|
175
301
|
You can also pass an algorithm to `previous_versions` for key rotation.
|
176
302
|
|
177
|
-
|
303
|
+
#### XSalsa20 Deployment
|
178
304
|
|
179
|
-
|
305
|
+
##### Heroku
|
180
306
|
|
181
|
-
[
|
307
|
+
Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packages) preinstalled.
|
182
308
|
|
183
|
-
|
184
|
-
|
309
|
+
##### Ubuntu
|
310
|
+
|
311
|
+
For Ubuntu 16.04, use:
|
312
|
+
|
313
|
+
```sh
|
314
|
+
sudo apt-get install libsodium18
|
315
|
+
```
|
316
|
+
|
317
|
+
For Ubuntu 18.04, use:
|
318
|
+
|
319
|
+
```sh
|
320
|
+
sudo apt-get install libsodium23
|
185
321
|
```
|
186
322
|
|
323
|
+
##### Travis CI
|
324
|
+
|
325
|
+
On Xenial, add to `.travis.yml`:
|
326
|
+
|
327
|
+
```yml
|
328
|
+
addons:
|
329
|
+
apt:
|
330
|
+
packages:
|
331
|
+
- libsodium18
|
332
|
+
```
|
333
|
+
|
334
|
+
##### CircleCI
|
335
|
+
|
336
|
+
Add a step to `.circleci/config.yml`:
|
337
|
+
|
338
|
+
```yml
|
339
|
+
- run:
|
340
|
+
name: install Libsodium
|
341
|
+
command: |
|
342
|
+
sudo apt-get install -y libsodium18
|
343
|
+
```
|
344
|
+
|
345
|
+
## Hybrid Cryptography
|
346
|
+
|
347
|
+
[Hybrid cryptography](https://en.wikipedia.org/wiki/Hybrid_cryptosystem) allows servers to encrypt data without being able to decrypt it.
|
348
|
+
|
349
|
+
Follow the instructions above for installing Libsodium and including `rbnacl` in your Gemfile.
|
350
|
+
|
187
351
|
Generate a key pair with:
|
188
352
|
|
189
353
|
```ruby
|
@@ -193,17 +357,8 @@ Lockbox.generate_key_pair
|
|
193
357
|
Store the keys with your other secrets. Then use:
|
194
358
|
|
195
359
|
```ruby
|
196
|
-
# files
|
197
|
-
box = Lockbox.new(algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key)
|
198
|
-
|
199
|
-
# Active Storage
|
200
360
|
class User < ApplicationRecord
|
201
|
-
|
202
|
-
end
|
203
|
-
|
204
|
-
# CarrierWave
|
205
|
-
class LicenseUploader < CarrierWave::Uploader::Base
|
206
|
-
encrypt algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
|
361
|
+
encrypts :email, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
|
207
362
|
end
|
208
363
|
```
|
209
364
|
|
@@ -211,15 +366,29 @@ Make sure `decryption_key` is `nil` on servers that shouldn’t decrypt.
|
|
211
366
|
|
212
367
|
This uses X25519 for key exchange and XSalsa20-Poly1305 for encryption.
|
213
368
|
|
369
|
+
## Key Separation
|
370
|
+
|
371
|
+
The master key is used to generate unique keys for each column. This technique comes from [CipherSweet](https://ciphersweet.paragonie.com/internals/key-hierarchy). The table name and column name are both used in this process. If you need to rename a table with encrypted columns, or an encrypted column itself, get the key:
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
|
375
|
+
```
|
376
|
+
|
377
|
+
And set it directly before renaming:
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
class User < ApplicationRecord
|
381
|
+
encrypts :email, key: ENV["USER_EMAIL_ENCRYPTION_KEY"]
|
382
|
+
end
|
383
|
+
```
|
384
|
+
|
214
385
|
## Key Management
|
215
386
|
|
216
387
|
You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
|
217
388
|
|
218
|
-
For Active Storage, use:
|
219
|
-
|
220
389
|
```ruby
|
221
390
|
class User < ApplicationRecord
|
222
|
-
|
391
|
+
encrypts :email, key: :kms_key
|
223
392
|
end
|
224
393
|
```
|
225
394
|
|
@@ -235,70 +404,121 @@ end
|
|
235
404
|
|
236
405
|
## Compatibility
|
237
406
|
|
238
|
-
It’s easy to read encrypted
|
407
|
+
It’s easy to read encrypted data in another language if needed.
|
239
408
|
|
240
|
-
|
241
|
-
|
242
|
-
The format for AES-GCM is:
|
409
|
+
For AES-GCM, the format is:
|
243
410
|
|
244
411
|
- nonce (IV) - 12 bytes
|
245
412
|
- ciphertext - variable length
|
246
413
|
- authentication tag - 16 bytes
|
247
414
|
|
248
|
-
|
415
|
+
Here are [some examples](docs/Compatibility.md).
|
249
416
|
|
250
|
-
|
417
|
+
For XSalsa20, use the appropriate [Libsodium library](https://libsodium.gitbook.io/doc/bindings_for_other_languages).
|
251
418
|
|
252
|
-
|
419
|
+
## Migrating from Another Library
|
253
420
|
|
254
|
-
|
255
|
-
2. XChaCha20
|
256
|
-
3. Hybrid cryptography
|
257
|
-
4. No need for separate IV columns
|
421
|
+
Lockbox makes it easy to migrate from another library. The example below uses `attr_encrypted` but the same approach should work for any library.
|
258
422
|
|
259
|
-
|
423
|
+
Let’s suppose your model looks like this:
|
260
424
|
|
261
425
|
```ruby
|
262
|
-
|
426
|
+
class User < ApplicationRecord
|
427
|
+
attr_encrypted :name, key: key
|
428
|
+
attr_encrypted :email, key: key
|
429
|
+
end
|
263
430
|
```
|
264
431
|
|
265
|
-
Create a migration
|
432
|
+
Create a migration with:
|
266
433
|
|
267
434
|
```ruby
|
268
|
-
class
|
435
|
+
class MigrateToLockbox < ActiveRecord::Migration[5.2]
|
269
436
|
def change
|
270
|
-
add_column :users, :
|
437
|
+
add_column :users, :name_ciphertext, :text
|
438
|
+
add_column :users, :email_ciphertext, :text
|
271
439
|
end
|
272
440
|
end
|
273
441
|
```
|
274
442
|
|
275
|
-
|
443
|
+
And add `encrypts` to your model with the `migrating` option:
|
276
444
|
|
277
445
|
```ruby
|
278
446
|
class User < ApplicationRecord
|
279
|
-
|
280
|
-
|
281
|
-
attribute :encrypted_phone_iv # prevent attr_encrypted error
|
447
|
+
encrypts :name, :email, migrating: true
|
282
448
|
end
|
283
449
|
```
|
284
450
|
|
285
|
-
|
451
|
+
Then run:
|
452
|
+
|
453
|
+
```ruby
|
454
|
+
Lockbox.migrate(User)
|
455
|
+
```
|
456
|
+
|
457
|
+
Once all records are migrated, remove the `migrating` option and the previous model code (the `attr_encrypted` methods in this example).
|
286
458
|
|
287
459
|
```ruby
|
288
460
|
class User < ApplicationRecord
|
289
|
-
|
461
|
+
encrypts :name, :email
|
462
|
+
end
|
463
|
+
```
|
464
|
+
|
465
|
+
Then remove the previous gem from your Gemfile and drop its columns.
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[5.2]
|
469
|
+
def change
|
470
|
+
remove_column :users, :encrypted_name, :text
|
471
|
+
remove_column :users, :encrypted_name_iv, :text
|
472
|
+
remove_column :users, :encrypted_email, :text
|
473
|
+
remove_column :users, :encrypted_email_iv, :text
|
474
|
+
end
|
475
|
+
end
|
476
|
+
```
|
477
|
+
|
478
|
+
## Upgrading
|
479
|
+
|
480
|
+
### 0.2.0
|
481
|
+
|
482
|
+
0.2.0 brings a number of improvements. Here are a few to be aware of:
|
483
|
+
|
484
|
+
- Added `encrypts` method for database fields
|
485
|
+
- Added support for XSalsa20
|
486
|
+
- `attached_encrypted` is deprecated in favor of `encrypts_attached`.
|
487
|
+
|
488
|
+
#### Optional
|
489
|
+
|
490
|
+
To switch to a master key, generate a key:
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
Lockbox.generate_key
|
494
|
+
```
|
290
495
|
|
291
|
-
|
496
|
+
And set `ENV["LOCKBOX_MASTER_KEY"]` or `Lockbox.master_key`.
|
497
|
+
|
498
|
+
Update your model:
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
class User < ApplicationRecord
|
502
|
+
encrypts_attached :license, previous_versions: [{key: key}]
|
292
503
|
end
|
293
504
|
```
|
294
505
|
|
295
|
-
|
506
|
+
New uploads will be encrypted with the new key.
|
296
507
|
|
297
|
-
|
508
|
+
You can rotate existing records with:
|
298
509
|
|
299
510
|
```ruby
|
300
|
-
|
301
|
-
|
511
|
+
User.unscoped.find_each do |user|
|
512
|
+
user.license.rotate_encryption!
|
513
|
+
end
|
514
|
+
```
|
515
|
+
|
516
|
+
Once that’s complete, update your model:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
class User < ApplicationRecord
|
520
|
+
encrypts_attached :license
|
521
|
+
end
|
302
522
|
```
|
303
523
|
|
304
524
|
## History
|
data/lib/lockbox.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
# dependencies
|
2
|
+
require "securerandom"
|
3
|
+
|
1
4
|
# modules
|
2
5
|
require "lockbox/box"
|
3
6
|
require "lockbox/encryptor"
|
7
|
+
require "lockbox/key_generator"
|
4
8
|
require "lockbox/utils"
|
5
9
|
require "lockbox/version"
|
6
10
|
|
@@ -8,14 +12,62 @@ require "lockbox/version"
|
|
8
12
|
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
9
13
|
require "lockbox/railtie" if defined?(Rails)
|
10
14
|
|
15
|
+
if defined?(ActiveSupport)
|
16
|
+
ActiveSupport.on_load(:active_record) do
|
17
|
+
require "lockbox/model"
|
18
|
+
extend Lockbox::Model
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
11
22
|
class Lockbox
|
12
23
|
class Error < StandardError; end
|
13
24
|
class DecryptionError < Error; end
|
14
25
|
|
15
26
|
class << self
|
16
27
|
attr_accessor :default_options
|
28
|
+
attr_writer :master_key
|
29
|
+
end
|
30
|
+
self.default_options = {}
|
31
|
+
|
32
|
+
def self.master_key
|
33
|
+
@master_key ||= ENV["LOCKBOX_MASTER_KEY"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.migrate(model, restart: false)
|
37
|
+
# get fields
|
38
|
+
fields = model.lockbox_attributes.select { |k, v| v[:migrating] }
|
39
|
+
|
40
|
+
# get blind indexes
|
41
|
+
blind_indexes = model.respond_to?(:blind_indexes) ? model.blind_indexes.select { |k, v| v[:migrating] } : {}
|
42
|
+
|
43
|
+
# build relation
|
44
|
+
relation = model.unscoped
|
45
|
+
|
46
|
+
unless restart
|
47
|
+
attributes = fields.map { |_, v| v[:encrypted_attribute] }
|
48
|
+
attributes += blind_indexes.map { |_, v| v[:bidx_attribute] }
|
49
|
+
|
50
|
+
attributes.each_with_index do |attribute, i|
|
51
|
+
relation =
|
52
|
+
if i == 0
|
53
|
+
relation.where(attribute => nil)
|
54
|
+
else
|
55
|
+
relation.or(model.where(attribute => nil))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# migrate
|
61
|
+
relation.find_each do |record|
|
62
|
+
fields.each do |k, v|
|
63
|
+
record.send("#{v[:attribute]}=", record.send(k)) if restart || !record.send(v[:encrypted_attribute])
|
64
|
+
end
|
65
|
+
blind_indexes.each do |k, v|
|
66
|
+
record.send("compute_#{k}_bidx") if restart || !record.send(v[:bidx_attribute])
|
67
|
+
end
|
68
|
+
record.save(validate: false) if record.changed?
|
69
|
+
end
|
17
70
|
end
|
18
|
-
self.default_options = {algorithm: "aes-gcm"}
|
19
71
|
|
20
72
|
def initialize(**options)
|
21
73
|
options = self.class.default_options.merge(options)
|
@@ -56,6 +108,10 @@ class Lockbox
|
|
56
108
|
end
|
57
109
|
end
|
58
110
|
|
111
|
+
def self.generate_key
|
112
|
+
SecureRandom.hex(32)
|
113
|
+
end
|
114
|
+
|
59
115
|
def self.generate_key_pair
|
60
116
|
require "rbnacl"
|
61
117
|
# encryption and decryption servers exchange public keys
|
@@ -65,11 +121,24 @@ class Lockbox
|
|
65
121
|
# alice is sending message to bob
|
66
122
|
# use bob first in both cases to prevent keys being swappable
|
67
123
|
{
|
68
|
-
encryption_key: (bob.public_key.to_bytes + alice.to_bytes)
|
69
|
-
decryption_key: (bob.to_bytes + alice.public_key.to_bytes)
|
124
|
+
encryption_key: to_hex(bob.public_key.to_bytes + alice.to_bytes),
|
125
|
+
decryption_key: to_hex(bob.to_bytes + alice.public_key.to_bytes)
|
70
126
|
}
|
71
127
|
end
|
72
128
|
|
129
|
+
def self.attribute_key(table:, attribute:, master_key: nil, encode: true)
|
130
|
+
master_key ||= Lockbox.master_key
|
131
|
+
raise ArgumentError, "Missing master key" unless master_key
|
132
|
+
|
133
|
+
key = Lockbox::KeyGenerator.new(master_key).attribute_key(table: table, attribute: attribute)
|
134
|
+
key = to_hex(key) if encode
|
135
|
+
key
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.to_hex(str)
|
139
|
+
str.unpack("H*").first
|
140
|
+
end
|
141
|
+
|
73
142
|
private
|
74
143
|
|
75
144
|
def check_string(str, name)
|
@@ -10,16 +10,16 @@ class Lockbox
|
|
10
10
|
def encrypted?
|
11
11
|
# could use record_type directly
|
12
12
|
# but record should already be loaded most of the time
|
13
|
-
Utils.encrypted_options(record, name).
|
13
|
+
!Utils.encrypted_options(record, name).nil?
|
14
14
|
end
|
15
15
|
|
16
16
|
def encrypt_attachable(attachable)
|
17
17
|
options = Utils.encrypted_options(record, name)
|
18
|
-
box = Utils.build_box(record, options)
|
18
|
+
box = Utils.build_box(record, options, record.class.table_name, name)
|
19
19
|
|
20
20
|
case attachable
|
21
21
|
when ActiveStorage::Blob
|
22
|
-
raise
|
22
|
+
raise NotImplementedError, "Not supported"
|
23
23
|
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
24
24
|
attachable = {
|
25
25
|
io: StringIO.new(box.encrypt(attachable.read)),
|
@@ -33,7 +33,7 @@ class Lockbox
|
|
33
33
|
content_type: attachable[:content_type]
|
34
34
|
}
|
35
35
|
when String
|
36
|
-
raise
|
36
|
+
raise NotImplementedError, "Not supported"
|
37
37
|
else
|
38
38
|
nil
|
39
39
|
end
|
@@ -107,7 +107,7 @@ class Lockbox
|
|
107
107
|
|
108
108
|
options = Utils.encrypted_options(record, name)
|
109
109
|
if options
|
110
|
-
result = Utils.build_box(record, options).decrypt(result)
|
110
|
+
result = Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
|
111
111
|
end
|
112
112
|
|
113
113
|
result
|
@@ -123,30 +123,5 @@ class Lockbox
|
|
123
123
|
after_save :mark_analyzed
|
124
124
|
end
|
125
125
|
end
|
126
|
-
|
127
|
-
module Model
|
128
|
-
def attached_encrypted(name, **options)
|
129
|
-
class_eval do
|
130
|
-
@encrypted_attachments ||= {}
|
131
|
-
|
132
|
-
unless respond_to?(:encrypted_attachments)
|
133
|
-
def self.encrypted_attachments
|
134
|
-
parent_attachments =
|
135
|
-
if superclass.respond_to?(:encrypted_attachments)
|
136
|
-
superclass.encrypted_attachments
|
137
|
-
else
|
138
|
-
{}
|
139
|
-
end
|
140
|
-
|
141
|
-
parent_attachments.merge(@encrypted_attachments || {})
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
raise ArgumentError, "Duplicate encrypted attachment: #{name}" if encrypted_attachments[name]
|
146
|
-
|
147
|
-
@encrypted_attachments[name] = options
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
126
|
end
|
152
127
|
end
|
data/lib/lockbox/box.rb
CHANGED
@@ -5,9 +5,9 @@ class Lockbox
|
|
5
5
|
def initialize(key: nil, algorithm: nil, encryption_key: nil, decryption_key: nil)
|
6
6
|
raise ArgumentError, "Cannot pass both key and public/private key" if key && (encryption_key || decryption_key)
|
7
7
|
|
8
|
-
key = decode_key(key) if key
|
9
|
-
encryption_key = decode_key(encryption_key) if encryption_key
|
10
|
-
decryption_key = decode_key(decryption_key) if decryption_key
|
8
|
+
key = Lockbox::Utils.decode_key(key) if key
|
9
|
+
encryption_key = Lockbox::Utils.decode_key(encryption_key) if encryption_key
|
10
|
+
decryption_key = Lockbox::Utils.decode_key(decryption_key) if decryption_key
|
11
11
|
|
12
12
|
algorithm ||= "aes-gcm"
|
13
13
|
|
@@ -20,6 +20,10 @@ class Lockbox
|
|
20
20
|
raise ArgumentError, "Missing key" unless key
|
21
21
|
require "rbnacl"
|
22
22
|
@box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)
|
23
|
+
when "xsalsa20"
|
24
|
+
raise ArgumentError, "Missing key" unless key
|
25
|
+
require "rbnacl"
|
26
|
+
@box = RbNaCl::SecretBoxes::XSalsa20Poly1305.new(key)
|
23
27
|
when "hybrid"
|
24
28
|
raise ArgumentError, "Missing key" unless encryption_key || decryption_key
|
25
29
|
require "rbnacl"
|
@@ -33,11 +37,15 @@ class Lockbox
|
|
33
37
|
end
|
34
38
|
|
35
39
|
def encrypt(message, associated_data: nil)
|
36
|
-
|
40
|
+
case @algorithm
|
41
|
+
when "hybrid"
|
37
42
|
raise ArgumentError, "No public key set" unless @encryption_box
|
38
43
|
raise ArgumentError, "Associated data not supported with this algorithm" if associated_data
|
39
44
|
nonce = generate_nonce(@encryption_box)
|
40
45
|
ciphertext = @encryption_box.encrypt(nonce, message)
|
46
|
+
when "xsalsa20"
|
47
|
+
nonce = generate_nonce(@box)
|
48
|
+
ciphertext = @box.encrypt(nonce, message)
|
41
49
|
else
|
42
50
|
nonce = generate_nonce(@box)
|
43
51
|
ciphertext = @box.encrypt(nonce, message, associated_data)
|
@@ -46,11 +54,15 @@ class Lockbox
|
|
46
54
|
end
|
47
55
|
|
48
56
|
def decrypt(ciphertext, associated_data: nil)
|
49
|
-
|
57
|
+
case @algorithm
|
58
|
+
when "hybrid"
|
50
59
|
raise ArgumentError, "No private key set" unless @decryption_box
|
51
60
|
raise ArgumentError, "Associated data not supported with this algorithm" if associated_data
|
52
61
|
nonce, ciphertext = extract_nonce(@decryption_box, ciphertext)
|
53
62
|
@decryption_box.decrypt(nonce, ciphertext)
|
63
|
+
when "xsalsa20"
|
64
|
+
nonce, ciphertext = extract_nonce(@box, ciphertext)
|
65
|
+
@box.decrypt(nonce, ciphertext)
|
54
66
|
else
|
55
67
|
nonce, ciphertext = extract_nonce(@box, ciphertext)
|
56
68
|
@box.decrypt(nonce, ciphertext, associated_data)
|
@@ -73,13 +85,5 @@ class Lockbox
|
|
73
85
|
nonce = bytes.slice(0, nonce_bytes)
|
74
86
|
[nonce, bytes.slice(nonce_bytes..-1)]
|
75
87
|
end
|
76
|
-
|
77
|
-
# decode hex key
|
78
|
-
def decode_key(key)
|
79
|
-
if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{64,128}\z/i
|
80
|
-
key = [key].pack("H*")
|
81
|
-
end
|
82
|
-
key
|
83
|
-
end
|
84
88
|
end
|
85
89
|
end
|
@@ -36,7 +36,21 @@ class Lockbox
|
|
36
36
|
private
|
37
37
|
|
38
38
|
define_method :lockbox do
|
39
|
-
@lockbox ||=
|
39
|
+
@lockbox ||= begin
|
40
|
+
table = model ? model.class.table_name : "_uploader"
|
41
|
+
attribute =
|
42
|
+
if mounted_as
|
43
|
+
mounted_as.to_s
|
44
|
+
else
|
45
|
+
uploader = self
|
46
|
+
while uploader.parent_version
|
47
|
+
uploader = uploader.parent_version
|
48
|
+
end
|
49
|
+
uploader.class.name.sub(/Uploader\z/, "").underscore
|
50
|
+
end
|
51
|
+
|
52
|
+
Utils.build_box(self, options, table, attribute)
|
53
|
+
end
|
40
54
|
end
|
41
55
|
end
|
42
56
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Lockbox
|
2
|
+
class KeyGenerator
|
3
|
+
def initialize(master_key)
|
4
|
+
@master_key = master_key
|
5
|
+
end
|
6
|
+
|
7
|
+
# pattern ported from CipherSweet
|
8
|
+
# https://ciphersweet.paragonie.com/internals/key-hierarchy
|
9
|
+
def attribute_key(table:, attribute:)
|
10
|
+
raise ArgumentError, "Missing table for key generation" if table.to_s.empty?
|
11
|
+
raise ArgumentError, "Missing attribute for key generation" if attribute.to_s.empty?
|
12
|
+
|
13
|
+
c = "\xB4"*32
|
14
|
+
hkdf(Lockbox::Utils.decode_key(@master_key), salt: table.to_s, info: "#{c}#{attribute}", length: 32, hash: "sha384")
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def hash_hmac(hash, ikm, salt)
|
20
|
+
OpenSSL::HMAC.digest(hash, salt, ikm)
|
21
|
+
end
|
22
|
+
|
23
|
+
def hkdf(ikm, salt:, info:, length:, hash:)
|
24
|
+
if OpenSSL::KDF.respond_to?(:hkdf)
|
25
|
+
return OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: hash)
|
26
|
+
end
|
27
|
+
|
28
|
+
prk = hash_hmac(hash, ikm, salt)
|
29
|
+
|
30
|
+
# empty binary string
|
31
|
+
t = String.new
|
32
|
+
last_block = String.new
|
33
|
+
block_index = 1
|
34
|
+
while t.bytesize < length
|
35
|
+
last_block = hash_hmac(hash, last_block + info + [block_index].pack("C"), prk)
|
36
|
+
t << last_block
|
37
|
+
block_index += 1
|
38
|
+
end
|
39
|
+
|
40
|
+
t[0, length]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
class Lockbox
|
2
|
+
module Model
|
3
|
+
def attached_encrypted(attribute, **options)
|
4
|
+
warn "[lockbox] DEPRECATION WARNING: Use encrypts_attached instead"
|
5
|
+
encrypts_attached(attribute, **options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def encrypts_attached(*attributes, **options)
|
9
|
+
attributes.each do |name|
|
10
|
+
name = name.to_sym
|
11
|
+
|
12
|
+
class_eval do
|
13
|
+
@lockbox_attachments ||= {}
|
14
|
+
|
15
|
+
unless respond_to?(:lockbox_attachments)
|
16
|
+
def self.lockbox_attachments
|
17
|
+
parent_attachments =
|
18
|
+
if superclass.respond_to?(:lockbox_attachments)
|
19
|
+
superclass.lockbox_attachments
|
20
|
+
else
|
21
|
+
{}
|
22
|
+
end
|
23
|
+
|
24
|
+
parent_attachments.merge(@lockbox_attachments || {})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
raise "Duplicate encrypted attachment: #{name}" if lockbox_attachments[name]
|
29
|
+
@lockbox_attachments[name] = options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def encrypts(*attributes, **options)
|
35
|
+
attributes.each do |name|
|
36
|
+
# add default options
|
37
|
+
encrypted_attribute = "#{name}_ciphertext"
|
38
|
+
|
39
|
+
options = options.dup
|
40
|
+
|
41
|
+
# migrating
|
42
|
+
original_name = name.to_sym
|
43
|
+
name = "migrated_#{name}" if options[:migrating]
|
44
|
+
|
45
|
+
name = name.to_sym
|
46
|
+
|
47
|
+
options[:attribute] = name.to_s
|
48
|
+
options[:encrypted_attribute] = encrypted_attribute
|
49
|
+
class_method_name = "generate_#{encrypted_attribute}"
|
50
|
+
|
51
|
+
class_eval do
|
52
|
+
if options[:migrating]
|
53
|
+
before_validation do
|
54
|
+
send("#{name}=", send(original_name)) if send("#{original_name}_changed?")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@lockbox_attributes ||= {}
|
59
|
+
|
60
|
+
unless respond_to?(:lockbox_attributes)
|
61
|
+
def self.lockbox_attributes
|
62
|
+
parent_attributes =
|
63
|
+
if superclass.respond_to?(:lockbox_attributes)
|
64
|
+
superclass.lockbox_attributes
|
65
|
+
else
|
66
|
+
{}
|
67
|
+
end
|
68
|
+
|
69
|
+
parent_attributes.merge(@lockbox_attributes || {})
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
|
74
|
+
@lockbox_attributes[original_name] = options
|
75
|
+
|
76
|
+
if @lockbox_attributes.size == 1
|
77
|
+
def serializable_hash(options = nil)
|
78
|
+
options = options.try(:dup) || {}
|
79
|
+
options[:except] = Array(options[:except])
|
80
|
+
options[:except] += self.class.lockbox_attributes.values.reject { |v| v[:attached] }.flat_map { |v| [v[:attribute], v[:encrypted_attribute]] }
|
81
|
+
super(options)
|
82
|
+
end
|
83
|
+
|
84
|
+
# use same approach as devise
|
85
|
+
def inspect
|
86
|
+
inspection =
|
87
|
+
serializable_hash.map do |k,v|
|
88
|
+
"#{k}: #{respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : v.inspect}"
|
89
|
+
end
|
90
|
+
"#<#{self.class} #{inspection.join(", ")}>"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
attribute name, :string
|
95
|
+
|
96
|
+
define_method("#{name}=") do |message|
|
97
|
+
# decrypt first for dirty tracking
|
98
|
+
# don't raise error if can't decrypt previous
|
99
|
+
begin
|
100
|
+
send(name)
|
101
|
+
rescue Lockbox::DecryptionError
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
ciphertext =
|
106
|
+
if message.nil? || message == ""
|
107
|
+
message
|
108
|
+
else
|
109
|
+
self.class.send(class_method_name, message, context: self)
|
110
|
+
end
|
111
|
+
send("#{encrypted_attribute}=", ciphertext)
|
112
|
+
|
113
|
+
super(message)
|
114
|
+
end
|
115
|
+
|
116
|
+
define_method(name) do
|
117
|
+
message = super()
|
118
|
+
unless message
|
119
|
+
ciphertext = send(encrypted_attribute)
|
120
|
+
message =
|
121
|
+
if ciphertext.nil? || ciphertext == ""
|
122
|
+
ciphertext
|
123
|
+
else
|
124
|
+
decoded = Base64.decode64(ciphertext)
|
125
|
+
Lockbox::Utils.build_box(self, options, self.class.table_name, encrypted_attribute).decrypt(decoded)
|
126
|
+
end
|
127
|
+
|
128
|
+
# set previous attribute on first decrypt
|
129
|
+
@attributes[name.to_s].instance_variable_set("@value_before_type_cast", message)
|
130
|
+
|
131
|
+
# cache
|
132
|
+
if respond_to?(:_write_attribute, true)
|
133
|
+
_write_attribute(name, message)
|
134
|
+
else
|
135
|
+
raw_write_attribute(name, message)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
message
|
139
|
+
end
|
140
|
+
|
141
|
+
# for fixtures
|
142
|
+
define_singleton_method class_method_name do |message, **opts|
|
143
|
+
Base64.strict_encode64(Lockbox::Utils.build_box(opts[:context], options, table_name, encrypted_attribute).encrypt(message))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/lib/lockbox/railtie.rb
CHANGED
@@ -8,7 +8,6 @@ class Lockbox
|
|
8
8
|
ActiveStorage::Attached.prepend(Lockbox::ActiveStorageExtensions::Attached)
|
9
9
|
ActiveStorage::Attached::One.prepend(Lockbox::ActiveStorageExtensions::AttachedOne)
|
10
10
|
ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
|
11
|
-
ActiveRecord::Base.extend(Lockbox::ActiveStorageExtensions::Model) if defined?(ActiveRecord)
|
12
11
|
end
|
13
12
|
|
14
13
|
app.config.to_prepare do
|
data/lib/lockbox/utils.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Lockbox
|
2
2
|
class Utils
|
3
|
-
def self.build_box(context, options)
|
4
|
-
options = options.
|
3
|
+
def self.build_box(context, options, table, attribute)
|
4
|
+
options = options.except(:attribute, :encrypted_attribute, :migrating, :attached)
|
5
5
|
options.each do |k, v|
|
6
6
|
if v.is_a?(Proc)
|
7
7
|
options[k] = context.instance_exec(&v) if v.respond_to?(:call)
|
@@ -10,11 +10,22 @@ class Lockbox
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
+
unless options[:key] || options[:encryption_key] || options[:decryption_key]
|
14
|
+
options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
|
15
|
+
end
|
16
|
+
|
13
17
|
Lockbox.new(options)
|
14
18
|
end
|
15
19
|
|
16
20
|
def self.encrypted_options(record, name)
|
17
|
-
record.class.respond_to?(:
|
21
|
+
record.class.respond_to?(:lockbox_attachments) && record.class.lockbox_attachments[name.to_sym]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.decode_key(key)
|
25
|
+
if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{64,128}\z/i
|
26
|
+
key = [key].pack("H*")
|
27
|
+
end
|
28
|
+
key
|
18
29
|
end
|
19
30
|
end
|
20
31
|
end
|
data/lib/lockbox/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lockbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: carrierwave
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: combustion
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rails
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -67,21 +67,21 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: minitest
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '5'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '5'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
@@ -95,35 +95,21 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: rbnacl
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
103
|
+
version: '6'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
110
|
+
version: '6'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: sqlite3
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - "~>"
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: 1.3.0
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - "~>"
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: 1.3.0
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: rbnacl
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
128
114
|
requirements:
|
129
115
|
- - ">="
|
@@ -137,7 +123,7 @@ dependencies:
|
|
137
123
|
- !ruby/object:Gem::Version
|
138
124
|
version: '0'
|
139
125
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
126
|
+
name: benchmark-ips
|
141
127
|
requirement: !ruby/object:Gem::Requirement
|
142
128
|
requirements:
|
143
129
|
- - ">="
|
@@ -165,6 +151,8 @@ files:
|
|
165
151
|
- lib/lockbox/box.rb
|
166
152
|
- lib/lockbox/carrier_wave_extensions.rb
|
167
153
|
- lib/lockbox/encryptor.rb
|
154
|
+
- lib/lockbox/key_generator.rb
|
155
|
+
- lib/lockbox/model.rb
|
168
156
|
- lib/lockbox/railtie.rb
|
169
157
|
- lib/lockbox/utils.rb
|
170
158
|
- lib/lockbox/version.rb
|
@@ -180,16 +168,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
180
168
|
requirements:
|
181
169
|
- - ">="
|
182
170
|
- !ruby/object:Gem::Version
|
183
|
-
version: '2.
|
171
|
+
version: '2.4'
|
184
172
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
185
173
|
requirements:
|
186
174
|
- - ">="
|
187
175
|
- !ruby/object:Gem::Version
|
188
176
|
version: '0'
|
189
177
|
requirements: []
|
190
|
-
|
191
|
-
rubygems_version: 2.7.6
|
178
|
+
rubygems_version: 3.0.4
|
192
179
|
signing_key:
|
193
180
|
specification_version: 4
|
194
|
-
summary:
|
181
|
+
summary: Modern encryption for Rails
|
195
182
|
test_files: []
|