lockbox 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/ankane/lockbox.svg?branch=master)](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: []
|