lockbox 0.3.1 → 0.3.2
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 +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +214 -90
- data/lib/generators/lockbox/audits_generator.rb +32 -0
- data/lib/generators/lockbox/templates/migration.rb.tt +12 -0
- data/lib/generators/lockbox/templates/model.rb.tt +6 -0
- data/lib/lockbox.rb +10 -3
- data/lib/lockbox/encryptor.rb +5 -1
- data/lib/lockbox/migrator.rb +91 -23
- data/lib/lockbox/model.rb +1 -4
- data/lib/lockbox/utils.rb +10 -1
- data/lib/lockbox/version.rb +1 -1
- metadata +5 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17af286829bd927b3f3dcf156672a4dbced52dcc083d8920a24230cb89f5104a
|
4
|
+
data.tar.gz: 30ec2e0be697c31933b88930c02a43e5544708fe8f847b813a42bd05cd9354dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2b0cfd5fff48b4907a29d1d67499e3bdb81d29f81c9843fae1f447ecf5dfad5d61bb383877324e6cb35f3aac9987f867f12a4106108fe5bc638f93b6fffe39a
|
7
|
+
data.tar.gz: be8abd51a5f0b8cea3d6cbd18d75bf1248333b9a575a5088b3d36c428fa1103f1645b5ca6b94bbf8dfcf2e9b1d3c31a14bd6f06fe9b9da8c26022bbb78e428c7
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 0.3.2 (2020-02-14)
|
2
|
+
|
3
|
+
- Added `encode` option to `Lockbox::Encryptor`
|
4
|
+
- Added support for `master_key` in `previous_versions`
|
5
|
+
- Added `Lockbox.rotate` method
|
6
|
+
- Improved performance of `migrate` method
|
7
|
+
- Added generator for audits
|
8
|
+
|
1
9
|
## 0.3.1 (2019-12-26)
|
2
10
|
|
3
11
|
- Fixed encoding for `encrypt_io` and `decrypt_io` in Ruby 2.7
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -4,11 +4,11 @@
|
|
4
4
|
|
5
5
|
- Uses state-of-the-art algorithms
|
6
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 (with the option to have more)
|
9
7
|
- Makes migrating existing data and key rotation easy
|
10
8
|
|
11
|
-
|
9
|
+
Lockbox aims to make encryption as friendly and intuitive as possible. Encrypted fields and files behave just like unencrypted ones for maximum compatibility with 3rd party libraries and existing code.
|
10
|
+
|
11
|
+
Learn [the principles behind it](https://ankane.org/modern-encryption-rails), [how to secure emails with Devise](https://ankane.org/securing-user-emails-lockbox), and [how to secure sensitive data in Rails](https://ankane.org/sensitive-data-rails).
|
12
12
|
|
13
13
|
[](https://travis-ci.org/ankane/lockbox)
|
14
14
|
|
@@ -22,7 +22,7 @@ gem 'lockbox'
|
|
22
22
|
|
23
23
|
## Key Generation
|
24
24
|
|
25
|
-
Generate
|
25
|
+
Generate a key
|
26
26
|
|
27
27
|
```ruby
|
28
28
|
Lockbox.generate_key
|
@@ -42,29 +42,25 @@ or create `config/initializers/lockbox.rb` with something like
|
|
42
42
|
Lockbox.master_key = Rails.application.credentials.lockbox_master_key
|
43
43
|
```
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
## Instructions
|
45
|
+
Then follow the instructions below for the data you want to encrypt.
|
48
46
|
|
49
|
-
Database
|
47
|
+
#### Database Fields
|
50
48
|
|
51
49
|
- [Active Record](#active-record)
|
52
50
|
- [Mongoid](#mongoid)
|
53
51
|
|
54
|
-
Files
|
52
|
+
#### Files
|
55
53
|
|
56
54
|
- [Active Storage](#active-storage)
|
57
55
|
- [CarrierWave](#carrierwave)
|
58
56
|
- [Shrine](#shrine)
|
59
57
|
- [Local Files](#local-files)
|
60
58
|
|
61
|
-
Other
|
59
|
+
#### Other
|
62
60
|
|
63
61
|
- [Strings](#strings)
|
64
62
|
|
65
|
-
##
|
66
|
-
|
67
|
-
### Active Record
|
63
|
+
## Active Record
|
68
64
|
|
69
65
|
Create a migration with:
|
70
66
|
|
@@ -94,7 +90,7 @@ If you need to query encrypted fields, check out [Blind Index](https://github.co
|
|
94
90
|
|
95
91
|
#### Types
|
96
92
|
|
97
|
-
Specify the type of a field with:
|
93
|
+
Fields are strings by default. Specify the type of a field with:
|
98
94
|
|
99
95
|
```ruby
|
100
96
|
class User < ApplicationRecord
|
@@ -110,7 +106,7 @@ class User < ApplicationRecord
|
|
110
106
|
end
|
111
107
|
```
|
112
108
|
|
113
|
-
**Note:**
|
109
|
+
**Note:** Use a `text` column for the ciphertext in migrations, regardless of the type
|
114
110
|
|
115
111
|
Lockbox automatically works with serialized fields for maximum compatibility with existing code and libraries.
|
116
112
|
|
@@ -140,7 +136,55 @@ end
|
|
140
136
|
|
141
137
|
Validations work as expected with the exception of uniqueness. Uniqueness validations require a [blind index](https://github.com/ankane/blind_index).
|
142
138
|
|
143
|
-
|
139
|
+
#### Fixtures
|
140
|
+
|
141
|
+
You can use encrypted attributes in fixtures with:
|
142
|
+
|
143
|
+
```yml
|
144
|
+
test_user:
|
145
|
+
email_ciphertext: <%= User.generate_email_ciphertext("secret").inspect %>
|
146
|
+
```
|
147
|
+
|
148
|
+
Be sure to include the `inspect` at the end or it won’t be encoded properly in YAML.
|
149
|
+
|
150
|
+
#### Migrating Existing Data
|
151
|
+
|
152
|
+
Lockbox makes it easy to encrypt an existing column. Add a new column for the ciphertext, then add to your model:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class User < ApplicationRecord
|
156
|
+
encrypts :email, migrating: true
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
Backfill the data in the Rails console:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
Lockbox.migrate(User)
|
164
|
+
```
|
165
|
+
|
166
|
+
Then update the model to the desired state:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class User < ApplicationRecord
|
170
|
+
encrypts :email
|
171
|
+
|
172
|
+
# remove this line after dropping email column
|
173
|
+
self.ignored_columns = ["email"]
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
Finally, drop the unencrypted column.
|
178
|
+
|
179
|
+
If adding blind indexes, Lockbox can migrate them at the same time.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class User < ApplicationRecord
|
183
|
+
blind_index :email, migrating: true
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
## Mongoid
|
144
188
|
|
145
189
|
Add to your model:
|
146
190
|
|
@@ -160,9 +204,7 @@ User.create!(email: "hi@example.org")
|
|
160
204
|
|
161
205
|
If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
|
162
206
|
|
163
|
-
##
|
164
|
-
|
165
|
-
### Active Storage
|
207
|
+
## Active Storage
|
166
208
|
|
167
209
|
Add to your model:
|
168
210
|
|
@@ -191,11 +233,12 @@ To serve encrypted files, use a controller action.
|
|
191
233
|
|
192
234
|
```ruby
|
193
235
|
def license
|
194
|
-
|
236
|
+
user = User.find(params[:id])
|
237
|
+
send_data user.license.download, type: user.license.content_type
|
195
238
|
end
|
196
239
|
```
|
197
240
|
|
198
|
-
|
241
|
+
## CarrierWave
|
199
242
|
|
200
243
|
Add to your uploader:
|
201
244
|
|
@@ -207,50 +250,76 @@ end
|
|
207
250
|
|
208
251
|
Encryption is applied to all versions after processing.
|
209
252
|
|
253
|
+
You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
class AddLicenseToUsers < ActiveRecord::Migration[6.0]
|
257
|
+
def change
|
258
|
+
add_column :users, :license, :string
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
And updating the model:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class User < ApplicationRecord
|
267
|
+
mount_uploader :license, LicenseUploader
|
268
|
+
end
|
269
|
+
```
|
270
|
+
|
210
271
|
To serve encrypted files, use a controller action.
|
211
272
|
|
212
273
|
```ruby
|
213
274
|
def license
|
214
|
-
|
275
|
+
user = User.find(params[:id])
|
276
|
+
send_data user.license.read, type: user.license.content_type
|
215
277
|
end
|
216
278
|
```
|
217
279
|
|
218
|
-
|
280
|
+
## Shrine
|
219
281
|
|
220
|
-
|
282
|
+
Generate a key
|
221
283
|
|
222
284
|
```ruby
|
223
|
-
|
285
|
+
key = Lockbox.generate_key
|
286
|
+
```
|
287
|
+
|
288
|
+
Create a lockbox
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
lockbox = Lockbox.new(key: key)
|
224
292
|
```
|
225
293
|
|
226
294
|
Encrypt files before passing them to Shrine
|
227
295
|
|
228
296
|
```ruby
|
229
|
-
LicenseUploader.upload(
|
297
|
+
LicenseUploader.upload(lockbox.encrypt_io(file), :store)
|
230
298
|
```
|
231
299
|
|
232
300
|
And decrypt them after reading
|
233
301
|
|
234
302
|
```ruby
|
235
|
-
|
303
|
+
lockbox.decrypt(uploaded_file.read)
|
236
304
|
```
|
237
305
|
|
238
306
|
For models, encrypt with:
|
239
307
|
|
240
308
|
```ruby
|
241
309
|
license = params.require(:user).fetch(:license)
|
242
|
-
|
310
|
+
user.license = lockbox.encrypt_io(license)
|
243
311
|
```
|
244
312
|
|
245
313
|
To serve encrypted files, use a controller action.
|
246
314
|
|
247
315
|
```ruby
|
248
316
|
def license
|
249
|
-
|
317
|
+
user = User.find(params[:id])
|
318
|
+
send_data box.decrypt(user.license.read), type: user.license.mime_type
|
250
319
|
end
|
251
320
|
```
|
252
321
|
|
253
|
-
|
322
|
+
## Local Files
|
254
323
|
|
255
324
|
Read the file as a binary string
|
256
325
|
|
@@ -262,58 +331,31 @@ Then follow the instructions for encrypting a string below.
|
|
262
331
|
|
263
332
|
## Strings
|
264
333
|
|
265
|
-
|
266
|
-
|
267
|
-
```ruby
|
268
|
-
box = Lockbox.new(key: key)
|
269
|
-
```
|
270
|
-
|
271
|
-
Encrypt
|
272
|
-
|
273
|
-
```ruby
|
274
|
-
ciphertext = box.encrypt(message)
|
275
|
-
```
|
276
|
-
|
277
|
-
Decrypt
|
334
|
+
Generate a key
|
278
335
|
|
279
336
|
```ruby
|
280
|
-
|
337
|
+
key = Lockbox.generate_key
|
281
338
|
```
|
282
339
|
|
283
|
-
|
340
|
+
Create a lockbox
|
284
341
|
|
285
342
|
```ruby
|
286
|
-
|
343
|
+
lockbox = Lockbox.new(key: key, encode: true)
|
287
344
|
```
|
288
345
|
|
289
|
-
|
290
|
-
|
291
|
-
Lockbox makes it easy to encrypt an existing column. Add a new column for the ciphertext, then add to your model:
|
346
|
+
Encrypt
|
292
347
|
|
293
348
|
```ruby
|
294
|
-
|
295
|
-
encrypts :email, migrating: true
|
296
|
-
end
|
349
|
+
ciphertext = lockbox.encrypt("hello")
|
297
350
|
```
|
298
351
|
|
299
|
-
|
352
|
+
Decrypt
|
300
353
|
|
301
354
|
```ruby
|
302
|
-
|
355
|
+
lockbox.decrypt(ciphertext)
|
303
356
|
```
|
304
357
|
|
305
|
-
|
306
|
-
|
307
|
-
```ruby
|
308
|
-
class User < ApplicationRecord
|
309
|
-
encrypts :email
|
310
|
-
|
311
|
-
# remove this line after dropping email column
|
312
|
-
self.ignored_columns = ["email"]
|
313
|
-
end
|
314
|
-
```
|
315
|
-
|
316
|
-
Finally, drop the unencrypted column.
|
358
|
+
Use `decrypt_str` get the value as UTF-8
|
317
359
|
|
318
360
|
## Key Rotation
|
319
361
|
|
@@ -321,7 +363,7 @@ To make key rotation easy, you can pass previous versions of keys that can decry
|
|
321
363
|
|
322
364
|
### Active Record
|
323
365
|
|
324
|
-
|
366
|
+
Update your model:
|
325
367
|
|
326
368
|
```ruby
|
327
369
|
class User < ApplicationRecord
|
@@ -329,15 +371,19 @@ class User < ApplicationRecord
|
|
329
371
|
end
|
330
372
|
```
|
331
373
|
|
332
|
-
|
374
|
+
Use `master_key` instead of `key` if passing the master key.
|
375
|
+
|
376
|
+
To rotate existing records, use:
|
333
377
|
|
334
378
|
```ruby
|
335
|
-
|
379
|
+
Lockbox.rotate(User, attributes: [:email])
|
336
380
|
```
|
337
381
|
|
382
|
+
Once all records are rotated, you can remove `previous_versions` from the model.
|
383
|
+
|
338
384
|
### Mongoid
|
339
385
|
|
340
|
-
|
386
|
+
Update your model:
|
341
387
|
|
342
388
|
```ruby
|
343
389
|
class User
|
@@ -345,15 +391,19 @@ class User
|
|
345
391
|
end
|
346
392
|
```
|
347
393
|
|
348
|
-
|
394
|
+
Use `master_key` instead of `key` if passing the master key.
|
395
|
+
|
396
|
+
To rotate existing records, use:
|
349
397
|
|
350
398
|
```ruby
|
351
|
-
|
399
|
+
Lockbox.rotate(User, attributes: [:email])
|
352
400
|
```
|
353
401
|
|
402
|
+
Once all records are rotated, you can remove `previous_versions` from the model.
|
403
|
+
|
354
404
|
### Active Storage
|
355
405
|
|
356
|
-
|
406
|
+
Update your model:
|
357
407
|
|
358
408
|
```ruby
|
359
409
|
class User < ApplicationRecord
|
@@ -361,15 +411,21 @@ class User < ApplicationRecord
|
|
361
411
|
end
|
362
412
|
```
|
363
413
|
|
414
|
+
Use `master_key` instead of `key` if passing the master key.
|
415
|
+
|
364
416
|
To rotate existing files, use:
|
365
417
|
|
366
418
|
```ruby
|
367
|
-
user
|
419
|
+
User.find_each do |user|
|
420
|
+
user.license.rotate_encryption!
|
421
|
+
end
|
368
422
|
```
|
369
423
|
|
424
|
+
Once all files are rotated, you can remove `previous_versions` from the model.
|
425
|
+
|
370
426
|
### CarrierWave
|
371
427
|
|
372
|
-
|
428
|
+
Update your model:
|
373
429
|
|
374
430
|
```ruby
|
375
431
|
class LicenseUploader < CarrierWave::Uploader::Base
|
@@ -377,12 +433,18 @@ class LicenseUploader < CarrierWave::Uploader::Base
|
|
377
433
|
end
|
378
434
|
```
|
379
435
|
|
436
|
+
Use `master_key` instead of `key` if passing the master key.
|
437
|
+
|
380
438
|
To rotate existing files, use:
|
381
439
|
|
382
440
|
```ruby
|
383
|
-
user
|
441
|
+
User.find_each do |user|
|
442
|
+
user.license.rotate_encryption!
|
443
|
+
end
|
384
444
|
```
|
385
445
|
|
446
|
+
Once all files are rotated, you can remove `previous_versions` from the model.
|
447
|
+
|
386
448
|
### Strings
|
387
449
|
|
388
450
|
For strings, use:
|
@@ -391,22 +453,53 @@ For strings, use:
|
|
391
453
|
Lockbox.new(key: key, previous_versions: [{key: previous_key}])
|
392
454
|
```
|
393
455
|
|
394
|
-
##
|
456
|
+
## Auditing
|
395
457
|
|
396
|
-
|
458
|
+
It’s a good idea to track user and employee access to sensitive data. Lockbox provides a convenient way to do this with Active Record, but you can use a similar pattern to write audits to any location.
|
397
459
|
|
398
|
-
```
|
399
|
-
|
400
|
-
|
460
|
+
```sh
|
461
|
+
rails generate lockbox:audits
|
462
|
+
rails db:migrate
|
401
463
|
```
|
402
464
|
|
403
|
-
|
465
|
+
Then create an audit wherever a user can view data:
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
class UsersController < ApplicationController
|
469
|
+
def show
|
470
|
+
@user = User.find(params[:id])
|
471
|
+
|
472
|
+
LockboxAudit.create!(
|
473
|
+
subject: @user,
|
474
|
+
viewer: current_user,
|
475
|
+
data: ["email", "dob"],
|
476
|
+
context: "#{controller_name}##{action_name}",
|
477
|
+
ip: request.remote_ip
|
478
|
+
)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
```
|
482
|
+
|
483
|
+
Query audits with:
|
484
|
+
|
485
|
+
```ruby
|
486
|
+
LockboxAudit.last(100)
|
487
|
+
```
|
488
|
+
|
489
|
+
**Note:** This approach is not intended to be used in the event of a breach or insider attack, as it’s trivial for someone with access to your infrastructure to bypass.
|
404
490
|
|
405
491
|
## Algorithms
|
406
492
|
|
407
493
|
### AES-GCM
|
408
494
|
|
409
|
-
This is the default algorithm.
|
495
|
+
This is the default algorithm. It’s:
|
496
|
+
|
497
|
+
- well-studied
|
498
|
+
- NIST recommended
|
499
|
+
- an IETF standard
|
500
|
+
- fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
|
501
|
+
|
502
|
+
**For users who do a lot of encryptions:** You should rotate an individual key after 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. Each database field and file uploader use a different key (derived from the master key) to extend this window.
|
410
503
|
|
411
504
|
### XSalsa20
|
412
505
|
|
@@ -550,29 +643,54 @@ end
|
|
550
643
|
|
551
644
|
**Note:** KMS Encrypted’s key rotation does not know to rotate encrypted files, so avoid calling `record.rotate_kms_key!` on models with file uploads for now.
|
552
645
|
|
553
|
-
##
|
646
|
+
## Data Leakage
|
647
|
+
|
648
|
+
While encryption hides the content of a message, an attacker can still get the length of the message (since the length of the ciphertext is the length of the message plus a constant number of bytes).
|
649
|
+
|
650
|
+
Let’s say you want to encrypt the status of a candidate’s background check. Valid statuses are `clear`, `consider`, and `fail`. Even with the data encrypted, it’s trivial to map the ciphertext to a status.
|
651
|
+
|
652
|
+
```ruby
|
653
|
+
lockbox = Lockbox.new(key: key)
|
654
|
+
lockbox.encrypt("fail").bytesize # 32
|
655
|
+
lockbox.encrypt("clear").bytesize # 33
|
656
|
+
lockbox.encrypt("consider").bytesize # 36
|
657
|
+
```
|
554
658
|
|
555
659
|
Add padding to conceal the exact length of messages.
|
556
660
|
|
557
661
|
```ruby
|
558
|
-
Lockbox.new(padding: true)
|
662
|
+
lockbox = Lockbox.new(key: key, padding: true)
|
663
|
+
lockbox.encrypt("fail").bytesize # 44
|
664
|
+
lockbox.encrypt("clear").bytesize # 44
|
665
|
+
lockbox.encrypt("consider").bytesize # 44
|
666
|
+
```
|
667
|
+
|
668
|
+
The block size for padding is 16 bytes by default. If we have a status larger than 15 bytes, it will have a different length than the others.
|
669
|
+
|
670
|
+
```ruby
|
671
|
+
box.encrypt("length15status!").bytesize # 44
|
672
|
+
box.encrypt("length16status!!").bytesize # 60
|
559
673
|
```
|
560
674
|
|
561
|
-
|
675
|
+
Change the block size with:
|
562
676
|
|
563
677
|
```ruby
|
564
678
|
Lockbox.new(padding: 32) # bytes
|
565
679
|
```
|
566
680
|
|
567
|
-
##
|
681
|
+
## Binary Columns
|
568
682
|
|
569
|
-
|
683
|
+
You can use `binary` columns for the ciphertext instead of `text` columns to save space.
|
570
684
|
|
571
685
|
```ruby
|
572
|
-
|
686
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
|
687
|
+
def change
|
688
|
+
add_column :users, :email_ciphertext, :binary
|
689
|
+
end
|
690
|
+
end
|
573
691
|
```
|
574
692
|
|
575
|
-
|
693
|
+
You should disable Base64 encoding if you do this.
|
576
694
|
|
577
695
|
```ruby
|
578
696
|
class User < ApplicationRecord
|
@@ -580,6 +698,12 @@ class User < ApplicationRecord
|
|
580
698
|
end
|
581
699
|
```
|
582
700
|
|
701
|
+
or set it globally:
|
702
|
+
|
703
|
+
```ruby
|
704
|
+
Lockbox.default_options = {encode: false}
|
705
|
+
```
|
706
|
+
|
583
707
|
## Compatibility
|
584
708
|
|
585
709
|
It’s easy to read encrypted data in another language if needed.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "rails/generators/active_record"
|
2
|
+
|
3
|
+
module Lockbox
|
4
|
+
module Generators
|
5
|
+
class AuditsGenerator < Rails::Generators::Base
|
6
|
+
include ActiveRecord::Generators::Migration
|
7
|
+
source_root File.join(__dir__, "templates")
|
8
|
+
|
9
|
+
def copy_migration
|
10
|
+
migration_template "migration.rb", "db/migrate/create_lockbox_audits.rb", migration_version: migration_version
|
11
|
+
template "model.rb", "app/models/lockbox_audit.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def migration_version
|
15
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
16
|
+
end
|
17
|
+
|
18
|
+
def data_type
|
19
|
+
# use connection_config instead of connection.adapter
|
20
|
+
# so database connection isn't needed
|
21
|
+
case ActiveRecord::Base.connection_config[:adapter].to_s
|
22
|
+
when /postg/i # postgres, postgis
|
23
|
+
"jsonb"
|
24
|
+
when /mysql/i
|
25
|
+
"json"
|
26
|
+
else
|
27
|
+
"text"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :lockbox_audits do |t|
|
4
|
+
t.references :subject, polymorphic: true
|
5
|
+
t.references :viewer, polymorphic: true
|
6
|
+
t.<%= data_type %> :data
|
7
|
+
t.string :context
|
8
|
+
t.string :ip
|
9
|
+
t.datetime :created_at
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/lockbox.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# stdlib
|
2
|
+
require "base64"
|
2
3
|
require "openssl"
|
3
4
|
require "securerandom"
|
4
5
|
|
@@ -35,6 +36,8 @@ module Lockbox
|
|
35
36
|
class DecryptionError < Error; end
|
36
37
|
class PaddingError < Error; end
|
37
38
|
|
39
|
+
autoload :Audit, "lockbox/audit"
|
40
|
+
|
38
41
|
extend Padding
|
39
42
|
|
40
43
|
class << self
|
@@ -47,8 +50,12 @@ module Lockbox
|
|
47
50
|
@master_key ||= ENV["LOCKBOX_MASTER_KEY"]
|
48
51
|
end
|
49
52
|
|
50
|
-
def self.migrate(
|
51
|
-
Migrator.new(
|
53
|
+
def self.migrate(relation, batch_size: 1000, restart: false)
|
54
|
+
Migrator.new(relation, batch_size: batch_size).migrate(restart: restart)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.rotate(relation, batch_size: 1000, attributes:)
|
58
|
+
Migrator.new(relation, batch_size: batch_size).rotate(attributes: attributes)
|
52
59
|
end
|
53
60
|
|
54
61
|
def self.generate_key
|
data/lib/lockbox/encryptor.rb
CHANGED
@@ -2,6 +2,7 @@ module Lockbox
|
|
2
2
|
class Encryptor
|
3
3
|
def initialize(**options)
|
4
4
|
options = Lockbox.default_options.merge(options)
|
5
|
+
@encode = options.delete(:encode)
|
5
6
|
previous_versions = options.delete(:previous_versions)
|
6
7
|
|
7
8
|
@boxes =
|
@@ -11,10 +12,13 @@ module Lockbox
|
|
11
12
|
|
12
13
|
def encrypt(message, **options)
|
13
14
|
message = check_string(message, "message")
|
14
|
-
@boxes.first.encrypt(message, **options)
|
15
|
+
ciphertext = @boxes.first.encrypt(message, **options)
|
16
|
+
ciphertext = Base64.strict_encode64(ciphertext) if @encode
|
17
|
+
ciphertext
|
15
18
|
end
|
16
19
|
|
17
20
|
def decrypt(ciphertext, **options)
|
21
|
+
ciphertext = Base64.decode64(ciphertext) if @encode
|
18
22
|
ciphertext = check_string(ciphertext, "ciphertext")
|
19
23
|
|
20
24
|
# ensure binary
|
data/lib/lockbox/migrator.rb
CHANGED
@@ -1,58 +1,126 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Migrator
|
3
|
-
def initialize(
|
4
|
-
@
|
3
|
+
def initialize(relation, batch_size:)
|
4
|
+
@relation = relation
|
5
|
+
@transaction = @relation.respond_to?(:transaction)
|
6
|
+
@batch_size = batch_size
|
5
7
|
end
|
6
8
|
|
7
|
-
def
|
8
|
-
model
|
9
|
+
def model
|
10
|
+
@model ||= @relation
|
11
|
+
end
|
12
|
+
|
13
|
+
def rotate(attributes:)
|
14
|
+
fields = {}
|
15
|
+
attributes.each do |a|
|
16
|
+
# use key instad of v[:attribute] to make it more intuitive when migrating: true
|
17
|
+
field = model.lockbox_attributes[a]
|
18
|
+
raise ArgumentError, "Bad attribute: #{a}" unless field
|
19
|
+
fields[a] = field
|
20
|
+
end
|
9
21
|
|
10
|
-
|
22
|
+
perform(fields: fields)
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO add attributes option
|
26
|
+
def migrate(restart:)
|
11
27
|
fields = model.lockbox_attributes.select { |k, v| v[:migrating] }
|
12
28
|
|
13
|
-
# get blind indexes
|
14
29
|
blind_indexes = model.respond_to?(:blind_indexes) ? model.blind_indexes.select { |k, v| v[:migrating] } : {}
|
15
30
|
|
16
|
-
|
17
|
-
|
31
|
+
perform(fields: fields, blind_indexes: blind_indexes, restart: restart)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def perform(fields:, blind_indexes: [], restart: true)
|
37
|
+
relation = @relation
|
38
|
+
|
39
|
+
# remove true condition in 0.4.0
|
40
|
+
if true || (defined?(ActiveRecord::Base) && base_relation.is_a?(ActiveRecord::Base))
|
41
|
+
relation = relation.unscoped
|
42
|
+
end
|
43
|
+
|
44
|
+
# convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
|
45
|
+
relation = relation.all
|
18
46
|
|
19
47
|
unless restart
|
20
48
|
attributes = fields.map { |_, v| v[:encrypted_attribute] }
|
21
49
|
attributes += blind_indexes.map { |_, v| v[:bidx_attribute] }
|
22
50
|
|
23
|
-
if defined?(ActiveRecord::
|
51
|
+
if defined?(ActiveRecord::Relation) && relation.is_a?(ActiveRecord::Relation)
|
52
|
+
base_relation = relation.unscoped
|
53
|
+
or_relation = relation.unscoped
|
54
|
+
|
24
55
|
attributes.each_with_index do |attribute, i|
|
25
|
-
|
56
|
+
or_relation =
|
26
57
|
if i == 0
|
27
|
-
|
58
|
+
base_relation.where(attribute => nil)
|
28
59
|
else
|
29
|
-
|
60
|
+
or_relation.or(base_relation.where(attribute => nil))
|
30
61
|
end
|
31
62
|
end
|
63
|
+
|
64
|
+
relation = relation.merge(or_relation)
|
65
|
+
else
|
66
|
+
relation = relation.or(attributes.map { |a| {a => nil} })
|
32
67
|
end
|
33
68
|
end
|
34
69
|
|
35
|
-
|
36
|
-
|
37
|
-
|
70
|
+
each_batch(relation) do |records|
|
71
|
+
migrate_records(records, fields: fields, blind_indexes: blind_indexes, restart: restart)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def each_batch(relation)
|
76
|
+
if relation.respond_to?(:find_in_batches)
|
77
|
+
relation.find_in_batches(batch_size: @batch_size) do |records|
|
78
|
+
yield records
|
38
79
|
end
|
39
80
|
else
|
81
|
+
# https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
|
82
|
+
# use cursor for Mongoid
|
83
|
+
records = []
|
40
84
|
relation.all.each do |record|
|
41
|
-
|
85
|
+
records << record
|
86
|
+
if records.length == @batch_size
|
87
|
+
yield records
|
88
|
+
records = []
|
89
|
+
end
|
42
90
|
end
|
91
|
+
yield records if records.any?
|
43
92
|
end
|
44
93
|
end
|
45
94
|
|
46
|
-
|
95
|
+
def migrate_records(records, fields:, blind_indexes:, restart:)
|
96
|
+
# do computation outside of transaction
|
97
|
+
# especially expensive blind index computation
|
98
|
+
records.each do |record|
|
99
|
+
fields.each do |k, v|
|
100
|
+
record.send("#{v[:attribute]}=", record.send(k)) if restart || !record.send(v[:encrypted_attribute])
|
101
|
+
end
|
102
|
+
blind_indexes.each do |k, v|
|
103
|
+
record.send("compute_#{k}_bidx") if restart || !record.send(v[:bidx_attribute])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
records.select! { |r| r.changed? }
|
47
108
|
|
48
|
-
|
49
|
-
|
50
|
-
|
109
|
+
with_transaction do
|
110
|
+
records.each do |record|
|
111
|
+
record.save!(validate: false)
|
112
|
+
end
|
51
113
|
end
|
52
|
-
|
53
|
-
|
114
|
+
end
|
115
|
+
|
116
|
+
def with_transaction
|
117
|
+
if @transaction
|
118
|
+
@relation.transaction do
|
119
|
+
yield
|
120
|
+
end
|
121
|
+
else
|
122
|
+
yield
|
54
123
|
end
|
55
|
-
record.save(validate: false) if record.changed?
|
56
124
|
end
|
57
125
|
end
|
58
126
|
end
|
data/lib/lockbox/model.rb
CHANGED
@@ -280,9 +280,7 @@ module Lockbox
|
|
280
280
|
if message.nil? || (message == "" && !options[:padding])
|
281
281
|
message
|
282
282
|
else
|
283
|
-
|
284
|
-
ciphertext = Base64.strict_encode64(ciphertext) if options[:encode]
|
285
|
-
ciphertext
|
283
|
+
Lockbox::Utils.build_box(opts[:context], options, table, encrypted_attribute).encrypt(message)
|
286
284
|
end
|
287
285
|
end
|
288
286
|
|
@@ -291,7 +289,6 @@ module Lockbox
|
|
291
289
|
if ciphertext.nil? || (ciphertext == "" && !options[:padding])
|
292
290
|
ciphertext
|
293
291
|
else
|
294
|
-
ciphertext = Base64.decode64(ciphertext) if options[:encode]
|
295
292
|
table = activerecord ? table_name : collection_name.to_s
|
296
293
|
Lockbox::Utils.build_box(opts[:context], options, table, encrypted_attribute).decrypt(ciphertext)
|
297
294
|
end
|
data/lib/lockbox/utils.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Utils
|
3
3
|
def self.build_box(context, options, table, attribute)
|
4
|
-
options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type
|
4
|
+
options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type)
|
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)
|
@@ -14,6 +14,15 @@ module Lockbox
|
|
14
14
|
options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
|
15
15
|
end
|
16
16
|
|
17
|
+
if options[:previous_versions].is_a?(Array)
|
18
|
+
options[:previous_versions] = options[:previous_versions].dup
|
19
|
+
options[:previous_versions].each_with_index do |version, i|
|
20
|
+
if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
|
21
|
+
options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key)))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
17
26
|
Lockbox.new(**options)
|
18
27
|
end
|
19
28
|
|
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.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -164,20 +164,6 @@ dependencies:
|
|
164
164
|
- - ">="
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
|
-
- !ruby/object:Gem::Dependency
|
168
|
-
name: mongoid
|
169
|
-
requirement: !ruby/object:Gem::Requirement
|
170
|
-
requirements:
|
171
|
-
- - ">="
|
172
|
-
- !ruby/object:Gem::Version
|
173
|
-
version: '0'
|
174
|
-
type: :development
|
175
|
-
prerelease: false
|
176
|
-
version_requirements: !ruby/object:Gem::Requirement
|
177
|
-
requirements:
|
178
|
-
- - ">="
|
179
|
-
- !ruby/object:Gem::Version
|
180
|
-
version: '0'
|
181
167
|
description:
|
182
168
|
email: andrew@chartkick.com
|
183
169
|
executables: []
|
@@ -187,6 +173,9 @@ files:
|
|
187
173
|
- CHANGELOG.md
|
188
174
|
- LICENSE.txt
|
189
175
|
- README.md
|
176
|
+
- lib/generators/lockbox/audits_generator.rb
|
177
|
+
- lib/generators/lockbox/templates/migration.rb.tt
|
178
|
+
- lib/generators/lockbox/templates/model.rb.tt
|
190
179
|
- lib/lockbox.rb
|
191
180
|
- lib/lockbox/active_storage_extensions.rb
|
192
181
|
- lib/lockbox/aes_gcm.rb
|