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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9369affd95f719bd5c13f2f1fbc5dd41a6a3c0f04f0b33da74d6834d6954f243
4
- data.tar.gz: 1afd356f1754db7c05ced1749705bf9d56632597ee3380c1b34f96b455f631d4
3
+ metadata.gz: f992a2aae3e31c5c369213e30abe9cb77896e7f92ac6fcdb7674e75e20e91f8e
4
+ data.tar.gz: 449de2e8b89d85509650bd5c38615fa9aa93b6585fce3e1e0b8e0ddd9db73e3b
5
5
  SHA512:
6
- metadata.gz: d25b721fa158761b2f43270d343386800c585e69c5b42db29b047daebefc0228d121ef7b0b8dc059049cc4a918f974b9bed8aec391e8039f3c42c9e75fc0bcef
7
- data.tar.gz: 9182542eb22718dd4379afd6b757b37d994e99ccdc1c216d2c6a757991c76060bb51338164db351c5a56e1806d955761142c631fdfab54ae5ca4239ddbacaf13
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
- Decorate encrypted binary column with attribute that transparently encrypt and decrypt sensitive data.
6
- It uses the ActiveRecord's Attribute API, and it is a simpler implementation than other gems.
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
- ```ruby
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
- ### Define serialized type in application
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
- $ post = Post.new(name: 'Author name', published_on: Date.current)
33
- #=> #<Post:0x00007f92169ac158 id: nil, name: "Author name", published_on: Thu, 29 Mar 2018>
34
- > post.save
35
- DEBUG -- : (0.1ms) begin transaction
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
- ### `#encrypted_attribute` options
27
+ ## Options
41
28
 
42
- You can pass the same arguments as [ActiveRecord::Attributes.attribute](https://apidock.com/rails/ActiveRecord/Attributes/ClassMethods/attribute)
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
- ### Migration
56
+ ## Supported Available Encryptors
57
+
58
+ There are four supported encryptors: `:active_support`, `:aes_256_cbc`.
53
59
 
54
- Create or modify the table that your model uses to add a column like the following:
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
- **NOTE: Default limit of binary is too long. Please set limit of encrypted columns**
69
+ ## Customize encryptor
70
+
71
+ You can easilly add your encryptor.
57
72
 
58
73
  ```ruby
59
- -ActiveRecord::Schema.define do
60
- create_table(:posts, force: true) do |t|
61
- t.binary :name, limit: 256
62
- t.binary :point, limit: 256
63
- t.binary :price, limit: 256
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
- *Tips* How to calculate limit of encrypted column? - It depends on algorithm of cipher.
103
+ ### Migration
69
104
 
70
- ## Development
105
+ Create or modify the table that your model like the following:
71
106
 
72
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
107
+ **NOTE: Default limit of ActiveRecord's binary column is too long. Please set limit(< 0xfff) for encrypted columns.**
73
108
 
74
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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 'active_record'
4
- require 'active_support/core_ext/module/attribute_accessors_per_thread'
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
- require 'active_record_encryption/version'
8
- require 'active_record_encryption/serializer_with_cast'
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
- thread_mattr_accessor(:cipher)
21
+ # Register `:encryption` type
22
+ ActiveRecord::Type.register(:encryption, ActiveRecordEncryption::Type)
17
23
 
18
- def self.with_cipher(new_cipher)
19
- previous = cipher
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 = name.to_s
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
- class << self
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
- def decrypt(value, cipher: ActiveRecordEncryption.cipher)
14
- raise_missing_cipher_error unless cipher
15
- cipher.decrypt(value)
16
- end
17
-
18
- private
13
+ class << self
14
+ attr_reader :registry
19
15
 
20
- def value_to_string(value)
21
- ActiveRecordEncryption::Quoter.instance.type_cast(value)
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 raise_missing_cipher_error
25
- raise(ActiveRecordEncryption::MissingCipherError, 'missing cipher')
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
- class Cipher
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
@@ -2,6 +2,5 @@
2
2
 
3
3
  module ActiveRecordEncryption
4
4
  class Error < StandardError; end
5
- class MissingCipherError < Error; end
6
5
  class InvalidMessage < Error; end
7
6
  end
@@ -28,9 +28,11 @@ module ActiveRecordEncryption
28
28
  end
29
29
 
30
30
  # Backport Rails5.2
31
- refine ActiveModel::Type::DateTime do
32
- def serialize(value)
33
- super(cast(value))
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(name, subtype)
10
- @name = name
11
- @subtype = subtype
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 :name, :subtype, :binary
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
- def encryptor
30
- ActiveRecordEncryption::Encryptor
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordEncryption
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  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.1.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-04-06 00:00:00.000000000 Z
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: Write a longer description or delete this line.
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
- - ".gitignore"
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/cipher.rb
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: Write a short summary, because RubyGems requires one.
230
+ summary: Transparent ActiveRecord encryption
223
231
  test_files: []
data/.gitignore DELETED
@@ -1,17 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- Gemfile.lock
11
-
12
- # rspec failure tracking
13
- .rspec_status
14
-
15
- /gemfiles/*.lock
16
- /gemfiles/**/config
17
- /gemfiles/.bundle
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
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
@@ -1,12 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.3.6
5
- - 2.4.3
6
- - 2.5.0
7
- before_install: gem install bundler -v 1.16.1
8
- gemfile:
9
- - gemfiles/5.0_stable.gemfile
10
- - gemfiles/5.1_stable.gemfile
11
- - gemfiles/5.2_stable.gemfile
12
- script: bundle exec rspec
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
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
-
7
- # Specify your gem's dependencies in activerecord_encryption.gemspec
8
- gemspec
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,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
@@ -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
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file was generated by Appraisal
4
-
5
- source 'https://rubygems.org'
6
-
7
- gem 'activerecord', '~> 5.0.0'
8
-
9
- gemspec path: '../'
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file was generated by Appraisal
4
-
5
- source 'https://rubygems.org'
6
-
7
- gem 'activerecord', '~> 5.1.0'
8
-
9
- gemspec path: '../'
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This file was generated by Appraisal
4
-
5
- source 'https://rubygems.org'
6
-
7
- gem 'activerecord', git: 'https://github.com/rails/rails', branch: '5-2-stable'
8
-
9
- gemspec path: '../'
@@ -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