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 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