From 117f91262ee1cbcf11335abae16b038df13fe84f Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 20:53:31 -0700 Subject: [PATCH 1/8] Fixed failing tests. - Tests were failing because the IV used to derive the encryption key was changing everytime the Proc was evaluated. - Other tests were failing because the default mode was :per_attribute_iv and the IV wasn't persisted because the column was missing. --- test/active_record_test.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 20abce58..a2f15ef9 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -10,6 +10,7 @@ def create_tables t.string :password t.string :encrypted_credentials t.binary :salt + t.binary :key_iv t.string :encrypted_email_salt t.string :encrypted_credentials_salt t.string :encrypted_email_iv @@ -23,10 +24,12 @@ def create_tables create_table :users do |t| t.string :login t.string :encrypted_password + t.string :encrypted_password_iv t.boolean :is_admin end create_table :prime_ministers do |t| t.string :encrypted_name + t.string :encrypted_name_iv end create_table :addresses do |t| t.binary :encrypted_street @@ -55,16 +58,17 @@ class UploadedFile; end class Person < ActiveRecord::Base self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt - attr_encrypted :email, :key => SECRET_KEY - attr_encrypted :credentials, :key => Proc.new { |user| Encryptor.encrypt(:value => user.salt, :key => SECRET_KEY, iv: SecureRandom.random_bytes(12)) }, :marshal => true + attr_encrypted :email, key: SECRET_KEY + attr_encrypted :credentials, key: Proc.new { |user| Encryptor.encrypt(value: user.salt, key: SECRET_KEY, iv: user.key_iv) }, marshal: true after_initialize :initialize_salt_and_credentials protected def initialize_salt_and_credentials + self.key_iv ||= SecureRandom.random_bytes(12) self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15] - self.credentials ||= { :username => 'example', :password => 'test' } + self.credentials ||= { username: 'example', password: 'test' } end end From c2b72ef4743aaa9847d37dcd2f073ee3f3bbac5b Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 21:33:03 -0700 Subject: [PATCH 2/8] Converted tests to Ruby 1.9.3 hash syntax. --- test/active_record_test.rb | 86 +++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/test/active_record_test.rb b/test/active_record_test.rb index a2f15ef9..cf3546fc 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -1,10 +1,10 @@ require_relative 'test_helper' -ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:' +ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') def create_tables silence_stream(STDOUT) do - ActiveRecord::Schema.define(:version => 1) do + ActiveRecord::Schema.define(version: 1) do create_table :people do |t| t.string :encrypted_email t.string :password @@ -77,34 +77,34 @@ class PersonWithValidation < Person end class PersonWithProcMode < Person - attr_encrypted :email, :key => SECRET_KEY, :mode => Proc.new { :per_attribute_iv_and_salt } - attr_encrypted :credentials, :key => SECRET_KEY, :mode => Proc.new { :single_iv_and_salt }, insecure_mode: true + attr_encrypted :email, key: SECRET_KEY, mode: Proc.new { :per_attribute_iv_and_salt } + attr_encrypted :credentials, key: SECRET_KEY, mode: Proc.new { :single_iv_and_salt }, insecure_mode: true end class Account < ActiveRecord::Base attr_accessor :key - attr_encrypted :password, :key => Proc.new {|account| account.key} + attr_encrypted :password, key: Proc.new {|account| account.key} end class PersonWithSerialization < ActiveRecord::Base self.table_name = 'people' - attr_encrypted :email, :key => SECRET_KEY + attr_encrypted :email, key: SECRET_KEY serialize :password end class UserWithProtectedAttribute < ActiveRecord::Base self.table_name = 'users' - attr_encrypted :password, :key => SECRET_KEY + attr_encrypted :password, key: SECRET_KEY attr_protected :is_admin if ::ActiveRecord::VERSION::STRING < "4.0" end class PersonUsingAlias < ActiveRecord::Base self.table_name = 'people' - attr_encryptor :email, :key => SECRET_KEY + attr_encryptor :email, key: SECRET_KEY end class PrimeMinister < ActiveRecord::Base - attr_encrypted :name, :marshal => true, :key => SECRET_KEY + attr_encrypted :name, marshal: true, key: SECRET_KEY end class Address < ActiveRecord::Base @@ -119,11 +119,11 @@ class ActiveRecordTest < Minitest::Test def setup ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) } create_tables - Account.create!(:key => SECRET_KEY, :password => "password") + Account.create!(key: SECRET_KEY, password: "password") end def test_should_encrypt_email - @person = Person.create :email => 'test@example.com' + @person = Person.create(email: 'test@example.com') refute_nil @person.encrypted_email refute_equal @person.email, @person.encrypted_email assert_equal @person.email, Person.first.email @@ -147,36 +147,36 @@ def test_should_validate_presence_of_email end def test_should_encrypt_decrypt_with_iv - @person = Person.create :email => 'test@example.com' + @person = Person.create(email: 'test@example.com') @person2 = Person.find(@person.id) refute_nil @person2.encrypted_email_iv assert_equal 'test@example.com', @person2.email end def test_should_ensure_attributes_can_be_deserialized - @person = PersonWithSerialization.new :email => 'test@example.com', :password => %w(an array of strings) + @person = PersonWithSerialization.new(email: 'test@example.com', password: %w(an array of strings)) @person.save assert_equal @person.password, %w(an array of strings) end def test_should_create_an_account_regardless_of_arguments_order - Account.create!(:key => SECRET_KEY, :password => "password") - Account.create!(:password => "password" , :key => SECRET_KEY) + Account.create!(key: SECRET_KEY, password: "password") + Account.create!(password: "password" , key: SECRET_KEY) end def test_should_set_attributes_regardless_of_arguments_order # minitest does not implement `assert_nothing_raised` https://github.com/seattlerb/minitest/issues/112 - Account.new.attributes = { :password => "password" , :key => SECRET_KEY } + Account.new.attributes = { password: "password", key: SECRET_KEY } end def test_should_preserve_hash_key_type - hash = { :foo => 'bar' } - account = Account.create!(:key => hash) + hash = { foo: 'bar' } + account = Account.create!(key: hash) assert_equal account.key, hash end def test_should_create_changed_predicate - person = Person.create!(:email => 'test@example.com') + person = Person.create!(email: 'test@example.com') assert !person.email_changed? person.email = 'test2@example.com' assert person.email_changed? @@ -184,7 +184,7 @@ def test_should_create_changed_predicate def test_should_create_was_predicate original_email = 'test@example.com' - person = Person.create!(:email => original_email) + person = Person.create!(email: original_email) assert !person.email_was person.email = 'test2@example.com' assert_equal person.email_was, original_email @@ -192,48 +192,48 @@ def test_should_create_was_predicate if ::ActiveRecord::VERSION::STRING > "4.0" def test_should_assign_attributes - @user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false - @user.attributes = ActionController::Parameters.new(:login => 'modified', :is_admin => true).permit(:login) + @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) + @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) assert_equal 'modified', @user.login end def test_should_not_assign_protected_attributes - @user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false - @user.attributes = ActionController::Parameters.new(:login => 'modified', :is_admin => true).permit(:login) + @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) + @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true).permit(:login) assert !@user.is_admin? end def test_should_raise_exception_if_not_permitted - @user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false + @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) assert_raises ActiveModel::ForbiddenAttributesError do - @user.attributes = ActionController::Parameters.new(:login => 'modified', :is_admin => true) + @user.attributes = ActionController::Parameters.new(login: 'modified', is_admin: true) end end def test_should_raise_exception_on_init_if_not_permitted assert_raises ActiveModel::ForbiddenAttributesError do - @user = UserWithProtectedAttribute.new ActionController::Parameters.new(:login => 'modified', :is_admin => true) + @user = UserWithProtectedAttribute.new ActionController::Parameters.new(login: 'modified', is_admin: true) end end else def test_should_assign_attributes - @user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false - @user.attributes = {:login => 'modified', :is_admin => true} + @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) + @user.attributes = { login: 'modified', is_admin: true } assert_equal 'modified', @user.login end def test_should_not_assign_protected_attributes - @user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false - @user.attributes = {:login => 'modified', :is_admin => true} + @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) + @user.attributes = { login: 'modified', is_admin: true } assert !@user.is_admin? end def test_should_assign_protected_attributes - @user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false + @user = UserWithProtectedAttribute.new(login: 'login', is_admin: false) if ::ActiveRecord::VERSION::STRING > "3.1" - @user.send :assign_attributes, {:login => 'modified', :is_admin => true}, :without_protection => true + @user.send(:assign_attributes, { login: 'modified', is_admin: true }, without_protection: true) else - @user.send :attributes=, {:login => 'modified', :is_admin => true}, false + @user.send(:attributes=, { login: 'modified', is_admin: true }, false) end assert @user.is_admin? end @@ -245,7 +245,7 @@ def test_should_allow_assignment_of_nil_attributes end def test_should_allow_proc_based_mode - @person = PersonWithProcMode.create :email => 'test@example.com', :credentials => 'password123' + @person = PersonWithProcMode.create(email: 'test@example.com', credentials: 'password123') # Email is :per_attribute_iv_and_salt assert_equal @person.class.encrypted_attributes[:email][:mode].class, Proc @@ -279,21 +279,21 @@ def test_that_alias_encrypts_column # See https://github.com/attr-encrypted/attr_encrypted/issues/68 def test_should_invalidate_virtual_attributes_on_reload - pm = PrimeMinister.new(:name => 'Winston Churchill') - pm.save! - assert_equal 'Winston Churchill', pm.name - pm.name = 'Neville Chamberlain' - assert_equal 'Neville Chamberlain', pm.name + old_pm_name = 'Winston Churchill' + new_pm_name = 'Neville Chamberlain' + pm = PrimeMinister.create!(name: old_pm_name) + assert_equal old_pm_name, pm.name + pm.name = new_pm_name + assert_equal new_pm_name, pm.name result = pm.reload assert_equal pm, result - assert_equal 'Winston Churchill', pm.name + assert_equal old_pm_name, pm.name end def test_should_save_encrypted_data_as_binary street = '123 Elm' - address = Address.new(street: street) - address.save! + address = Address.create!(street: street) refute_equal address.encrypted_street, street assert_equal Address.first.street, street end From f9820256c1cd85f89204fd04b90b1c647b94c183 Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 20:55:42 -0700 Subject: [PATCH 3/8] Fixed ActiveModel::Dirty methods. - Decrypting the attribute_was value requires some special handling now that we're changing the IV during every encryption operation. Namely, we have to evaluate all the options for the attribute and pass in the encrypted_attribute_iv_was and the encrypted_attribute_salt_was to properly decrypt the encrypted_attribute_was value. - We delete the :iv, :salt, and :operation keys from the encrypted_attributes hash that were used to decrypt the encrypted_attribute_was value just to be safe. Normally those values are not persisted in the encrypted_attributes hash. --- lib/attr_encrypted/adapters/active_record.rb | 10 ++++++++-- test/active_record_test.rb | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/attr_encrypted/adapters/active_record.rb b/lib/attr_encrypted/adapters/active_record.rb index 203dbe70..8a4961df 100644 --- a/lib/attr_encrypted/adapters/active_record.rb +++ b/lib/attr_encrypted/adapters/active_record.rb @@ -53,11 +53,17 @@ def attr_encrypted(*attrs) options.merge! encrypted_attributes[attr] define_method("#{attr}_changed?") do - send(attr) != decrypt(attr, send("#{options[:attribute]}_was")) + if send("#{options[:attribute]}_changed?") + send(attr) != send("#{attr}_was") + end end define_method("#{attr}_was") do - decrypt(attr, send("#{options[:attribute]}_was")) if send("#{attr}_changed?") + iv_and_salt = { iv: send("#{options[:attribute]}_iv_was"), salt: send("#{options[:attribute]}_salt_was"), operation: :decrypting } + encrypted_attributes[attr].merge!(iv_and_salt) + evaluated_options = evaluated_attr_encrypted_options_for(attr) + [:iv, :salt, :operation].each { |key| encrypted_attributes[attr].delete(key) } + self.class.decrypt(attr, send("#{options[:attribute]}_was"), evaluated_options) end alias_method "#{attr}_before_type_cast", attr diff --git a/test/active_record_test.rb b/test/active_record_test.rb index cf3546fc..7335b741 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -177,7 +177,11 @@ def test_should_preserve_hash_key_type def test_should_create_changed_predicate person = Person.create!(email: 'test@example.com') - assert !person.email_changed? + refute person.email_changed? + person.email = 'test@example.com' + refute person.email_changed? + person.email = nil + assert person.email_changed? person.email = 'test2@example.com' assert person.email_changed? end @@ -185,9 +189,9 @@ def test_should_create_changed_predicate def test_should_create_was_predicate original_email = 'test@example.com' person = Person.create!(email: original_email) - assert !person.email_was + assert_equal original_email, person.email_was person.email = 'test2@example.com' - assert_equal person.email_was, original_email + assert_equal original_email, person.email_was end if ::ActiveRecord::VERSION::STRING > "4.0" From 1c4dc70d053031b9dfb35377131463c79ad61d95 Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 21:01:34 -0700 Subject: [PATCH 4/8] Added loading the iv and salt from the encrypted_attributes hash. - This is required for passing in the IV and salt to decrypted the encrypted_attribute_was ciphertext. - The options evaluated should be used from the instance level, not the class level. --- lib/attr_encrypted.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index 22478381..78c69c3a 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -350,7 +350,7 @@ def encrypted_attributes def evaluated_attr_encrypted_options_for(attribute) evaluated_options = Hash.new attribute_option_value = encrypted_attributes[attribute.to_sym][:attribute] - self.class.encrypted_attributes[attribute.to_sym].map do |option, value| + encrypted_attributes[attribute.to_sym].map do |option, value| evaluated_options[option] = evaluate_attr_encrypted_option(value) end @@ -383,7 +383,7 @@ def evaluate_attr_encrypted_option(option) def load_iv_for_attribute(attribute, options) encrypted_attribute_name = options[:attribute] encode_iv = options[:encode_iv] - iv = send("#{encrypted_attribute_name}_iv") + iv = options[:iv] || send("#{encrypted_attribute_name}_iv") if options[:operation] == :encrypting begin iv = generate_iv(options[:algorithm]) @@ -407,7 +407,7 @@ def generate_iv(algorithm) def load_salt_for_attribute(attribute, options) encrypted_attribute_name = options[:attribute] encode_salt = options[:encode_salt] - salt = send("#{encrypted_attribute_name}_salt") + salt = options[:salt] || send("#{encrypted_attribute_name}_salt") if (salt == nil) salt = SecureRandom.random_bytes salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt From 5834ef49af5ff9c7e9b9e6a025522b7c37584ea8 Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 21:03:48 -0700 Subject: [PATCH 5/8] Now using Encryptor v3.0.0. --- attr_encrypted.gemspec | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/attr_encrypted.gemspec b/attr_encrypted.gemspec index 16d31d01..f6fcfe1a 100644 --- a/attr_encrypted.gemspec +++ b/attr_encrypted.gemspec @@ -28,7 +28,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.0.0' - s.add_dependency('encryptor', ['~> 2.0.0']) + s.add_dependency('encryptor', ['~> 3.0.0']) # support for testing with specific active record version activerecord_version = if ENV.key?('ACTIVERECORD') "~> #{ENV['ACTIVERECORD']}" @@ -55,6 +55,9 @@ Gem::Specification.new do |s| s.cert_chain = ['certs/saghaulor.pem'] s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/ - s.post_install_message = "\n\n\nWARNING: Several insecure default options and features have been deprecated in attr_encrypted v2.0.0. Please see the README for more details.\n\n\n" + s.post_install_message = "\n\n\nWARNING: Several insecure default options and features were deprecated in attr_encrypted v2.0.0.\n +Additionally, there was a bug in Encryptor v2.0.0 that insecurely encrypted data when using an AES-*-GCM algorithm.\n +This bug was fixed but introduced breaking changes between v2.x and v3.x.\n +Please see the README for more information regarding upgrading to attr_encrypted v3.0.0.\n\n\n" end From 9e22d4ade3b46a2c176c08424d1460e439e202db Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 21:04:22 -0700 Subject: [PATCH 6/8] Updated README to include instructions on how to move from attr_encrypted v2 to v3. --- README.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 81fcf887..b2754f83 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It works with ANY class, however, you get a few extra features when you're using Add attr_encrypted to your gemfile: ```ruby - gem "attr_encrypted", "~> 2.0.0" + gem "attr_encrypted", "~> 3.0.0" ``` Then install the gem: @@ -37,22 +37,22 @@ If you're using a PORO, you have to do a little bit more work by extending the c extend AttrEncrypted attr_accessor :name attr_encrypted :ssn, key: 'This is a key that is 256 bits!!' - + def load # loads the stored data end - + def save # saves the :name and :encrypted_ssn attributes somewhere (e.g. filesystem, database, etc) end end - + user = User.new user.ssn = '123-45-6789' user.ssn # returns the unencrypted object ie. '123-45-6789' user.encrypted_ssn # returns the encrypted version of :ssn user.save - + user = User.load user.ssn # decrypts :encrypted_ssn and returns '123-45-6789' ``` @@ -242,7 +242,7 @@ Lets suppose you'd like to use this custom encryptor class: def self.silly_encrypt(options) (options[:value] + options[:secret_key]).reverse end - + def self.silly_decrypt(options) options[:value].reverse.gsub(/#{options[:secret_key]}$/, '') end @@ -374,12 +374,12 @@ Backwards compatibility is supported by providing a special option that is passe The `:insecure_mode` option will allow encryptor to ignore the new security requirements. It is strongly advised that if you use this older insecure behavior that you migrate to the newer more secure behavior. -## Upgrading from attr_encrypted v1.x to v2.x +## Upgrading from attr_encrypted v1.x to v3.x Modify your gemfile to include the new version of attr_encrypted: ```ruby - gem attr_encrypted, "~> 2.0.0" + gem attr_encrypted, "~> 3.0.0" ``` The update attr_encrypted: @@ -390,6 +390,30 @@ The update attr_encrypted: Then modify your models using attr\_encrypted to account for the changes in default options. Specifically, pass in the `:mode` and `:algorithm` options that you were using if you had not previously done so. If your key is insufficient length relative to the algorithm that you use, you should also pass in `insecure_mode: true`; this will prevent Encryptor from raising an exception regarding insufficient key length. Please see the Deprecations sections for more details including an example of how to specify your model with default options from attr_encrypted v1.x. +## Upgrading from attr_encrypted v2.x to v3.x + +A bug was discovered in Encryptor v2.0.0 that inccorectly set the IV when using an AES-\*-GCM algorithm. Unfornately fixing this major security issue results in the inability to decrypt records encrypted using an AES-*-GCM algorithm from Encryptor v2.0.0. Please see [Upgrading to Encryptor v3.0.0](https://github.com/attr-encrypted/encryptor#upgrading-from-v200-to-v300) for more info. + +It is strongly advised that you re-encrypt your data encrypted with Encryptor v2.0.0. However, you'll have to take special care to re-encrypt. To decrypt data encrypted with Encryptor v2.0.0 using an AES-\*-GCM algorithm you can use the `:v2_gcm_iv` option. + +It is recommended that you implement a strategy to insure that you do not mix the encryption implementations of Encryptor. One way to do this is to re-encrypt everything while your application is offline.Another way is to add a column that keeps track of what implementation was used. The path that you choose will depend on your situtation. Below is an example of how you might go about re-encrypting your data. + +```ruby + class User + attr_encrypted :ssn, key: :encryption_key, v2_gcm_iv: :is_decrypting?(:ssn) + + def is_decrypting?(attribute) + encrypted_atributes[attribute][operation] == :decrypting + end + end + + User.all.each do |user| + old_ssn = user.ssn + user.ssn= old_ssn + user.save + end +``` + ## Things to consider before using attr_encrypted #### Searching, joining, etc From df5ce0b66ed4846b44903cf528e2dfeaa5626642 Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 21:06:30 -0700 Subject: [PATCH 7/8] Updated CHANGELOG to include v3.0.0 changes. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39031114..feacab21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # attr_encrypted # +## 3.0.0 ## +* Changed: Updated gemspec to use Encryptor v3.0.0. (@saghaulor) +* Changed: Updated README with instructions related to moving from v2.0.0 to v3.0.0. (@saghaulor) +* Fixed: ActiveModel::Dirty methods in the ActiveRecord adapter. (@saghaulor) + ## 2.0.0 ## * Added: Now using Encryptor v2.0.0 (@saghaulor) * Added: Options are copied to the instance. (@saghaulor) From 715b0cc8f3dc1519d0067c753ff7af513cbd04f6 Mon Sep 17 00:00:00 2001 From: Stephen Aghaulor Date: Mon, 28 Mar 2016 21:40:29 -0700 Subject: [PATCH 8/8] Bump version to v3.0.0. --- lib/attr_encrypted/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/attr_encrypted/version.rb b/lib/attr_encrypted/version.rb index e402d7a2..77230728 100644 --- a/lib/attr_encrypted/version.rb +++ b/lib/attr_encrypted/version.rb @@ -1,7 +1,7 @@ module AttrEncrypted # Contains information about this gem's version module Version - MAJOR = 2 + MAJOR = 3 MINOR = 0 PATCH = 0