active_record_encryption 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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
|
-
[![Build Status](https://travis-ci.org/alpaca-tc/active_record_encryption.png)](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
|