Skip to content

Commit

Permalink
Replace AesNew provider with ActiveSupport::MessageEncryptor
Browse files Browse the repository at this point in the history
  • Loading branch information
itspriddle committed Jun 19, 2017
1 parent ea5ce8b commit 0b60ab2
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 115 deletions.
20 changes: 10 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,7 +157,7 @@ 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
```

Expand Down
1 change: 0 additions & 1 deletion crypt_keeper.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Gem::Specification.new do |gem|

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
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_pgp'
require 'crypt_keeper/provider/postgres_pgp_public_key'
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 @@ -12,6 +12,7 @@ def escape_and_execute_sql(query)

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 @@ -13,7 +13,7 @@ class PostgresPgp < Base
#
# 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 @@ -8,7 +8,7 @@ class PostgresPgpPublicKey < Base
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
37 changes: 37 additions & 0 deletions spec/crypt_keeper/provider/active_support_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'spec_helper'

describe CryptKeeper::Provider::ActiveSupport do
subject { described_class.new(key: 'cake', salt: 'salt') }

let :plaintext do
"string"
end

let :encrypted do
subject.encrypt plaintext
end

let :decrypted do
subject.decrypt encrypted
end

describe "#encrypt" do
specify { expect(encrypted).to_not eq(plaintext) }
specify { expect(encrypted).to_not be_blank }
end

describe "#decrypt" do
specify { expect(decrypted).to eq(plaintext) }
specify { expect(decrypted).to_not be_blank }
end

describe "#search" do
let :records do
[{ name: 'Bob' }, { name: 'Tim' }]
end

it "finds the matching record" do
expect(subject.search(records, :name, 'Bob')).to eql([records.first])
end
end
end
41 changes: 0 additions & 41 deletions spec/crypt_keeper/provider/aes_new_spec.rb

This file was deleted.

0 comments on commit 0b60ab2

Please sign in to comment.