Skip to content

Commit

Permalink
main implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gabetax committed Jan 17, 2013
1 parent d75795d commit 50eec7f
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -1,3 +1,4 @@
.DS_Store
*.gem
*.rbc
.bundle
Expand All @@ -16,3 +17,6 @@ tmp
.yardoc
_yardoc
doc/

spec/database.yml
spec/debug.log
30 changes: 30 additions & 0 deletions Gemfile.lock
Expand Up @@ -2,12 +2,39 @@ PATH
remote: .
specs:
attr_encrypted_pgcrypto (0.0.1)
activerecord (>= 3.0)
activesupport (>= 3.0)
attr_encrypted (~> 1.2.0)

GEM
remote: https://rubygems.org/
specs:
activemodel (3.2.11)
activesupport (= 3.2.11)
builder (~> 3.0.0)
activerecord (3.2.11)
activemodel (= 3.2.11)
activesupport (= 3.2.11)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activesupport (3.2.11)
i18n (~> 0.6)
multi_json (~> 1.0)
arel (3.0.2)
attr_encrypted (1.2.1)
encryptor (>= 1.1.1)
builder (3.0.4)
coderay (1.0.8)
diff-lcs (1.1.3)
encryptor (1.1.3)
i18n (0.6.1)
method_source (0.8.1)
multi_json (1.5.0)
pg (0.14.1)
pry (0.9.10)
coderay (~> 1.0.5)
method_source (~> 0.8)
slop (~> 3.3.1)
rspec (2.12.0)
rspec-core (~> 2.12.0)
rspec-expectations (~> 2.12.0)
Expand All @@ -16,11 +43,14 @@ GEM
rspec-expectations (2.12.1)
diff-lcs (~> 1.1.3)
rspec-mocks (2.12.1)
slop (3.3.3)
tzinfo (0.3.35)

PLATFORMS
ruby

DEPENDENCIES
attr_encrypted_pgcrypto!
pg (~> 0.14.0)
pry
rspec (~> 2.12.0)
59 changes: 55 additions & 4 deletions README.md
@@ -1,6 +1,11 @@
# attr_encrypted_pgcrypto

A pgcrypto based Encryptor implementation for attr_encrypted
A [pgcrypto](http://www.postgresql.org/docs/9.1/static/pgcrypto.html)-based [Encryptor](https://github.com/shuber/encryptor) implementation for [attr_encrypted](https://github.com/shuber/attr_encrypted). It delegates to `pgp_sym_encrypt()` and `pgp_sym_decrypt()` to provide symmetric-key encryption. It's useful if you need to:

- Access the plain text values directly from SQL without bringing the data into Ruby
- Integrate databases managed by other applications

Is this library a bad idea? _Potentially!_ Please open an issue to discuss and help document any caveats.

## Installation

Expand All @@ -12,13 +17,59 @@ And then execute:

$ bundle

Or install it yourself as:
Your platform may not ship with the pgcrypto extensions by default. On Ubuntu, run:

`apt-get install postgresql-contrib-9.1`

Generate a migration to load the pgcrypto extension into your database. Your user will need [superuser privileges](http://www.postgresql.org/docs/9.1/static/sql-createextension.html) to run this query, so you may need to manually run this via `psql` as the `postgres` user if your Rails database user does not have access.

```ruby
execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
```

$ gem install attr_encrypted_pgcrypto
Extensions are database specific. To ensure that the extension is also enabled for your test database, rails needs to use the [sql schema format](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-schema_format). Edit `config/application.rb` to set:

```ruby
config.active_record.schema_format = :sql
```

## Usage

TODO: Write usage instructions here
See [attr_encrypted's Custom encryptor documentation](https://github.com/shuber/attr_encrypted#custom-encryptor).

```ruby
class User
attr_encrypted :ssn, :key => 'a secret key', :encryptor => AttrEncryptedPgcrypto::Encryptor, :encode => false
end
```

If you do not disable `:encode`, attr_encrypted will base64 encode the output, defeating the purpose of being able to query the data directly from SQL.

This is an example - please don't actually embed your keys directly in your model as literal strings, or even commit them in your repository. I recommend storing your key in a .gitignored config/pgcrypto_key.txt file, having capistrano (or your preferred deployment utility) copy this from a local 'shared/' folder, and reading the value into `Rails.application.config.pgcrypto` via an initializer.

## Caveats

- Your key is embedded into any SQL queries. The key itself will be automatically filtered from your Rails logs. However, make sure you are using a secured or private connection between your Rails server and your database.
- Unlike the OpenSSL algorithms used in the default Encryptor, `pgp_sym_encrypt()` uses an IV and will generate different cipher text every call. While this is more secure, you will not be able to use attr_encrypted's [find_by_ methods](https://github.com/shuber/attr_encrypted#dynamic-find_by_-and-scoped_by_-methods).

## Compatability

Tested against:

- MRI Ruby 1.9.3
- Rails 3.2.11
- attr_encrypted 1.2.1
- PostgreSQL 9.1

## Credits

The bulk of this code is a humble verbatim copy and paste job from [jmazzi's crypt_keeper gem](https://github.com/jmazzi/crypt_keeper). Thanks, Justin!

Why not just use crypt_keeper? crypt_keeper uses ActiveRecord callbacks to encrypt and decrypt, while attr\_encrypted uses accessor methods. This means:

- Your model is always dirty after a fetch
- Data is eagerly encrypted and decrypted, causing unnecessary extra queries
- If you have other callback based dependencies (e.g. papertrail) they may receive either the encrytped or plaintext version of the columns.

## Contributing

Expand Down
7 changes: 6 additions & 1 deletion Rakefile
@@ -1 +1,6 @@
require "bundler/gem_tasks"
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new('spec')

# If you want to make this the default task
task default: :spec
5 changes: 5 additions & 0 deletions attr_encrypted_pgcrypto.gemspec
Expand Up @@ -17,6 +17,11 @@ Gem::Specification.new do |gem|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ["lib"]

gem.add_runtime_dependency 'attr_encrypted', '~> 1.2.0'
gem.add_runtime_dependency 'activerecord', '>= 3.0'
gem.add_runtime_dependency 'activesupport', '>= 3.0'

gem.add_development_dependency 'pry'
gem.add_development_dependency 'rspec', '~> 2.12.0'
gem.add_development_dependency 'pg', '~> 0.14.0'
end
9 changes: 4 additions & 5 deletions lib/attr_encrypted_pgcrypto.rb
@@ -1,5 +1,4 @@
require "attr_encrypted_pgcrypto/version"

module AttrEncryptedPgcrypto
# Your code goes here...
end
require 'attr_encrypted'
require 'attr_encrypted_pgcrypto/version'
require 'attr_encrypted_pgcrypto/log_subscriber/postgres_pgp'
require 'attr_encrypted_pgcrypto/encryptor'
48 changes: 48 additions & 0 deletions lib/attr_encrypted_pgcrypto/encryptor.rb
@@ -0,0 +1,48 @@
module AttrEncryptedPgcrypto
module Encryptor

extend self
ActiveSupport.run_load_hooks(:attr_encrypted_pgcrypto_posgres_pgp_log, self)

# Encrypts a <tt>:value</tt> with a specified <tt>:key</tt>
#
# Example
#
# encrypted_value = AttrEncryptedPgcrypto::Encryptor.encrypt(:value => 'some string to encrypt', :key => 'some secret key')
# # or
# encrypted_value = AttrEncryptedPgcrypto::Encryptor.encrypt('some string to encrypt', :key => 'some secret key')
def encrypt(*args, &block)
escape_and_execute_sql(["SELECT pgp_sym_encrypt(?, ?)", value(args), key(args)])['pgp_sym_encrypt']
end

# Decrypts a <tt>:value</tt> with a specified <tt>:key</tt>
#
# Example
#
# decrypted_value = AttrEncryptedPgcrypto::Encryptor.decrypt(:value => 'some encrypted string', :key => 'some secret key')
# # or
# decrypted_value = AttrEncryptedPgcrypto::Encryptor.decrypt('some encrypted string', :key => 'some secret key')
def decrypt(*args, &block)
escape_and_execute_sql(["SELECT pgp_sym_decrypt(?, ?)", value(args), key(args)])['pgp_sym_decrypt']
end

protected

def value(args)
if args.first.is_a?(String)
args.first
else
args.last[:value]
end
end

def key(args)
args.last.is_a?(Hash) && args.last[:key] || (raise ArgumentError.new('must specify a :key'))
end

def escape_and_execute_sql(query)
query = ::ActiveRecord::Base.send :sanitize_sql_array, query
::ActiveRecord::Base.connection.execute(query).first
end
end
end
31 changes: 31 additions & 0 deletions lib/attr_encrypted_pgcrypto/log_subscriber/postgres_pgp.rb
@@ -0,0 +1,31 @@
require 'active_record'
require 'active_record/log_subscriber'
require 'active_support/concern'
require 'active_support/lazy_load_hooks'

module AttrEncryptedPgcrypto
module LogSubscriber
module PostgresPgp
extend ActiveSupport::Concern

included do
alias_method_chain :sql, :postgres_pgp
end

# Public: Prevents sensitive data from being logged
def sql_with_postgres_pgp(event)
filter = /(pgp_sym_(encrypt|decrypt))\(((.|\n)*?)\)/i

event.payload[:sql] = event.payload[:sql].gsub(filter) do |_|
"#{$1}([FILTERED])"
end

sql_without_postgres_pgp(event)
end
end
end
end

ActiveSupport.on_load :attr_encrypted_pgcrypto_posgres_pgp_log do
ActiveRecord::LogSubscriber.send :include, AttrEncryptedPgcrypto::LogSubscriber::PostgresPgp
end
2 changes: 1 addition & 1 deletion lib/attr_encrypted_pgcrypto/version.rb
@@ -1,3 +1,3 @@
module AttrEncryptedPgcrypto
VERSION = "0.0.1"
VERSION = "1.2.1"
end
8 changes: 8 additions & 0 deletions spec/default.database.yml
@@ -0,0 +1,8 @@
postgres:
adapter: postgresql
encoding: utf8
reconnect: false
database: attr_encrytped_pgcrypto
pool: 5
username: postgres
password:
38 changes: 38 additions & 0 deletions spec/lib/encryptor_spec.rb
@@ -0,0 +1,38 @@
require 'spec_helper'

describe AttrEncryptedPgcrypto::Encryptor do
use_postgres

subject { AttrEncryptedPgcrypto::Encryptor }
let(:plaintext) { "Hello, World!" }
let(:cipher) { "\\xc30d040703027a5b637f1c6654686cd23e01bb477e90e6483b9f270ce2a5a2a1694e1d0df4ebf95aaca80e0825a42c8ec3c70dff19a421f54ae785a2d35b6c48d0f9e5108a34fbf6b681f92f739e0f" }
let(:key) { "What do you want? I'm a test key!" }
describe "#encrypt" do
context "without key" do
it do
expect { subject.encrypt "plaintext" }.to raise_exception(ArgumentError)
end
end

context "valid" do
it "returns cipher text" do
AttrEncryptedPgcrypto::Encryptor.encrypt(plaintext, key: key).should be_a(String)
end
end
end

describe "#decrypt" do
context "valid" do
it "returns plaintext" do
AttrEncryptedPgcrypto::Encryptor.decrypt(cipher, key: key).should == plaintext
end
end

context "invalid" do
let(:key) { "This is not the key you're looking for." }
specify do
expect { AttrEncryptedPgcrypto::Encryptor.decrypt(cipher, key: key) }.to raise_exception(ActiveRecord::StatementInvalid)
end
end
end
end
25 changes: 25 additions & 0 deletions spec/log_subscriber/postgres_pgp_spec.rb
@@ -0,0 +1,25 @@
require 'spec_helper'

module AttrEncryptedPgcrypto::LogSubscriber
describe PostgresPgp do
use_postgres

subject { ::ActiveRecord::LogSubscriber.new }

let(:input_query) do
"SELECT pgp_sym_encrypt('encrypt_value', 'encrypt_key'), pgp_sym_decrypt('decrypt_value', 'decrypt_key') FROM DUAL;"
end

let(:output_query) do
"SELECT pgp_sym_encrypt([FILTERED]), pgp_sym_decrypt([FILTERED]) FROM DUAL;"
end

it "filters pgp functions" do
subject.should_receive(:sql_without_postgres_pgp) do |event|
event.payload[:sql].should == output_query
end

subject.sql(ActiveSupport::Notifications::Event.new(:sql, 1, 1, 1, { sql: output_query }))
end
end
end
6 changes: 6 additions & 0 deletions spec/spec_helper.rb
@@ -1,3 +1,9 @@
require 'pry'
require 'attr_encrypted_pgcrypto'

SPEC_ROOT = Pathname.new File.expand_path File.dirname __FILE__
Dir[SPEC_ROOT.join('support/*.rb')].each{|f| require f }

# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# Require this file using `require "spec_helper"` to ensure that it is only
Expand Down
25 changes: 25 additions & 0 deletions spec/support/active_record.rb
@@ -0,0 +1,25 @@
require 'active_record'
require 'logger'

::ActiveRecord::Base.logger = Logger.new SPEC_ROOT.join('debug.log').to_s
::ActiveRecord::Migration.verbose = false

module AttrEncryptedPgcrypto
class SensitiveData < ActiveRecord::Base; end

module ConnectionHelpers
def use_postgres
before :all do
::ActiveRecord::Base.clear_active_connections!
config = YAML.load_file SPEC_ROOT.join('database.yml')
::ActiveRecord::Base.establish_connection(config['postgres'])
end
end

end
end


RSpec.configure do |config|
config.extend AttrEncryptedPgcrypto::ConnectionHelpers
end

0 comments on commit 50eec7f

Please sign in to comment.