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