Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace AesNew provider with ActiveSupport::MessageEncryptor #140

Merged
merged 1 commit into from
Dec 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
language: ruby

rvm:
- 2.1.10
- 2.2.5
- 2.3.1
- 2.2.7
- 2.3.4
- 2.4.1

gemfile:
- gemfiles/activerecord_4_2.gemfile
- gemfiles/activerecord_5_0.gemfile
- gemfiles/activerecord_5_1.gemfile

matrix:
exclude:
- gemfile: gemfiles/activerecord_5_1.gemfile
rvm: 2.1.10

addons:
postgresql: 9.3

Expand Down
5 changes: 5 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ appraise "activerecord_5_0" do
gem "activerecord", "~> 5.0.0"
gem "activesupport", "~> 5.0.0"
end

appraise "activerecord_5_1" do
gem "activerecord", "~> 5.1.0"
gem "activesupport", "~> 5.1.0"
end
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ is a simple class that does 3 things.
Note: Any options defined using `crypt_keeper` will be passed to `new` as a
hash.

You can see an AES example [here](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes_new.rb).
You can see an example [here](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/active_support.rb).

## Why?

Expand All @@ -27,7 +27,7 @@ simple that *just works*.

```ruby
class MyModel < ActiveRecord::Base
crypt_keeper :field, :other_field, :encryptor => :aes_new, :key => 'super_good_password', salt: 'salt'
crypt_keeper :field, :other_field, encryptor: :active_support, key: 'super_good_password', salt: 'salt'
end

model = MyModel.new(field: 'sometext')
Expand All @@ -53,7 +53,7 @@ You can force an encoding on the plaintext before encryption and after decryptio

```ruby
class MyModel < ActiveRecord::Base
crypt_keeper :field, :other_field, :encryptor => :aes_new, :key => 'super_good_password', salt: 'salt', :encoding => 'UTF-8'
crypt_keeper :field, :other_field, encryptor: :active_support, key: 'super_good_password', salt: 'salt', encoding: 'UTF-8'
end

model = MyModel.new(field: 'Tromsø')
Expand All @@ -68,7 +68,7 @@ If you are working with an existing table you would like to encrypt, you must us

```ruby
class MyExistingModel < ActiveRecord::Base
crypt_keeper :field, :other_field, :encryptor => :aes_new, :key => 'super_good_password', salt: 'salt'
crypt_keeper :field, :other_field, encryptor: :active_support, key: 'super_good_password', salt: 'salt'
end

MyExistingModel.encrypt_table!
Expand All @@ -78,10 +78,10 @@ Running `encrypt_table!` will encrypt all rows in the database using the encrypt

## Supported Available Encryptors

There are four supported encryptors: `aes_new`, `mysql_aes_new`, `postgres_pgp`, `postgres_pgp_public_key`.
There are four supported encryptors: `active_support`, `mysql_aes_new`, `postgres_pgp`, `postgres_pgp_public_key`.

* [AES New](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes_new.rb)
* Encryption is peformed using AES-256 via OpenSSL.
* [ActiveSupport](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/active_support.rb)
* Encryption is performed using [ActiveSupport::MessageEncryptor](http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html)
* Passphrases are derived using [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)

* [MySQL AES New](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/mysql_aes_new.rb)
Expand Down Expand Up @@ -111,8 +111,8 @@ There are four supported encryptors: `aes_new`, `mysql_aes_new`, `postgres_pgp`,
## Searching
Searching ciphertext is a complex problem that varies depending on the encryption algorithm you choose. All of the bundled providers include search support, but they have some caveats.

* AES
* The Ruby implementation of AES uses a random initialization vector. The same plaintext encrypted multiple times will have different output each time for the ciphertext. Since this is the case, it is not possible to search leveraging the database. Database rows will need to be filtered in memory. It is suggested that you use a scope or ActiveRecord batches to narrow the results before seaching them.
* ActiveSupport::MessageEncryptor
* ActiveSupport's MessageEncryptor uses a random initialization vector when generating keys. The same plaintext encrypted multiple times will have different output each time for the ciphertext. Since this is the case, it is not possible to search leveraging the database. Database rows will need to be filtered in memory. It is suggested that you use a scope or ActiveRecord batches to narrow the results before seaching them.

* Mysql AES
* Surprisingly, MySQL's implementation of AES does not use a random initialization vector. The column containing the ciphertext can be indexed and searched quickly.
Expand Down Expand Up @@ -157,10 +157,35 @@ as a string or an underscored symbol

```ruby
class MyModel < ActiveRecord::Base
crypt_keeper :field, :other_field, :encryptor => :my_encryptor, :key => 'super_good_password'
crypt_keeper :field, :other_field, encryptor: :my_encryptor, key: 'super_good_password'
end
```

## Migrating from CryptKeeper 1.x to 2.0

CryptKeeper 2.0 removes the AES encryptor due to security issues in the
underlying AES gem. If you were previously using the `aes_new` encryptor, you
will need to follow these instructions to reencrypt your data.

The general migration path is as follows:

1. Enable maintenance mode in any live apps
2. Backup database
3. Decrypt tables: TableName.decrypt_table!
4. Update to 2.0.0.rc1 in your app. Update the encryptor to use :active_support
5. Encrypt tables: `TableName.encrypt_table!`
6. Verify data can be decrypted: `TableName.first`
7. Disable maintenance mode if necessary

In case you experience problems, the rollback procedure is as follows:

1. Enable maintenance mode
2. Backup database again
3. Restore first database dump, from before CryptKeeper 2.0.0.rc1
4. Verify data can be decrypted
5. Disable maintenance mode
6. Let us know what happened :(

## Requirements

CryptKeeper has been tested against ActiveRecord 4.2 and 5.0 using Ruby
Expand Down
3 changes: 2 additions & 1 deletion crypt_keeper.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"]
gem.version = CryptKeeper::VERSION

gem.post_install_message = "WARNING: CryptKeeper 2.0 contains breaking changes and may require you to reencrypt your data! Please view the README at https://github.com/jmazzi/crypt_keeper for more information."

gem.add_runtime_dependency 'activerecord', '>= 4.2', '< 5.2'
gem.add_runtime_dependency 'activesupport', '>= 4.2', '< 5.2'
gem.add_runtime_dependency 'aes', '~> 0.5.0'
gem.add_runtime_dependency 'armor', '~> 0.0.2'

gem.add_development_dependency 'rspec', '~> 3.5.0'
Expand Down
8 changes: 8 additions & 0 deletions gemfiles/activerecord_5_0.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "activerecord", "~> 5.0.0"
gem "activesupport", "~> 5.0.0"

gemspec :path => "../"
4 changes: 2 additions & 2 deletions gemfiles/activerecord_5_1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

source "https://rubygems.org"

gem "activerecord", "~> 5.1"
gem "activesupport", "~> 5.1"
gem "activerecord", "~> 5.1.0"
gem "activesupport", "~> 5.1.0"

gemspec :path => "../"
2 changes: 1 addition & 1 deletion lib/crypt_keeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require 'crypt_keeper/model'
require 'crypt_keeper/helper'
require 'crypt_keeper/provider/base'
require 'crypt_keeper/provider/aes_new'
require 'crypt_keeper/provider/active_support'
require 'crypt_keeper/provider/mysql_aes_new'
require 'crypt_keeper/provider/postgres_base'
require 'crypt_keeper/provider/postgres_pgp'
Expand Down
1 change: 1 addition & 0 deletions lib/crypt_keeper/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def execute_sql(query, new_transaction: false)

module DigestPassphrase
def digest_passphrase(key, salt)
require "armor"
raise ArgumentError.new("Missing :key") if key.blank?
raise ArgumentError.new("Missing :salt") if salt.blank?
::Armor.digest(key, salt)
Expand Down
2 changes: 1 addition & 1 deletion lib/crypt_keeper/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

module CryptKeeper
module Model
extend ActiveSupport::Concern
extend ::ActiveSupport::Concern

# Public: Ensures that each field exist and is of type text. This prevents
# encrypted data from being truncated.
Expand Down
51 changes: 51 additions & 0 deletions lib/crypt_keeper/provider/active_support.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require "active_support/message_encryptor"

module CryptKeeper
module Provider
class ActiveSupport < Base
attr_reader :encryptor

# Public: Initializes the encryptor
#
# options - A hash, :key and :salt are required
#
# Returns nothing.
def initialize(options = {})
key = options.fetch(:key)
salt = options.fetch(:salt)

@encryptor = ::ActiveSupport::MessageEncryptor.new \
::ActiveSupport::KeyGenerator.new(key).generate_key(salt, 32)
end

# Public: Encrypts a string
#
# value - Plaintext value
#
# Returns an encrypted string
def encrypt(value)
encryptor.encrypt_and_sign(value)
end

# Public: Decrypts a string
#
# value - Cipher text
#
# Returns a plaintext string
def decrypt(value)
encryptor.decrypt_and_verify(value)
end

# Public: Searches the table
#
# records - ActiveRecord::Relation
# field - Field name to match
# criteria - Value to match
#
# Returns an Enumerable
def search(records, field, criteria)
records.select { |record| record[field] == criteria }
end
end
end
end
53 changes: 0 additions & 53 deletions lib/crypt_keeper/provider/aes_new.rb

This file was deleted.

2 changes: 1 addition & 1 deletion lib/crypt_keeper/provider/mysql_aes_new.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MysqlAesNew < Base
#
# options - A hash, :key and :salt are required
def initialize(options = {})
ActiveSupport.run_load_hooks(:crypt_keeper_mysql_aes_log, self)
::ActiveSupport.run_load_hooks(:crypt_keeper_mysql_aes_log, self)
@key = digest_passphrase(options[:key], options[:salt])
end

Expand Down
2 changes: 1 addition & 1 deletion lib/crypt_keeper/provider/postgres_pgp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class PostgresPgp < PostgresBase
#
# options - A hash, :key is required
def initialize(options = {})
ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)
::ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)

@key = options.fetch(:key) do
raise ArgumentError, "Missing :key"
Expand Down
2 changes: 1 addition & 1 deletion lib/crypt_keeper/provider/postgres_pgp_public_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class PostgresPgpPublicKey < PostgresBase
attr_accessor :key

def initialize(options = {})
ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)
::ActiveSupport.run_load_hooks(:crypt_keeper_postgres_pgp_log, self)

@key = options.fetch(:key) do
raise ArgumentError, "Missing :key"
Expand Down
10 changes: 5 additions & 5 deletions spec/crypt_keeper/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
end

context "Encodings" do
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' }
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support, encoding: 'utf-8' }

it "forces the encoding on decrypt" do
record = subject.create!(storage: 'Tromsø')
Expand All @@ -137,7 +137,7 @@
end

context "Initial Table Encryption" do
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new }
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support }

before do
subject.delete_all
Expand All @@ -146,14 +146,14 @@
end

it "encrypts the table" do
expect { subject.first(5).map(&:storage) }.to raise_error(OpenSSL::Cipher::CipherError)
expect { subject.first(5).map(&:storage) }.to raise_error(ActiveSupport::MessageVerifier::InvalidSignature)
subject.encrypt_table!
expect { subject.first(5).map(&:storage) }.not_to raise_error
end
end

context "Table Decryption (Reverse of Initial Table Encryption)" do
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new }
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support }
let!(:storage_entries) { 5.times.map { |i| "testing#{i}" } }

before do
Expand All @@ -168,7 +168,7 @@
end

context "Missing Attributes" do
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' }
subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :active_support, encoding: 'utf-8' }

it "doesn't attempt decryption of missing attributes" do
subject.create!(storage: 'blah')
Expand Down
Loading