active_record_encryption 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +81 -40
- data/lib/active_record_encryption.rb +15 -14
- data/lib/active_record_encryption/binary.rb +68 -0
- data/lib/active_record_encryption/encrypted_attribute.rb +1 -12
- data/lib/active_record_encryption/encryptor.rb +18 -17
- data/lib/active_record_encryption/encryptor/active_support.rb +28 -0
- data/lib/active_record_encryption/encryptor/aes_256_cbc.rb +77 -0
- data/lib/active_record_encryption/encryptor/{cipher.rb → base.rb} +10 -1
- data/lib/active_record_encryption/encryptor/raw.rb +13 -0
- data/lib/active_record_encryption/encryptor/registry.rb +57 -0
- data/lib/active_record_encryption/exceptions.rb +0 -1
- data/lib/active_record_encryption/serializer_with_cast.rb +5 -3
- data/lib/active_record_encryption/type.rb +39 -6
- data/lib/active_record_encryption/version.rb +1 -1
- metadata +26 -18
- data/.gitignore +0 -17
- data/.rspec +0 -3
- data/.rubocop.yml +0 -77
- data/.travis.yml +0 -12
- data/Appraisals +0 -13
- data/Gemfile +0 -8
- data/Guardfile +0 -51
- data/Rakefile +0 -6
- data/active_record_encryption.gemspec +0 -37
- data/gemfiles/5.0_stable.gemfile +0 -9
- data/gemfiles/5.1_stable.gemfile +0 -9
- data/gemfiles/5.2_stable.gemfile +0 -9
- data/lib/active_record_encryption/testing/test_cipher.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f992a2aae3e31c5c369213e30abe9cb77896e7f92ac6fcdb7674e75e20e91f8e
|
4
|
+
data.tar.gz: 449de2e8b89d85509650bd5c38615fa9aa93b6585fce3e1e0b8e0ddd9db73e3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ee286e350bad31cefce68330445a66c25ba3bc55ab0d2d0e2af2f74c7b1ccc139a02dc1556c9f8367aa6b84e46d84d3ca28a855a0d8fa508962514c33c4f2b6
|
7
|
+
data.tar.gz: 3565c31c4f309bcb6d79fa44abf5bcca01195224fa6759e2b7bdeb7db4f939ed0bfa2beb9741c9cb1e1335314eca502fc3107814e80a5b46be8927e4b3b7fa6e
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
## 0.2.0
|
2
|
+
|
3
|
+
### New features
|
4
|
+
|
5
|
+
- Find custom encryptor by `:encryption` options
|
6
|
+
- `encrypted_attribute(:field, :type, encryption: { encryptor: :active_support })`
|
7
|
+
- Register your encryption
|
8
|
+
- `ActiveRecordEncryption::Type.register`
|
9
|
+
- Lookup your encryption
|
10
|
+
- `ActiveRecordEncryption::Type.lookup`
|
11
|
+
- Add `ActiveRecordEncryption.default_encryptor`
|
12
|
+
- Buildin some encryption
|
13
|
+
- ActiveSupport
|
14
|
+
- `encrypted_attribute(:field, :type, encryption: { encryptor: :active_support, key: ENV['KEY'], salt: ENV['SALT'] })`
|
15
|
+
- AES-256-CBC
|
16
|
+
- `encrypted_attribute(:field, :type, encryption: { encryptor: :aes_256_cbc, key: ENV['KEY'] })`
|
17
|
+
|
18
|
+
### Bug fixes
|
19
|
+
|
20
|
+
- `ActiveRecordEncryption::Type#change_in_place?` Compare old value with new value.
|
21
|
+
- `ActiveRecordEncryption::Type#cast` supports TimeWithZone
|
22
|
+
|
23
|
+
### Changes
|
24
|
+
|
25
|
+
- Remove `ActiveRecordEncryption.with_cipher`
|
26
|
+
- Remove `ActiveRecordEncryption.cipher`
|
27
|
+
- Now, base class of Encryption is `ActiveRecordEncryption::Encryptor::Base`
|
28
|
+
|
29
|
+
## 0.1.1
|
30
|
+
|
31
|
+
- [#1](https://github.com/alpaca-tc/active_record_encryption/pull/1) Support only binary column
|
32
|
+
|
33
|
+
## 0.1.0
|
34
|
+
|
35
|
+
- First release
|
data/README.md
CHANGED
@@ -1,81 +1,122 @@
|
|
1
|
-
[](https://travis-ci.org/alpaca-tc/active_record_encryption)
|
2
|
-
|
3
1
|
# ActiveRecordEncryption
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
## Installation
|
9
|
-
|
10
|
-
Add this line to your application's Gemfile:
|
3
|
+
Provides transparent encryption attribute for ActiveRecord.
|
4
|
+
You can easily encrypt and decrypt sensitive data.
|
11
5
|
|
12
|
-
|
13
|
-
gem 'active_record_encryption'
|
14
|
-
```
|
6
|
+
This implementation is based on ActiveRecord's Attribute-API, and it is very simple and powerful.
|
15
7
|
|
16
8
|
## Usage
|
17
9
|
|
18
|
-
|
19
|
-
|
20
|
-
Here is an example of passing a type of object you want:
|
10
|
+
Add definition of the encrypted attribute in your application.
|
21
11
|
|
22
12
|
```ruby
|
13
|
+
# app/models/post.rb
|
23
14
|
class Post < ActiveRecord::Base
|
24
|
-
include ActiveRecordEncryption::EncryptedAttribute
|
25
|
-
|
26
15
|
encrypted_attribute(:name, :string)
|
27
|
-
encrypted_attribute(:published_on, :date)
|
28
16
|
end
|
17
|
+
```
|
29
18
|
|
30
|
-
|
19
|
+
That's all. This column is already enabled for transparent encryption.
|
31
20
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
DEBUG -- : Post Create (0.1ms) INSERT INTO "posts" ("name", "published_on") VALUES (?, ?) [["name", "N\xFA\xDD\xC2\xB0&\xAE\x9A..."], ["published_on", "N\xFA\xDD\xX2\xB0&\xAE\x9A..."]]
|
37
|
-
DEBUG -- : (0.0ms) commit transaction
|
21
|
+
```ruby
|
22
|
+
post = Post.create!(name: 'Baker')
|
23
|
+
post.name #=> "Baker"
|
24
|
+
post.name_before_type_cast #=> "ZS~\xAB\x8C\xD1\xCA\u0016\xA8\x80f@\xE8s\xB7J/\xA9\xEC/\xBDj\xDE6(Y\u007F\u0016<W\u0011\x96"
|
38
25
|
```
|
39
26
|
|
40
|
-
|
27
|
+
## Options
|
41
28
|
|
42
|
-
You can
|
29
|
+
You can set encryption as default.
|
43
30
|
|
31
|
+
```ruby
|
32
|
+
# config/initializers/active_record_encryption.rb
|
33
|
+
ActiveRecordEncryption.default_encryption = {
|
34
|
+
encryptor: :active_support,
|
35
|
+
key: ENV['ENCRYPTION_KEY'],
|
36
|
+
salt: ENV['ENCRYPTION_SALT']
|
37
|
+
}
|
44
38
|
```
|
39
|
+
|
40
|
+
### encrypted_attribute
|
41
|
+
|
42
|
+
`.encrypted_attribute()` wrapped on `.attribute` method. and you can pass the same arguments as [ActiveRecord::Attributes.attribute](https://apidock.com/rails/ActiveRecord/Attributes/ClassMethods/attribute)
|
43
|
+
|
44
|
+
```ruby
|
45
45
|
class PointLog < ActiveRecord::Base
|
46
46
|
encrypted_attribute(:date, :date)
|
47
47
|
encrypted_attribute(:point, :integer, default: -> { Current.user.current_point })
|
48
48
|
encrypted_attribute(:price, Money.new)
|
49
|
+
encrypted_attribute(:serialized_address, :string)
|
50
|
+
|
51
|
+
# Change encryptor
|
52
|
+
encrypted_attribute(:name, :field, encryption: { encryptor: :active_support, key: ENV['ENCRYPTION_KEY'], salt: ['ENCRYPTION_SALT'] })
|
49
53
|
end
|
50
54
|
```
|
51
55
|
|
52
|
-
|
56
|
+
## Supported Available Encryptors
|
57
|
+
|
58
|
+
There are four supported encryptors: `:active_support`, `:aes_256_cbc`.
|
53
59
|
|
54
|
-
|
60
|
+
- `:active_support`
|
61
|
+
- Encryption is performed using `ActiveSupport::MessageEncryptor`
|
62
|
+
- Example
|
63
|
+
- `encrypted_attribute(:field, :type, encryption: { encryptor: :active_support, key: SecureRandom.hex(64), salt: SecureRandom.hex(64) })`
|
64
|
+
- `:aes_256_cbc`
|
65
|
+
- Encryption is performed using `OpenSSL::Cipher.new('AES-256-CBC')
|
66
|
+
- Example
|
67
|
+
- `encrypted_attribute(:field, :type, encryption: { encryptor: :active_support, key: SecureRandom.hex(64) })`
|
55
68
|
|
56
|
-
|
69
|
+
## Customize encryptor
|
70
|
+
|
71
|
+
You can easilly add your encryptor.
|
57
72
|
|
58
73
|
```ruby
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
74
|
+
class YourEncryptor < ActiveRecordEncryption::Encryptor::Base
|
75
|
+
def initialize(key:)
|
76
|
+
@key = key
|
77
|
+
end
|
78
|
+
|
79
|
+
def encrypt(value)
|
80
|
+
# An encrypt method that returns the encrypted string
|
81
|
+
end
|
82
|
+
|
83
|
+
def decrypt(value)
|
84
|
+
# A decrypt method that returns the plaintext
|
64
85
|
end
|
65
86
|
end
|
87
|
+
|
88
|
+
ActiveRecordEncryption.default_encryption = {
|
89
|
+
encryptor: YourEncryptor,
|
90
|
+
key: ENV['ENCRYPTION_KEY']
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
## Secret key/salt
|
95
|
+
|
96
|
+
For encryptors requiring secret keys, you can generate them.
|
97
|
+
These values should be stored outside of your application repository for added security.
|
98
|
+
|
99
|
+
```bash
|
100
|
+
ruby -e "require 'securerandom'; puts SecureRandom.hex(64)"`
|
66
101
|
```
|
67
102
|
|
68
|
-
|
103
|
+
### Migration
|
69
104
|
|
70
|
-
|
105
|
+
Create or modify the table that your model like the following:
|
71
106
|
|
72
|
-
|
107
|
+
**NOTE: Default limit of ActiveRecord's binary column is too long. Please set limit(< 0xfff) for encrypted columns.**
|
73
108
|
|
74
|
-
|
109
|
+
```ruby
|
110
|
+
ActiveRecord::Schema.define do
|
111
|
+
create_table(:posts) do |t|
|
112
|
+
t.binary :name, limit: 1000
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
75
116
|
|
76
117
|
### Run spec
|
77
118
|
|
78
|
-
```
|
119
|
+
```bash
|
79
120
|
bundle exec appraisal install
|
80
121
|
bundle exec appraisal 5.0-stable rspec
|
81
122
|
bundle exec appraisal 5.1-stable rspec
|
@@ -92,4 +133,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
92
133
|
|
93
134
|
## Code of Conduct
|
94
135
|
|
95
|
-
Everyone interacting in the ActiveRecord::Encryption project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/alpaca-tc/active_record_encryption/blob/master/CODE_OF_CONDUCT.md).
|
136
|
+
Everyone interacting in the `ActiveRecord::Encryption` project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/alpaca-tc/active_record_encryption/blob/master/CODE_OF_CONDUCT.md).
|
@@ -1,25 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'active_support/
|
3
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
4
|
+
require 'active_support/lazy_load_hooks'
|
5
|
+
require 'active_record_encryption/version'
|
6
|
+
require 'active_record_encryption/exceptions'
|
5
7
|
|
6
8
|
module ActiveRecordEncryption
|
7
|
-
|
8
|
-
|
9
|
+
mattr_accessor(:default_encryption, instance_accessor: false) do
|
10
|
+
{ encryptor: :raw }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
9
15
|
require 'active_record_encryption/type'
|
10
16
|
require 'active_record_encryption/encryptor'
|
11
|
-
require 'active_record_encryption/encryptor/cipher'
|
12
17
|
require 'active_record_encryption/encrypted_attribute'
|
18
|
+
require 'active_record_encryption/binary'
|
13
19
|
require 'active_record_encryption/quoter'
|
14
|
-
require 'active_record_encryption/exceptions'
|
15
20
|
|
16
|
-
|
21
|
+
# Register `:encryption` type
|
22
|
+
ActiveRecord::Type.register(:encryption, ActiveRecordEncryption::Type)
|
17
23
|
|
18
|
-
|
19
|
-
|
20
|
-
self.cipher = new_cipher
|
21
|
-
yield
|
22
|
-
ensure
|
23
|
-
self.cipher = previous
|
24
|
-
end
|
24
|
+
# Define `.encrypted_attribute`
|
25
|
+
ActiveRecord::Base.include(ActiveRecordEncryption::EncryptedAttribute)
|
25
26
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module ActiveRecordEncryption
|
6
|
+
class Binary < ::StringIO
|
7
|
+
FORMATS = {
|
8
|
+
u_long_long: {
|
9
|
+
length: 8,
|
10
|
+
code: 'Q>'
|
11
|
+
},
|
12
|
+
long_long: {
|
13
|
+
length: 8,
|
14
|
+
code: 'q>'
|
15
|
+
},
|
16
|
+
u_int: {
|
17
|
+
length: 4,
|
18
|
+
code: 'L>'
|
19
|
+
},
|
20
|
+
int: {
|
21
|
+
length: 4,
|
22
|
+
code: 'l>'
|
23
|
+
},
|
24
|
+
u_short: {
|
25
|
+
length: 2,
|
26
|
+
code: 'S>'
|
27
|
+
},
|
28
|
+
short: {
|
29
|
+
length: 2,
|
30
|
+
code: 's>'
|
31
|
+
},
|
32
|
+
u_char: {
|
33
|
+
length: 1,
|
34
|
+
code: 'C'
|
35
|
+
},
|
36
|
+
char: {
|
37
|
+
length: 1,
|
38
|
+
code: 'c'
|
39
|
+
}
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
FORMATS.each do |format_name, format|
|
43
|
+
class_eval(<<-METHOD, __FILE__, __LINE__ + 1)
|
44
|
+
def read_#{format_name}
|
45
|
+
read(#{format[:length]}).unpack('#{format[:code]}')[0]
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_#{format_name}(value)
|
49
|
+
write([value].pack('#{format[:code]}'))
|
50
|
+
end
|
51
|
+
METHOD
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(*)
|
55
|
+
super
|
56
|
+
binmode
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_string_255(value)
|
60
|
+
write_u_char(value.bytesize)
|
61
|
+
write(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def read_string_255
|
65
|
+
read(read_u_char)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -6,18 +6,7 @@ module ActiveRecordEncryption
|
|
6
6
|
|
7
7
|
module ClassMethods
|
8
8
|
def encrypted_attribute(name, subtype, **options)
|
9
|
-
name
|
10
|
-
|
11
|
-
attribute(name, subtype, **options)
|
12
|
-
decorate_encrypted_attribute(name)
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def decorate_encrypted_attribute(name)
|
18
|
-
decorate_attribute_type(name, :encrypted) do |subtype|
|
19
|
-
ActiveRecordEncryption::Type.new(name, subtype)
|
20
|
-
end
|
9
|
+
attribute(name, :encryption, options.merge(subtype: subtype))
|
21
10
|
end
|
22
11
|
end
|
23
12
|
end
|
@@ -1,29 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_record_encryption/encryptor/registry'
|
4
|
+
require 'active_record_encryption/encryptor/base'
|
5
|
+
require 'active_record_encryption/encryptor/raw'
|
6
|
+
require 'active_record_encryption/encryptor/active_support'
|
7
|
+
require 'active_record_encryption/encryptor/aes_256_cbc'
|
8
|
+
|
3
9
|
module ActiveRecordEncryption
|
4
10
|
module Encryptor
|
5
|
-
|
6
|
-
def encrypt(value, cipher: ActiveRecordEncryption.cipher)
|
7
|
-
raise_missing_cipher_error unless cipher
|
8
|
-
|
9
|
-
string = value_to_string(value)
|
10
|
-
cipher.encrypt(string)
|
11
|
-
end
|
11
|
+
@registry = Registry.new
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
cipher.decrypt(value)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
13
|
+
class << self
|
14
|
+
attr_reader :registry
|
19
15
|
|
20
|
-
|
21
|
-
|
16
|
+
# Add a new type to the registry, allowing it to be gotten through ActiveRecordEncryption::Type#lookup
|
17
|
+
def register(type_name, klass = nil, **options, &block)
|
18
|
+
registry.register(type_name, klass, **options, &block)
|
22
19
|
end
|
23
20
|
|
24
|
-
def
|
25
|
-
|
21
|
+
def lookup(*args, **kwargs)
|
22
|
+
registry.lookup(*args, **kwargs)
|
26
23
|
end
|
27
24
|
end
|
25
|
+
|
26
|
+
register(:raw, ActiveRecordEncryption::Encryptor::Raw)
|
27
|
+
register(:active_support, ActiveRecordEncryption::Encryptor::ActiveSupport)
|
28
|
+
register(:aes_256_cbc, ActiveRecordEncryption::Encryptor::Aes256Cbc)
|
28
29
|
end
|
29
30
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordEncryption
|
4
|
+
module Encryptor
|
5
|
+
class ActiveSupport < Raw
|
6
|
+
def initialize(key:, salt:)
|
7
|
+
key_generator = ::ActiveSupport::KeyGenerator.new(key)
|
8
|
+
@encryptor = ::ActiveSupport::MessageEncryptor.new(key_generator.generate_key(salt, 32))
|
9
|
+
end
|
10
|
+
|
11
|
+
def encrypt(value)
|
12
|
+
encryptor.encrypt_and_sign(super)
|
13
|
+
end
|
14
|
+
|
15
|
+
def decrypt(value)
|
16
|
+
encryptor.decrypt_and_verify(super)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
super && encryptor == other.encryptor
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
attr_reader :encryptor
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module ActiveRecordEncryption
|
6
|
+
module Encryptor
|
7
|
+
class ActiveRecordEncryption::Encryptor::Aes256Cbc < Raw
|
8
|
+
def initialize(key:, encoding: Encoding::UTF_8)
|
9
|
+
@key = key
|
10
|
+
@encoding = encoding
|
11
|
+
end
|
12
|
+
|
13
|
+
def encrypt(value)
|
14
|
+
string = super.dup.force_encoding(encoding)
|
15
|
+
encrypted_data, iv = _encrypt(string, key)
|
16
|
+
|
17
|
+
binary = Binary.new
|
18
|
+
binary.write(iv) # IV is 16 byte
|
19
|
+
binary.write(encrypted_data)
|
20
|
+
binary.string
|
21
|
+
end
|
22
|
+
|
23
|
+
def decrypt(value)
|
24
|
+
binary = Binary.new(value)
|
25
|
+
iv = binary.read(16)
|
26
|
+
encrypted_data = binary.read
|
27
|
+
|
28
|
+
decrypted = _decrypt(encrypted_data, key, iv)
|
29
|
+
decrypted.force_encoding(encoding)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
super &&
|
34
|
+
key == other.key &&
|
35
|
+
encoding == other.encoding
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
attr_reader :key, :encoding
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def valid_encoding?(value)
|
45
|
+
value.valid_encoding? && value.encoding == encoding
|
46
|
+
end
|
47
|
+
|
48
|
+
def _encrypt(value, key)
|
49
|
+
raise ArgumentError, "invalid string given. #{value}" unless valid_encoding?(value)
|
50
|
+
|
51
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
52
|
+
cipher.encrypt
|
53
|
+
cipher.key = key
|
54
|
+
iv = cipher.random_iv # NOTE: Do not reuse IV. See more details https://stackoverflow.com/questions/3008139/why-is-using-a-non-random-iv-with-cbc-mode-a-vulnerability
|
55
|
+
|
56
|
+
result = ''.dup.tap do |buffer|
|
57
|
+
buffer << cipher.update(value) unless value.empty?
|
58
|
+
buffer << cipher.final
|
59
|
+
end
|
60
|
+
|
61
|
+
[result, iv]
|
62
|
+
end
|
63
|
+
|
64
|
+
def _decrypt(value, key, iv)
|
65
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
66
|
+
cipher.decrypt
|
67
|
+
cipher.key = key
|
68
|
+
cipher.iv = iv
|
69
|
+
|
70
|
+
''.dup.tap do |buffer|
|
71
|
+
buffer << cipher.update(value)
|
72
|
+
buffer << cipher.final
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
module ActiveRecordEncryption
|
4
4
|
module Encryptor
|
5
|
-
|
5
|
+
# Abstract interface of encryptor
|
6
|
+
class Base
|
7
|
+
def initialize(*); end
|
8
|
+
|
6
9
|
def encrypt(value)
|
7
10
|
value
|
8
11
|
end
|
@@ -10,6 +13,12 @@ module ActiveRecordEncryption
|
|
10
13
|
def decrypt(value)
|
11
14
|
value
|
12
15
|
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
self.class == other.class
|
19
|
+
end
|
20
|
+
|
21
|
+
alias eql? ==
|
13
22
|
end
|
14
23
|
end
|
15
24
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordEncryption
|
4
|
+
module Encryptor
|
5
|
+
# Basic base class of encryptor.
|
6
|
+
# Format user input to string before encryption.
|
7
|
+
class Raw < Base
|
8
|
+
def encrypt(value)
|
9
|
+
ActiveRecordEncryption::Quoter.instance.type_cast(super)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordEncryption
|
4
|
+
module Encryptor
|
5
|
+
class Registry
|
6
|
+
def initialize
|
7
|
+
@registrations = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(encryptor_name, klass = nil, **options, &block)
|
11
|
+
block ||= proc { |_, *args| klass.new(*args) }
|
12
|
+
registrations << Registration.new(encryptor_name, block, **options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def lookup(symbol, *args)
|
16
|
+
registration = find_registration(symbol, *args)
|
17
|
+
|
18
|
+
if registration
|
19
|
+
registration.call(self, symbol, *args)
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Unknown encryptor #{symbol.inspect}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :registrations
|
28
|
+
|
29
|
+
def find_registration(symbol, *args)
|
30
|
+
registrations.find { |registration| registration.matches?(symbol, *args) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Registration
|
35
|
+
def initialize(name, block, **)
|
36
|
+
@name = name
|
37
|
+
@block = block
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(_registry, *args, **kwargs)
|
41
|
+
if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
|
42
|
+
block.call(*args, **kwargs)
|
43
|
+
else
|
44
|
+
block.call(*args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def matches?(encryptor_name, *_args, **_kwargs)
|
49
|
+
encryptor_name == name
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :name, :block
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -28,9 +28,11 @@ module ActiveRecordEncryption
|
|
28
28
|
end
|
29
29
|
|
30
30
|
# Backport Rails5.2
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
if ActiveRecord.gem_version < Gem::Version.create('5.2')
|
32
|
+
refine ActiveModel::Type::DateTime do
|
33
|
+
def serialize(value)
|
34
|
+
super(cast(value))
|
35
|
+
end
|
34
36
|
end
|
35
37
|
end
|
36
38
|
end
|
@@ -1,15 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_record_encryption/serializer_with_cast'
|
4
|
+
|
3
5
|
module ActiveRecordEncryption
|
4
6
|
class Type < ActiveRecord::Type::Value
|
5
7
|
using(ActiveRecordEncryption::SerializerWithCast)
|
6
8
|
|
7
9
|
delegate :type, :cast, to: :subtype
|
10
|
+
delegate :user_input_in_time_zone, to: :subtype # for ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
|
8
11
|
|
9
|
-
def initialize(
|
10
|
-
|
11
|
-
|
12
|
+
def initialize(
|
13
|
+
subtype: default_value,
|
14
|
+
encryption: ActiveRecordEncryption.default_encryption.clone,
|
15
|
+
**options
|
16
|
+
)
|
17
|
+
|
18
|
+
# Lookup encryptor from options[:encryption]
|
19
|
+
@encryptor = build_encryptor(encryption)
|
12
20
|
@binary = ActiveRecord::Type.lookup(:binary)
|
21
|
+
|
22
|
+
subtype = ActiveRecord::Type.lookup(subtype, **options) if subtype.is_a?(Symbol)
|
23
|
+
@subtype = subtype
|
13
24
|
end
|
14
25
|
|
15
26
|
def deserialize(value)
|
@@ -22,12 +33,34 @@ module ActiveRecordEncryption
|
|
22
33
|
binary.serialize(encryptor.encrypt(serialized)) unless serialized.nil?
|
23
34
|
end
|
24
35
|
|
36
|
+
def changed_in_place?(raw_old_value, value)
|
37
|
+
old_value = deserialize(raw_old_value)
|
38
|
+
@subtype.changed_in_place?(old_value, value)
|
39
|
+
end
|
40
|
+
|
25
41
|
private
|
26
42
|
|
27
|
-
attr_reader :
|
43
|
+
attr_reader :subtype, :binary, :encryptor
|
44
|
+
|
45
|
+
# NOTE: `ActiveRecord::Type.default_value` is not defined in Rails 5.0
|
46
|
+
def default_value
|
47
|
+
if ActiveRecord.gem_version < Gem::Version.create('5.1.0')
|
48
|
+
ActiveRecord::Type::Value.new
|
49
|
+
else
|
50
|
+
ActiveRecord::Type.default_value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_encryptor(options)
|
55
|
+
encryptor = options.delete(:encryptor)
|
28
56
|
|
29
|
-
|
30
|
-
|
57
|
+
if encryptor.is_a?(Symbol)
|
58
|
+
ActiveRecordEncryption::Encryptor.lookup(encryptor, **options)
|
59
|
+
elsif encryptor.is_a?(Class)
|
60
|
+
encryptor.new(options)
|
61
|
+
else
|
62
|
+
encryptor
|
63
|
+
end
|
31
64
|
end
|
32
65
|
end
|
33
66
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_encryption
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- alpaca-tc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: mysql2
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,36 +178,30 @@ dependencies:
|
|
164
178
|
- - ">="
|
165
179
|
- !ruby/object:Gem::Version
|
166
180
|
version: '0'
|
167
|
-
description:
|
181
|
+
description: Provides transparent encryption for ActiveRecord. You can protect your
|
182
|
+
data with any encryption algorithm you want.
|
168
183
|
email:
|
169
184
|
- alpaca-tc@alpaca.tc
|
170
185
|
executables: []
|
171
186
|
extensions: []
|
172
187
|
extra_rdoc_files: []
|
173
188
|
files:
|
174
|
-
-
|
175
|
-
- ".rspec"
|
176
|
-
- ".rubocop.yml"
|
177
|
-
- ".travis.yml"
|
178
|
-
- Appraisals
|
189
|
+
- CHANGELOG.md
|
179
190
|
- CODE_OF_CONDUCT.md
|
180
|
-
- Gemfile
|
181
|
-
- Guardfile
|
182
191
|
- LICENSE.txt
|
183
192
|
- README.md
|
184
|
-
- Rakefile
|
185
|
-
- active_record_encryption.gemspec
|
186
|
-
- gemfiles/5.0_stable.gemfile
|
187
|
-
- gemfiles/5.1_stable.gemfile
|
188
|
-
- gemfiles/5.2_stable.gemfile
|
189
193
|
- lib/active_record_encryption.rb
|
194
|
+
- lib/active_record_encryption/binary.rb
|
190
195
|
- lib/active_record_encryption/encrypted_attribute.rb
|
191
196
|
- lib/active_record_encryption/encryptor.rb
|
192
|
-
- lib/active_record_encryption/encryptor/
|
197
|
+
- lib/active_record_encryption/encryptor/active_support.rb
|
198
|
+
- lib/active_record_encryption/encryptor/aes_256_cbc.rb
|
199
|
+
- lib/active_record_encryption/encryptor/base.rb
|
200
|
+
- lib/active_record_encryption/encryptor/raw.rb
|
201
|
+
- lib/active_record_encryption/encryptor/registry.rb
|
193
202
|
- lib/active_record_encryption/exceptions.rb
|
194
203
|
- lib/active_record_encryption/quoter.rb
|
195
204
|
- lib/active_record_encryption/serializer_with_cast.rb
|
196
|
-
- lib/active_record_encryption/testing/test_cipher.rb
|
197
205
|
- lib/active_record_encryption/type.rb
|
198
206
|
- lib/active_record_encryption/version.rb
|
199
207
|
homepage: https://github.com/alpaca-tc/active_record_encryption
|
@@ -219,5 +227,5 @@ rubyforge_project:
|
|
219
227
|
rubygems_version: 2.7.3
|
220
228
|
signing_key:
|
221
229
|
specification_version: 4
|
222
|
-
summary:
|
230
|
+
summary: Transparent ActiveRecord encryption
|
223
231
|
test_files: []
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.rubocop.yml
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
AllCops:
|
2
|
-
Exclude:
|
3
|
-
- 'bin/*'
|
4
|
-
- 'db/**/*'
|
5
|
-
- 'vendor/**/*'
|
6
|
-
- '**/Rakefile'
|
7
|
-
- '**/config.ru'
|
8
|
-
- 'node_modules/**/*'
|
9
|
-
TargetRubyVersion: 2.5
|
10
|
-
DisplayCopNames: true
|
11
|
-
|
12
|
-
Performance:
|
13
|
-
Enabled: false
|
14
|
-
|
15
|
-
Bundler:
|
16
|
-
Enabled: false
|
17
|
-
|
18
|
-
Naming:
|
19
|
-
Enabled: false
|
20
|
-
|
21
|
-
Metrics/BlockNesting:
|
22
|
-
Enabled: false
|
23
|
-
|
24
|
-
Metrics/ClassLength:
|
25
|
-
Enabled: false
|
26
|
-
|
27
|
-
Metrics/LineLength:
|
28
|
-
Enabled: false
|
29
|
-
|
30
|
-
Metrics/MethodLength:
|
31
|
-
Enabled: false
|
32
|
-
|
33
|
-
Metrics/BlockLength:
|
34
|
-
Enabled: false
|
35
|
-
|
36
|
-
Metrics/ModuleLength:
|
37
|
-
Enabled: false
|
38
|
-
|
39
|
-
Style/AsciiComments:
|
40
|
-
Enabled: false
|
41
|
-
|
42
|
-
Style/BlockDelimiters:
|
43
|
-
Exclude:
|
44
|
-
- 'spec/**/*'
|
45
|
-
|
46
|
-
Style/Documentation:
|
47
|
-
Enabled: false
|
48
|
-
|
49
|
-
Style/BlockDelimiters:
|
50
|
-
Enabled: false
|
51
|
-
|
52
|
-
Style/DoubleNegation:
|
53
|
-
Enabled: false
|
54
|
-
|
55
|
-
Style/GuardClause:
|
56
|
-
Enabled: false
|
57
|
-
|
58
|
-
Style/ClassAndModuleChildren:
|
59
|
-
Enabled: false
|
60
|
-
|
61
|
-
Style/SpecialGlobalVars:
|
62
|
-
Enabled: false
|
63
|
-
|
64
|
-
Style/NumericPredicate:
|
65
|
-
Enabled: false
|
66
|
-
|
67
|
-
Style/Lambda:
|
68
|
-
Enabled: false
|
69
|
-
|
70
|
-
Layout/AlignParameters:
|
71
|
-
EnforcedStyle: with_fixed_indentation
|
72
|
-
|
73
|
-
Layout/MultilineMethodCallIndentation:
|
74
|
-
EnforcedStyle: indented
|
75
|
-
|
76
|
-
Lint/AmbiguousRegexpLiteral:
|
77
|
-
Enabled: false
|
data/.travis.yml
DELETED
data/Appraisals
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
appraise '5.0-stable' do
|
4
|
-
gem 'activerecord', '~> 5.0.0'
|
5
|
-
end
|
6
|
-
|
7
|
-
appraise '5.1-stable' do
|
8
|
-
gem 'activerecord', '~> 5.1.0'
|
9
|
-
end
|
10
|
-
|
11
|
-
appraise '5.2-stable' do
|
12
|
-
gem 'activerecord', git: 'https://github.com/rails/rails', branch: '5-2-stable'
|
13
|
-
end
|
data/Gemfile
DELETED
data/Guardfile
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
guard :rspec, cmd: 'bundle exec rspec' do
|
4
|
-
require 'guard/rspec/dsl'
|
5
|
-
dsl = Guard::RSpec::Dsl.new(self)
|
6
|
-
|
7
|
-
# Feel free to open issues for suggestions and improvements
|
8
|
-
|
9
|
-
# RSpec files
|
10
|
-
rspec = dsl.rspec
|
11
|
-
watch(rspec.spec_helper) { rspec.spec_dir }
|
12
|
-
watch(rspec.spec_support) { rspec.spec_dir }
|
13
|
-
watch(rspec.spec_files)
|
14
|
-
|
15
|
-
# Ruby files
|
16
|
-
ruby = dsl.ruby
|
17
|
-
dsl.watch_spec_files_for(ruby.lib_files)
|
18
|
-
|
19
|
-
# Rails files
|
20
|
-
rails = dsl.rails(view_extensions: %w[erb haml slim])
|
21
|
-
dsl.watch_spec_files_for(rails.app_files)
|
22
|
-
dsl.watch_spec_files_for(rails.views)
|
23
|
-
|
24
|
-
watch(rails.controllers) do |m|
|
25
|
-
[
|
26
|
-
rspec.spec.call("routing/#{m[1]}_routing"),
|
27
|
-
rspec.spec.call("controllers/#{m[1]}_controller"),
|
28
|
-
rspec.spec.call("acceptance/#{m[1]}")
|
29
|
-
]
|
30
|
-
end
|
31
|
-
|
32
|
-
# Rails config changes
|
33
|
-
watch(rails.spec_helper) { rspec.spec_dir }
|
34
|
-
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
35
|
-
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
36
|
-
|
37
|
-
# Capybara features specs
|
38
|
-
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
39
|
-
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
40
|
-
|
41
|
-
# Turnip features and steps
|
42
|
-
watch(%r{^spec/acceptance/(.+)\.feature$})
|
43
|
-
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
44
|
-
Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
guard :rubocop, all_on_start: false, cli: ['--auto-correct'] do
|
49
|
-
watch(/.+\.rb$/)
|
50
|
-
watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
|
51
|
-
end
|
data/Rakefile
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
lib = File.expand_path('lib', __dir__)
|
5
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
-
require 'active_record_encryption/version'
|
7
|
-
|
8
|
-
Gem::Specification.new do |spec|
|
9
|
-
spec.name = 'active_record_encryption'
|
10
|
-
spec.version = ActiveRecordEncryption::VERSION
|
11
|
-
spec.authors = ['alpaca-tc']
|
12
|
-
spec.email = ['alpaca-tc@alpaca.tc']
|
13
|
-
|
14
|
-
spec.summary = 'Write a short summary, because RubyGems requires one.'
|
15
|
-
spec.description = 'Write a longer description or delete this line.'
|
16
|
-
spec.homepage = 'https://github.com/alpaca-tc/active_record_encryption'
|
17
|
-
spec.license = 'MIT'
|
18
|
-
|
19
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
-
f.match(%r{^(test|spec|features)/})
|
21
|
-
end
|
22
|
-
|
23
|
-
spec.require_paths = ['lib']
|
24
|
-
|
25
|
-
spec.add_dependency 'activerecord', '>= 5.0'
|
26
|
-
|
27
|
-
spec.add_development_dependency 'appraisal'
|
28
|
-
spec.add_development_dependency 'bundler', '~> 1.16'
|
29
|
-
spec.add_development_dependency 'guard-rspec'
|
30
|
-
spec.add_development_dependency 'guard-rubocop'
|
31
|
-
spec.add_development_dependency 'mysql2', '< 0.5.0'
|
32
|
-
spec.add_development_dependency 'pry'
|
33
|
-
spec.add_development_dependency 'rake'
|
34
|
-
spec.add_development_dependency 'rspec'
|
35
|
-
spec.add_development_dependency 'rubocop'
|
36
|
-
spec.add_development_dependency 'sqlite3'
|
37
|
-
end
|
data/gemfiles/5.0_stable.gemfile
DELETED
data/gemfiles/5.1_stable.gemfile
DELETED
data/gemfiles/5.2_stable.gemfile
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'digest'
|
4
|
-
|
5
|
-
module ActiveRecordEncryption
|
6
|
-
module Testing
|
7
|
-
class TestCipher < ActiveRecordEncryption::Encryptor::Cipher
|
8
|
-
attr_reader :key
|
9
|
-
|
10
|
-
def initialize(key: SecureRandom.hex)
|
11
|
-
@key = key
|
12
|
-
end
|
13
|
-
|
14
|
-
def ==(other)
|
15
|
-
other.is_a?(self.class) && key == other.key
|
16
|
-
end
|
17
|
-
|
18
|
-
def encrypt(value)
|
19
|
-
super("#{value}#{key}")
|
20
|
-
end
|
21
|
-
|
22
|
-
def decrypt(value)
|
23
|
-
value = value.to_s
|
24
|
-
|
25
|
-
if value.match(/#{key}$/)
|
26
|
-
super(value.sub(/#{key}$/, ''))
|
27
|
-
else
|
28
|
-
raise InvalidMessage, 'invalid value given'
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|