attr_keyring 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +1 -0
- data/Gemfile.lock +14 -8
- data/README.md +64 -50
- data/Rakefile +1 -0
- data/attr_keyring.gemspec +4 -2
- data/examples/active_record_sample.rb +85 -0
- data/examples/keyring_sample.rb +18 -0
- data/examples/sequel_sample.rb +83 -0
- data/lib/attr_keyring.rb +106 -25
- data/lib/attr_keyring/active_record.rb +18 -75
- data/lib/attr_keyring/sequel.rb +21 -0
- data/lib/attr_keyring/version.rb +1 -1
- data/lib/keyring.rb +61 -0
- data/lib/keyring/encryptor/aes.rb +57 -0
- data/lib/{attr_keyring → keyring}/key.rb +1 -1
- metadata +40 -10
- data/lib/attr_keyring/encryptor/aes.rb +0 -32
- data/lib/attr_keyring/encryptor/aes_128_cbc.rb +0 -9
- data/lib/attr_keyring/encryptor/aes_256_cbc.rb +0 -9
- data/lib/attr_keyring/keyring.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b5e38839a79e65282d4964fefb09e9c9c53cf1cf4d85852c0a8405d89928074
|
4
|
+
data.tar.gz: 9a667ea4f855d5affb9f4612dbfdaa40d8330f4803d59fc9004c61062fce84cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23229cced01974f35114c55a2d725b9e112f2021c0d3ddffe59cd8d64b25ffc84bf56131276941a879fe7970e414704d6ce4af5f2f5407205603fdb5cef596fd
|
7
|
+
data.tar.gz: 372331b1209502523de2e3282d6af0b0ff888f514c11762f4c0a270ff4b057a4aca952b7f2e8507a6fef04bd8cb24edc420a7f46a73f183afaaae1be1fbf7026
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
attr_keyring (0.
|
5
|
-
activerecord
|
4
|
+
attr_keyring (0.4.0)
|
6
5
|
|
7
6
|
GEM
|
8
7
|
remote: https://rubygems.org/
|
@@ -23,19 +22,23 @@ GEM
|
|
23
22
|
awesome_print (1.8.0)
|
24
23
|
byebug (10.0.2)
|
25
24
|
coderay (1.1.2)
|
26
|
-
concurrent-ruby (1.1.
|
25
|
+
concurrent-ruby (1.1.4)
|
27
26
|
docile (1.3.1)
|
28
|
-
i18n (1.
|
27
|
+
i18n (1.2.0)
|
29
28
|
concurrent-ruby (~> 1.0)
|
30
29
|
jaro_winkler (1.5.1)
|
31
30
|
json (2.1.0)
|
31
|
+
metaclass (0.0.4)
|
32
32
|
method_source (0.9.2)
|
33
33
|
minitest (5.11.3)
|
34
34
|
minitest-utils (0.4.4)
|
35
35
|
minitest
|
36
|
+
mocha (1.7.0)
|
37
|
+
metaclass (~> 0.0.1)
|
36
38
|
parallel (1.12.1)
|
37
39
|
parser (2.5.3.0)
|
38
40
|
ast (~> 2.4.0)
|
41
|
+
pg (1.1.3)
|
39
42
|
powerpack (0.1.2)
|
40
43
|
pry (0.12.2)
|
41
44
|
coderay (~> 1.1.0)
|
@@ -52,8 +55,8 @@ GEM
|
|
52
55
|
pry (~> 0.9)
|
53
56
|
slop (~> 3.0)
|
54
57
|
rainbow (3.0.0)
|
55
|
-
rake (12.3.
|
56
|
-
rubocop (0.
|
58
|
+
rake (12.3.2)
|
59
|
+
rubocop (0.61.1)
|
57
60
|
jaro_winkler (~> 1.5.1)
|
58
61
|
parallel (~> 1.10)
|
59
62
|
parser (>= 2.5, != 2.5.1.1)
|
@@ -62,13 +65,13 @@ GEM
|
|
62
65
|
ruby-progressbar (~> 1.7)
|
63
66
|
unicode-display_width (~> 1.4.0)
|
64
67
|
ruby-progressbar (1.10.0)
|
68
|
+
sequel (5.15.0)
|
65
69
|
simplecov (0.16.1)
|
66
70
|
docile (~> 1.1)
|
67
71
|
json (>= 1.8, < 3)
|
68
72
|
simplecov-html (~> 0.10.0)
|
69
73
|
simplecov-html (0.10.2)
|
70
74
|
slop (3.6.0)
|
71
|
-
sqlite3 (1.3.13)
|
72
75
|
thread_safe (0.3.6)
|
73
76
|
tzinfo (1.2.5)
|
74
77
|
thread_safe (~> 0.1)
|
@@ -78,14 +81,17 @@ PLATFORMS
|
|
78
81
|
ruby
|
79
82
|
|
80
83
|
DEPENDENCIES
|
84
|
+
activerecord
|
81
85
|
attr_keyring!
|
82
86
|
bundler
|
83
87
|
minitest-utils
|
88
|
+
mocha
|
89
|
+
pg
|
84
90
|
pry-meta
|
85
91
|
rake
|
86
92
|
rubocop
|
93
|
+
sequel
|
87
94
|
simplecov
|
88
|
-
sqlite3
|
89
95
|
|
90
96
|
BUNDLED WITH
|
91
97
|
1.17.1
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
![attr_keyring: Simple encryption-at-rest with key rotation support for
|
1
|
+
![attr_keyring: Simple encryption-at-rest with key rotation support for Ruby.](https://raw.githubusercontent.com/fnando/attr_keyring/master/attr_keyring.png)
|
2
2
|
|
3
3
|
<p align="center">
|
4
4
|
<a href="https://travis-ci.org/fnando/attr_keyring"><img src="https://travis-ci.org/fnando/attr_keyring.svg" alt="Travis-CI"></a>
|
@@ -30,45 +30,42 @@ Or install it yourself as:
|
|
30
30
|
|
31
31
|
## Usage
|
32
32
|
|
33
|
-
###
|
33
|
+
### Configuration
|
34
34
|
|
35
|
-
|
35
|
+
As far as database schema goes:
|
36
36
|
|
37
37
|
1. You'll need a column to track the key that was used for encryption; by default it's called `keyring_id`.
|
38
38
|
2. Every encrypted columns must follow the name `encrypted_<column name>`.
|
39
39
|
3. Optionally, you can also have a `<column name>_digest` to help with searching (see Lookup section below).
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
class CreateUsers < ActiveRecord::Migration[5.2]
|
45
|
-
def change
|
46
|
-
create_table :users do |t|
|
47
|
-
t.citext :email, null: false
|
48
|
-
t.timestamps
|
49
|
-
|
50
|
-
# The following columns are used for encryption.
|
51
|
-
t.binary :encrypted_twitter_oauth_token
|
52
|
-
t.binary :encrypted_social_security_number
|
53
|
-
t.text :social_security_number_digest
|
54
|
-
t.integer :keyring_id
|
55
|
-
end
|
56
|
-
|
57
|
-
add_index :users, :email, unique: true
|
58
|
-
add_index :users, :social_security_number_digest, unique: true
|
59
|
-
end
|
60
|
-
end
|
61
|
-
```
|
41
|
+
As far as model configuration goes, they're pretty similar, as you can see below:
|
62
42
|
|
63
43
|
#### ActiveRecord
|
64
44
|
|
45
|
+
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead. This is how you set it up:
|
46
|
+
|
65
47
|
```ruby
|
66
48
|
class ApplicationRecord < ActiveRecord::Base
|
67
49
|
self.abstract_class = true
|
50
|
+
include AttrKeyring.active_record
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
#### Sequel
|
68
55
|
|
69
|
-
|
56
|
+
Sequel doesn't have an abstract model class (but it could), so you can set up the model class directly like the following:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class User < Sequel::Model
|
60
|
+
include AttrKeyring.sequel
|
70
61
|
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Defining encrypted attributes
|
65
|
+
|
66
|
+
To set up your model, you have to define the keyring (set of encryption keys) and the attributes that will be encrypted. Both ActiveRecord and Sequel have the same API, so the examples below work for both ORMs.
|
71
67
|
|
68
|
+
```ruby
|
72
69
|
class User < ApplicationRecord
|
73
70
|
attr_keyring ENV["USER_KEYRING"]
|
74
71
|
attr_encrypt :twitter_oauth_token, :social_security_number
|
@@ -81,30 +78,17 @@ You can use the model as you would normally do.
|
|
81
78
|
|
82
79
|
```ruby
|
83
80
|
user = User.create(
|
84
|
-
email: "john@example.com"
|
85
|
-
twitter_oauth_token: "TOKEN",
|
86
|
-
social_security_number: "SSN"
|
81
|
+
email: "john@example.com"
|
87
82
|
)
|
88
83
|
|
89
|
-
user.
|
90
|
-
#=>
|
84
|
+
user.email
|
85
|
+
#=> john@example.com
|
91
86
|
|
92
87
|
user.keyring_id
|
93
88
|
#=> 1
|
94
89
|
|
95
|
-
user.
|
96
|
-
#=>
|
97
|
-
```
|
98
|
-
|
99
|
-
You may want to store a Base64 version instead of binary data (e.g. `jsonb` column with `store_accessor`). In this case, you may specify the option `encode: true`.
|
100
|
-
|
101
|
-
```ruby
|
102
|
-
class User < ApplicationRecord
|
103
|
-
store_accessor :meta, :twitter_oauth_token
|
104
|
-
|
105
|
-
attr_keyring ENV["USER_KEYRING"]
|
106
|
-
attr_encrypt :twitter_oauth_token, encode: true
|
107
|
-
end
|
90
|
+
user.encrypted_email
|
91
|
+
#=> WG8Epo0ABz0Z1X5gX7kttc98w9Ei59B5uXGK36Zin9G0VqbxX3naOWOm4RI6w6Uu
|
108
92
|
```
|
109
93
|
|
110
94
|
### Encryption
|
@@ -126,11 +110,11 @@ class User < ApplicationRecord
|
|
126
110
|
end
|
127
111
|
```
|
128
112
|
|
129
|
-
|
113
|
+
#### Key size
|
130
114
|
|
131
|
-
|
132
|
-
|
133
|
-
|
115
|
+
- `aes-128-cbc`: 16 bytes.
|
116
|
+
- `aes-192-cbc`: 24 bytes.
|
117
|
+
- `aes-256-cbc`: 32 bytes.
|
134
118
|
|
135
119
|
#### About the encrypted message
|
136
120
|
|
@@ -173,15 +157,15 @@ Other possibilities (e.g. the keyring file is provided by configuration manageme
|
|
173
157
|
One tricky aspect of encryption is looking up records by known secret. E.g.,
|
174
158
|
|
175
159
|
```ruby
|
176
|
-
User.where(
|
160
|
+
User.where(email: "john@example.com")
|
177
161
|
```
|
178
162
|
|
179
163
|
is trivial with plain text fields, but impossible with the model defined as above.
|
180
164
|
|
181
|
-
If
|
165
|
+
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will be saved. This will allow you to lookup by that value instead and add unique indexes.
|
182
166
|
|
183
167
|
```ruby
|
184
|
-
User.where(
|
168
|
+
User.where(email: Digest::SHA1.hexdigest("john@example.com"))
|
185
169
|
```
|
186
170
|
|
187
171
|
### Key Rotation
|
@@ -203,6 +187,36 @@ User.where(keyring_id: 1234).find_each do |user|
|
|
203
187
|
end
|
204
188
|
```
|
205
189
|
|
190
|
+
### What if I don't use ActiveRecord/Sequel?
|
191
|
+
|
192
|
+
You can also leverage the encryption mechanism of `attr_keyring` totally decoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead. Then you can create a keyring to encrypt/decrypt strings, without even touching the database.
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
require "keyring"
|
196
|
+
|
197
|
+
keyring = Keyring.new("1" => "QSXyoiRDPoJmfkJUZ4hJeQ==")
|
198
|
+
|
199
|
+
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
200
|
+
|
201
|
+
puts encrypted
|
202
|
+
#=> encrypted: +mOWmIWKMV01nCm076OBnzgPGhWAZqNs8Etaad/0s3I=
|
203
|
+
|
204
|
+
puts keyring_id
|
205
|
+
#=> 1
|
206
|
+
|
207
|
+
puts digest
|
208
|
+
#=> e24fe0dea7f9abe8cbb192702578715079689a3e
|
209
|
+
|
210
|
+
decrypted = keyring.decrypt(encrypted, keyring_id)
|
211
|
+
|
212
|
+
puts decrypted
|
213
|
+
#=> super secret
|
214
|
+
```
|
215
|
+
|
216
|
+
### Exchange data with Node.js
|
217
|
+
|
218
|
+
If you use Node.js, you may be interested in <https://github.com/fnando/keyring-node>, which is able to read and write messages using the same format.
|
219
|
+
|
206
220
|
## Development
|
207
221
|
|
208
222
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
data/attr_keyring.gemspec
CHANGED
@@ -20,12 +20,14 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.
|
23
|
+
spec.add_development_dependency "activerecord"
|
24
24
|
spec.add_development_dependency "bundler"
|
25
25
|
spec.add_development_dependency "minitest-utils"
|
26
|
+
spec.add_development_dependency "mocha"
|
27
|
+
spec.add_development_dependency "pg"
|
26
28
|
spec.add_development_dependency "pry-meta"
|
27
29
|
spec.add_development_dependency "rake"
|
28
30
|
spec.add_development_dependency "rubocop"
|
31
|
+
spec.add_development_dependency "sequel"
|
29
32
|
spec.add_development_dependency "simplecov"
|
30
|
-
spec.add_development_dependency "sqlite3"
|
31
33
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "bundler/inline"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
gemfile do
|
5
|
+
source "https://rubygems.org"
|
6
|
+
gem "sqlite3"
|
7
|
+
gem "activerecord", require: "active_record"
|
8
|
+
gem "attr_keyring",
|
9
|
+
path: File.expand_path("..", __dir__)
|
10
|
+
end
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection "sqlite3::memory:"
|
13
|
+
|
14
|
+
begin
|
15
|
+
previous_stdout = $stdout
|
16
|
+
$stdout = StringIO.new
|
17
|
+
|
18
|
+
ActiveRecord::Schema.define(version: 0) do
|
19
|
+
create_table :users do |t|
|
20
|
+
t.binary :encrypted_email, null: false
|
21
|
+
t.text :email_digest, null: false
|
22
|
+
t.integer :keyring_id, null: false
|
23
|
+
end
|
24
|
+
|
25
|
+
add_index :users, :email_digest, unique: true
|
26
|
+
end
|
27
|
+
ensure
|
28
|
+
$stdout = previous_stdout
|
29
|
+
end
|
30
|
+
|
31
|
+
class ApplicationRecord < ActiveRecord::Base
|
32
|
+
self.abstract_class = true
|
33
|
+
|
34
|
+
include AttrKeyring.active_record
|
35
|
+
end
|
36
|
+
|
37
|
+
class User < ApplicationRecord
|
38
|
+
attr_keyring "1" => "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
39
|
+
attr_encrypt :email
|
40
|
+
|
41
|
+
validates_uniqueness_of :email_digest
|
42
|
+
end
|
43
|
+
|
44
|
+
john = User.create(email: "john@example.com")
|
45
|
+
|
46
|
+
puts "👱 attributes"
|
47
|
+
puts john.email
|
48
|
+
puts john.email_digest
|
49
|
+
puts john.encrypted_email
|
50
|
+
puts john.keyring_id
|
51
|
+
puts
|
52
|
+
|
53
|
+
puts "🔁 rotate key"
|
54
|
+
User.keyring["2"] = "r6AfOeilPDJomFsiOXLdfQ=="
|
55
|
+
puts john.keyring_rotate!
|
56
|
+
puts
|
57
|
+
|
58
|
+
puts "👱 attributes (after key rotation)"
|
59
|
+
puts john.email
|
60
|
+
puts john.email_digest
|
61
|
+
puts john.encrypted_email
|
62
|
+
puts john.keyring_id
|
63
|
+
puts
|
64
|
+
|
65
|
+
puts "👨 assign new email"
|
66
|
+
puts john.update(email: "jdoe@example.com")
|
67
|
+
puts john.email
|
68
|
+
puts john.email_digest
|
69
|
+
puts john.encrypted_email
|
70
|
+
puts john.keyring_id
|
71
|
+
puts
|
72
|
+
|
73
|
+
puts "🔎 search by email digest"
|
74
|
+
user = User.find_by_email_digest(Digest::SHA1.hexdigest("jdoe@example.com"))
|
75
|
+
puts user.email
|
76
|
+
puts user == john
|
77
|
+
puts
|
78
|
+
|
79
|
+
puts "❌ duplicated email address"
|
80
|
+
copycat = User.create(email: john.email)
|
81
|
+
p copycat.errors.to_h
|
82
|
+
puts
|
83
|
+
|
84
|
+
puts "🔑 retrieve latest key from keyring"
|
85
|
+
puts User.keyring.current_key
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/inline"
|
2
|
+
|
3
|
+
gemfile do
|
4
|
+
gem "attr_keyring",
|
5
|
+
require: "keyring",
|
6
|
+
path: File.expand_path("..", __dir__)
|
7
|
+
end
|
8
|
+
|
9
|
+
keyring = Keyring.new("1" => "QSXyoiRDPoJmfkJUZ4hJeQ==")
|
10
|
+
|
11
|
+
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
12
|
+
|
13
|
+
puts encrypted
|
14
|
+
puts keyring_id
|
15
|
+
puts digest
|
16
|
+
|
17
|
+
decrypted = keyring.decrypt(encrypted, keyring_id)
|
18
|
+
puts decrypted
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "bundler/inline"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
gemfile do
|
5
|
+
source "https://rubygems.org"
|
6
|
+
gem "sqlite3"
|
7
|
+
gem "sequel"
|
8
|
+
gem "pry-meta"
|
9
|
+
gem "attr_keyring",
|
10
|
+
path: File.expand_path("..", __dir__)
|
11
|
+
end
|
12
|
+
|
13
|
+
Sequel.extension :migration
|
14
|
+
|
15
|
+
DB = Sequel.sqlite
|
16
|
+
|
17
|
+
Sequel.migration do
|
18
|
+
up do
|
19
|
+
create_table(:users) do
|
20
|
+
primary_key :id
|
21
|
+
String :encrypted_email, null: false
|
22
|
+
String :email_digest, null: false
|
23
|
+
Integer :keyring_id, null: false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end.apply(DB, :up)
|
27
|
+
|
28
|
+
class User < Sequel::Model
|
29
|
+
include AttrKeyring.sequel
|
30
|
+
plugin :validation_helpers
|
31
|
+
|
32
|
+
attr_keyring "1" => "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
33
|
+
attr_encrypt :email
|
34
|
+
|
35
|
+
def validate
|
36
|
+
super
|
37
|
+
validates_unique :email_digest
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
john = User.create(email: "john@example.com")
|
42
|
+
|
43
|
+
puts "👱 attributes"
|
44
|
+
puts john.email
|
45
|
+
puts john.email_digest
|
46
|
+
puts john.encrypted_email
|
47
|
+
puts john.keyring_id
|
48
|
+
puts
|
49
|
+
|
50
|
+
puts "🔁 rotate key"
|
51
|
+
User.keyring["2"] = "r6AfOeilPDJomFsiOXLdfQ=="
|
52
|
+
puts john.keyring_rotate!
|
53
|
+
puts
|
54
|
+
|
55
|
+
puts "👱 attributes (after key rotation)"
|
56
|
+
puts john.email
|
57
|
+
puts john.email_digest
|
58
|
+
puts john.encrypted_email
|
59
|
+
puts john.keyring_id
|
60
|
+
puts
|
61
|
+
|
62
|
+
puts "👨 assign new email"
|
63
|
+
puts john.update(email: "jdoe@example.com")
|
64
|
+
puts john.email
|
65
|
+
puts john.email_digest
|
66
|
+
puts john.encrypted_email
|
67
|
+
puts john.keyring_id
|
68
|
+
puts
|
69
|
+
|
70
|
+
puts "🔎 search by email digest"
|
71
|
+
user = User.first(email_digest: Digest::SHA1.hexdigest("jdoe@example.com"))
|
72
|
+
puts user.email
|
73
|
+
puts user == john
|
74
|
+
puts
|
75
|
+
|
76
|
+
puts "❌ duplicated email address"
|
77
|
+
copycat = User.new(email: john.email)
|
78
|
+
puts copycat.valid?
|
79
|
+
p copycat.errors.to_h
|
80
|
+
puts
|
81
|
+
|
82
|
+
puts "🔑 retrieve latest key from keyring"
|
83
|
+
puts User.keyring.current_key
|
data/lib/attr_keyring.rb
CHANGED
@@ -1,41 +1,122 @@
|
|
1
1
|
module AttrKeyring
|
2
|
-
require "active_record"
|
3
|
-
require "openssl"
|
4
|
-
require "digest/sha1"
|
5
|
-
|
6
2
|
require "attr_keyring/version"
|
7
|
-
require "
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
require "keyring"
|
4
|
+
|
5
|
+
def self.active_record
|
6
|
+
require "attr_keyring/active_record"
|
7
|
+
::AttrKeyring::ActiveRecord
|
8
|
+
end
|
13
9
|
|
14
|
-
|
15
|
-
|
10
|
+
def self.sequel
|
11
|
+
require "attr_keyring/sequel"
|
12
|
+
::AttrKeyring::Sequel
|
13
|
+
end
|
16
14
|
|
17
|
-
def self.
|
15
|
+
def self.setup(target)
|
18
16
|
target.class_eval do
|
19
|
-
extend
|
20
|
-
include
|
17
|
+
extend ClassMethods
|
18
|
+
include InstanceMethods
|
21
19
|
|
22
20
|
class << self
|
23
|
-
attr_accessor :
|
21
|
+
attr_accessor :encrypted_attributes
|
24
22
|
attr_accessor :keyring
|
23
|
+
attr_accessor :keyring_column_name
|
24
|
+
end
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
self.encrypted_attributes = []
|
27
|
+
self.keyring = Keyring.new({})
|
28
|
+
self.keyring_column_name = :keyring_id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
def inherited(subclass)
|
34
|
+
super
|
35
|
+
|
36
|
+
subclass.encrypted_attributes = encrypted_attributes.dup
|
37
|
+
subclass.keyring = keyring
|
38
|
+
subclass.keyring_column_name = keyring_column_name
|
39
|
+
end
|
28
40
|
|
29
|
-
|
30
|
-
|
31
|
-
|
41
|
+
def attr_keyring(keyring, encryptor: Keyring::Encryptor::AES::AES128CBC)
|
42
|
+
self.keyring = Keyring.new(keyring, encryptor)
|
43
|
+
end
|
44
|
+
|
45
|
+
def attr_encrypt(*attributes)
|
46
|
+
self.encrypted_attributes ||= []
|
47
|
+
encrypted_attributes.push(*attributes)
|
48
|
+
|
49
|
+
attributes.each do |attribute|
|
50
|
+
define_attr_encrypt_writer(attribute)
|
51
|
+
define_attr_encrypt_reader(attribute)
|
32
52
|
end
|
53
|
+
end
|
33
54
|
|
34
|
-
|
35
|
-
|
36
|
-
|
55
|
+
def define_attr_encrypt_writer(attribute)
|
56
|
+
define_method("#{attribute}=") do |value|
|
57
|
+
attr_encrypt_column(attribute, value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def define_attr_encrypt_reader(attribute)
|
62
|
+
define_method(attribute) do
|
63
|
+
attr_decrypt_column(attribute)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module InstanceMethods
|
69
|
+
private def attr_encrypt_column(attribute, value)
|
70
|
+
clear_decrypted_column_cache(attribute)
|
71
|
+
return reset_encrypted_column(attribute) unless value
|
72
|
+
|
73
|
+
value = value.to_s
|
74
|
+
|
75
|
+
previous_keyring_id = public_send(self.class.keyring_column_name)
|
76
|
+
encrypted_value, keyring_id, digest = self.class.keyring.encrypt(value, previous_keyring_id)
|
77
|
+
|
78
|
+
public_send("#{self.class.keyring_column_name}=", keyring_id)
|
79
|
+
public_send("encrypted_#{attribute}=", encrypted_value)
|
80
|
+
public_send("#{attribute}_digest=", digest) if respond_to?("#{attribute}_digest=")
|
81
|
+
end
|
82
|
+
|
83
|
+
private def attr_decrypt_column(attribute)
|
84
|
+
cache_name = :"@#{attribute}"
|
85
|
+
return instance_variable_get(cache_name) if instance_variable_defined?(cache_name)
|
86
|
+
|
87
|
+
encrypted_value = public_send("encrypted_#{attribute}")
|
88
|
+
return unless encrypted_value
|
89
|
+
|
90
|
+
decrypted_value = self.class.keyring.decrypt(encrypted_value, public_send(self.class.keyring_column_name))
|
91
|
+
|
92
|
+
instance_variable_set(cache_name, decrypted_value)
|
93
|
+
end
|
94
|
+
|
95
|
+
private def clear_decrypted_column_cache(attribute)
|
96
|
+
cache_name = :"@#{attribute}"
|
97
|
+
remove_instance_variable(cache_name) if instance_variable_defined?(cache_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
private def reset_encrypted_column(attribute)
|
101
|
+
public_send("encrypted_#{attribute}=", nil)
|
102
|
+
public_send("#{attribute}_digest=", nil) if respond_to?("#{attribute}_digest=")
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
private def migrate_to_latest_encryption_key
|
107
|
+
keyring_id = self.class.keyring.current_key.id
|
108
|
+
|
109
|
+
self.class.encrypted_attributes.each do |attribute|
|
110
|
+
value = public_send(attribute)
|
111
|
+
next if value.nil?
|
112
|
+
|
113
|
+
encrypted_value, _, digest = self.class.keyring.encrypt(value)
|
114
|
+
|
115
|
+
public_send("encrypted_#{attribute}=", encrypted_value)
|
116
|
+
public_send("#{attribute}_digest=", digest) if respond_to?("#{attribute}_digest")
|
117
|
+
end
|
37
118
|
|
38
|
-
|
119
|
+
public_send("#{self.class.keyring_column_name}=", keyring_id)
|
39
120
|
end
|
40
121
|
end
|
41
122
|
end
|
@@ -1,87 +1,30 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
1
3
|
module AttrKeyring
|
2
4
|
module ActiveRecord
|
3
|
-
|
4
|
-
|
5
|
-
self.keyring = Keyring.new(keyring, encryptor)
|
6
|
-
end
|
5
|
+
def self.included(target)
|
6
|
+
AttrKeyring.setup(target)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
attributes.each do |attribute|
|
12
|
-
keyring_attrs[attribute.to_sym] = {encode: encode}
|
13
|
-
end
|
8
|
+
target.class_eval do
|
9
|
+
before_save :migrate_to_latest_encryption_key
|
14
10
|
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
def keyring_rotate!
|
12
|
+
migrate_to_latest_encryption_key
|
13
|
+
save!
|
18
14
|
end
|
19
15
|
end
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
17
|
+
target.prepend(
|
18
|
+
Module.new do
|
19
|
+
def reload(options = nil)
|
20
|
+
super
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
encrypted_value = Base64.strict_encode64(encrypted_value) if options[:encode]
|
30
|
-
|
31
|
-
public_send("#{keyring_column_name}=", keyring_id) unless stored_keyring_id
|
32
|
-
public_send("encrypted_#{attribute}=", encrypted_value)
|
33
|
-
attr_encrypt_digest(attribute, value)
|
22
|
+
self.class.encrypted_attributes.each do |attribute|
|
23
|
+
clear_decrypted_column_cache(attribute)
|
24
|
+
end
|
25
|
+
end
|
34
26
|
end
|
35
|
-
|
36
|
-
|
37
|
-
def define_attr_encrypt_reader(attribute)
|
38
|
-
define_method(attribute) do
|
39
|
-
encrypted_value = public_send("encrypted_#{attribute}")
|
40
|
-
|
41
|
-
return unless encrypted_value
|
42
|
-
|
43
|
-
options = self.class.keyring_attrs.fetch(attribute)
|
44
|
-
encrypted_value = Base64.strict_decode64(encrypted_value) if options[:encode]
|
45
|
-
keyring_id = public_send(keyring_column_name)
|
46
|
-
value = self.class.keyring.decrypt(encrypted_value, keyring_id)
|
47
|
-
value
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
module InstanceMethods
|
53
|
-
private def attr_reset_column(attribute)
|
54
|
-
public_send("encrypted_#{attribute}=", nil)
|
55
|
-
public_send("#{attribute}_digest=", nil) if respond_to?("#{attribute}_digest=")
|
56
|
-
nil
|
57
|
-
end
|
58
|
-
|
59
|
-
private def attr_encrypt_digest(attribute, value)
|
60
|
-
digest_column = "#{attribute}_digest"
|
61
|
-
public_send("#{digest_column}=", Digest::SHA1.hexdigest(value)) if respond_to?(digest_column)
|
62
|
-
end
|
63
|
-
|
64
|
-
private def migrate_to_latest_encryption_key
|
65
|
-
keyring_id = self.class.keyring.current_key.id
|
66
|
-
|
67
|
-
self.class.keyring_attrs.each do |attribute, options|
|
68
|
-
value = public_send(attribute)
|
69
|
-
next if value.nil?
|
70
|
-
|
71
|
-
encrypted_value = self.class.keyring.encrypt(value, keyring_id)
|
72
|
-
encrypted_value = Base64.strict_encode64(encrypted_value) if options[:encode]
|
73
|
-
|
74
|
-
public_send("encrypted_#{attribute}=", encrypted_value)
|
75
|
-
attr_encrypt_digest(attribute, value)
|
76
|
-
end
|
77
|
-
|
78
|
-
public_send("#{keyring_column_name}=", keyring_id)
|
79
|
-
end
|
80
|
-
|
81
|
-
def keyring_rotate!
|
82
|
-
migrate_to_latest_encryption_key
|
83
|
-
save! if changed?
|
84
|
-
end
|
27
|
+
)
|
85
28
|
end
|
86
29
|
end
|
87
30
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "sequel"
|
2
|
+
|
3
|
+
module AttrKeyring
|
4
|
+
module Sequel
|
5
|
+
def self.included(target)
|
6
|
+
AttrKeyring.setup(target)
|
7
|
+
|
8
|
+
target.class_eval do
|
9
|
+
def before_save
|
10
|
+
super
|
11
|
+
migrate_to_latest_encryption_key
|
12
|
+
end
|
13
|
+
|
14
|
+
def keyring_rotate!
|
15
|
+
migrate_to_latest_encryption_key
|
16
|
+
save
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/attr_keyring/version.rb
CHANGED
data/lib/keyring.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module Keyring
|
2
|
+
require "openssl"
|
3
|
+
require "base64"
|
4
|
+
require "digest/sha1"
|
5
|
+
|
6
|
+
require "keyring/key"
|
7
|
+
require "keyring/encryptor/aes"
|
8
|
+
|
9
|
+
UnknownKey = Class.new(StandardError)
|
10
|
+
InvalidSecret = Class.new(StandardError)
|
11
|
+
EmptyKeyring = Class.new(StandardError)
|
12
|
+
|
13
|
+
class Base
|
14
|
+
def initialize(keyring, encryptor)
|
15
|
+
@encryptor = encryptor
|
16
|
+
@keyring = keyring.map do |id, value|
|
17
|
+
Key.new(id, value, @encryptor.key_size)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_key
|
22
|
+
@keyring.max_by(&:id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](id)
|
26
|
+
raise EmptyKeyring, "keyring doesn't have any keys" if @keyring.empty?
|
27
|
+
|
28
|
+
key = @keyring.find {|k| k.id == id.to_i }
|
29
|
+
return key if key
|
30
|
+
|
31
|
+
raise UnknownKey, "key=#{id} is not available on keyring"
|
32
|
+
end
|
33
|
+
|
34
|
+
def []=(id, value)
|
35
|
+
@keyring << Key.new(id, value, @encryptor.key_size)
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear
|
39
|
+
@keyring.clear
|
40
|
+
end
|
41
|
+
|
42
|
+
def encrypt(message, keyring_id = nil)
|
43
|
+
keyring_id ||= current_key&.id
|
44
|
+
digest = Digest::SHA1.hexdigest(message)
|
45
|
+
|
46
|
+
[
|
47
|
+
@encryptor.encrypt(self[keyring_id].value, message),
|
48
|
+
keyring_id,
|
49
|
+
digest
|
50
|
+
]
|
51
|
+
end
|
52
|
+
|
53
|
+
def decrypt(message, keyring_id)
|
54
|
+
@encryptor.decrypt(self[keyring_id].value, message)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.new(keyring, encryptor = Encryptor::AES::AES128CBC)
|
59
|
+
Base.new(keyring, encryptor)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Keyring
|
2
|
+
module Encryptor
|
3
|
+
module AES
|
4
|
+
class Base
|
5
|
+
def self.build_cipher
|
6
|
+
OpenSSL::Cipher.new(cipher_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.key_size
|
10
|
+
@key_size ||= build_cipher.key_len
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.encrypt(key, message)
|
14
|
+
cipher = build_cipher
|
15
|
+
cipher.encrypt
|
16
|
+
iv = cipher.random_iv
|
17
|
+
cipher.iv = iv
|
18
|
+
cipher.key = key
|
19
|
+
encrypted = cipher.update(message) + cipher.final
|
20
|
+
|
21
|
+
Base64.strict_encode64("#{iv}#{encrypted}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.decrypt(key, message)
|
25
|
+
cipher = build_cipher
|
26
|
+
cipher.decrypt
|
27
|
+
|
28
|
+
message = Base64.strict_decode64(message)
|
29
|
+
iv = message[0...cipher.iv_len]
|
30
|
+
encrypted = message[cipher.iv_len..-1]
|
31
|
+
|
32
|
+
cipher.iv = iv
|
33
|
+
cipher.key = key
|
34
|
+
cipher.update(encrypted) + cipher.final
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class AES128CBC < Base
|
39
|
+
def self.cipher_name
|
40
|
+
"AES-128-CBC"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class AES192CBC < Base
|
45
|
+
def self.cipher_name
|
46
|
+
"AES-192-CBC"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class AES256CBC < Base
|
51
|
+
def self.cipher_name
|
52
|
+
"AES-256-CBC"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_keyring
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Vieira
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-12-
|
11
|
+
date: 2018-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,7 +17,7 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
|
-
type: :
|
20
|
+
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: pry-meta
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -95,7 +123,7 @@ dependencies:
|
|
95
123
|
- !ruby/object:Gem::Version
|
96
124
|
version: '0'
|
97
125
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
126
|
+
name: sequel
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
100
128
|
requirements:
|
101
129
|
- - ">="
|
@@ -109,7 +137,7 @@ dependencies:
|
|
109
137
|
- !ruby/object:Gem::Version
|
110
138
|
version: '0'
|
111
139
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
140
|
+
name: simplecov
|
113
141
|
requirement: !ruby/object:Gem::Requirement
|
114
142
|
requirements:
|
115
143
|
- - ">="
|
@@ -143,14 +171,16 @@ files:
|
|
143
171
|
- attr_keyring.svg
|
144
172
|
- bin/console
|
145
173
|
- bin/setup
|
174
|
+
- examples/active_record_sample.rb
|
175
|
+
- examples/keyring_sample.rb
|
176
|
+
- examples/sequel_sample.rb
|
146
177
|
- lib/attr_keyring.rb
|
147
178
|
- lib/attr_keyring/active_record.rb
|
148
|
-
- lib/attr_keyring/
|
149
|
-
- lib/attr_keyring/encryptor/aes_128_cbc.rb
|
150
|
-
- lib/attr_keyring/encryptor/aes_256_cbc.rb
|
151
|
-
- lib/attr_keyring/key.rb
|
152
|
-
- lib/attr_keyring/keyring.rb
|
179
|
+
- lib/attr_keyring/sequel.rb
|
153
180
|
- lib/attr_keyring/version.rb
|
181
|
+
- lib/keyring.rb
|
182
|
+
- lib/keyring/encryptor/aes.rb
|
183
|
+
- lib/keyring/key.rb
|
154
184
|
homepage: https://github.com/fnando/attr_keyring
|
155
185
|
licenses:
|
156
186
|
- MIT
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module AttrKeyring
|
2
|
-
module Encryptor
|
3
|
-
class AES
|
4
|
-
def self.build_cipher
|
5
|
-
OpenSSL::Cipher.new(cipher_name)
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.key_size
|
9
|
-
@key_size ||= build_cipher.key_len
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.encrypt(key, message)
|
13
|
-
cipher = build_cipher
|
14
|
-
cipher.encrypt
|
15
|
-
iv = cipher.random_iv
|
16
|
-
cipher.iv = iv
|
17
|
-
cipher.key = key
|
18
|
-
iv + cipher.update(message) + cipher.final
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.decrypt(key, message)
|
22
|
-
cipher = build_cipher
|
23
|
-
cipher.decrypt
|
24
|
-
iv = message[0...cipher.iv_len]
|
25
|
-
encrypted = message[cipher.iv_len..-1]
|
26
|
-
cipher.iv = iv
|
27
|
-
cipher.key = key
|
28
|
-
cipher.update(encrypted) + cipher.final
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
data/lib/attr_keyring/keyring.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
module AttrKeyring
|
2
|
-
class Keyring
|
3
|
-
def initialize(keyring, encryptor = Encryptor::AES128CBC)
|
4
|
-
@encryptor = encryptor
|
5
|
-
@keyring = keyring.map do |id, value|
|
6
|
-
Key.new(id, value, @encryptor.key_size)
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
def current_key
|
11
|
-
@keyring.max_by(&:id)
|
12
|
-
end
|
13
|
-
|
14
|
-
def [](id)
|
15
|
-
key = @keyring.find {|k| k.id == id.to_i }
|
16
|
-
return key if key
|
17
|
-
|
18
|
-
raise UnknownKey, "key=#{id} is not available on keyring"
|
19
|
-
end
|
20
|
-
|
21
|
-
def []=(id, value)
|
22
|
-
@keyring << Key.new(id, value, @encryptor.key_size)
|
23
|
-
end
|
24
|
-
|
25
|
-
def clear
|
26
|
-
@keyring.clear
|
27
|
-
end
|
28
|
-
|
29
|
-
def encrypt(message, keyring_id = current_key.id)
|
30
|
-
@encryptor.encrypt(self[keyring_id].value, message)
|
31
|
-
end
|
32
|
-
|
33
|
-
def decrypt(message, keyring_id)
|
34
|
-
@encryptor.decrypt(self[keyring_id].value, message)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|