pgcrypto 0.3.6 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +5 -1
- data/Gemfile +1 -4
- data/README.markdown +126 -33
- data/VERSION +1 -1
- data/lib/active_record/connection_adapters/pgcrypto_adapter.rb +4 -0
- data/lib/active_record/connection_adapters/pgcrypto_adapter/rails_3.rb +20 -0
- data/lib/active_record/connection_adapters/pgcrypto_adapter/rails_4.rb +22 -0
- data/lib/pgcrypto.rb +15 -105
- data/lib/pgcrypto/adapter.rb +162 -0
- data/lib/pgcrypto/column.rb +9 -8
- data/lib/pgcrypto/column_converter.rb +26 -0
- data/lib/pgcrypto/generators/base_generator.rb +12 -0
- data/lib/{generators/pgcrypto → pgcrypto/generators}/install/USAGE +0 -0
- data/lib/{generators/pgcrypto → pgcrypto/generators}/install/install_generator.rb +2 -5
- data/lib/{generators/pgcrypto → pgcrypto/generators}/install/templates/initializer.rb +0 -0
- data/lib/pgcrypto/generators/install/templates/migration.rb +5 -0
- data/lib/pgcrypto/generators/upgrade/USAGE +8 -0
- data/lib/pgcrypto/generators/upgrade/templates/migration.rb +22 -0
- data/lib/pgcrypto/generators/upgrade/upgrade_generator.rb +15 -0
- data/lib/pgcrypto/has_encrypted_column.rb +25 -0
- data/lib/pgcrypto/key.rb +0 -10
- data/lib/pgcrypto/key_manager.rb +11 -0
- data/lib/pgcrypto/railtie.rb +15 -0
- data/lib/pgcrypto/table.rb +11 -0
- data/lib/pgcrypto/table_manager.rb +2 -10
- data/lib/tasks/pgcrypto.rake +7 -0
- data/pgcrypto.gemspec +24 -17
- data/spec/lib/pgcrypto_spec.rb +101 -100
- data/spec/spec_helper.rb +27 -28
- metadata +20 -36
- data/lib/generators/pgcrypto/install/templates/migration.rb +0 -17
- data/lib/pgcrypto/active_record.rb +0 -88
- data/lib/pgcrypto/arel.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b99c62fc6bec129aea118f72d80707481a5432af
|
4
|
+
data.tar.gz: c2a12881ae7160f7d1660470a31fc3ae508637e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a600e76151008c35358173898eb71a0f1d840d65d22d98048ff8039222f8452fd9c31706db4b1df6798f8c80a86c7bc2bf36a4963163324ecd5e61dd5707e3a4
|
7
|
+
data.tar.gz: a2a6a9f4f63ccd43e46c6956f25f090cd694c22cb9945e2f428cae39ed918ede71cfa684f71b28feb9dd993af4ad0b5a757490936c52a7832cc8f04d413c9519
|
data/CHANGES.md
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
# CHANGELOG
|
2
|
+
## 0.4.0
|
3
|
+
- Refactored EVERYTHING to support encryption directly
|
4
|
+
on columns. See README for upgrade instructions.
|
5
|
+
|
2
6
|
## 0.3.5
|
3
7
|
- Fixed ActiveRecord dependency issue (now handles any
|
4
8
|
version of ActiveRecord from 3.2 to current)
|
@@ -52,7 +56,7 @@
|
|
52
56
|
|
53
57
|
## 0.1.2
|
54
58
|
- Added automatic installation of the pgcrypto extension if'n it
|
55
|
-
doesn't already exist. Helpful, but doesn't fully make the
|
59
|
+
doesn't already exist. Helpful, but doesn't fully make the
|
56
60
|
`rake db:test:prepare` cut yet. Still working on that bit...
|
57
61
|
|
58
62
|
## 0.1.1
|
data/Gemfile
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
source 'http://rubygems.org'
|
2
2
|
|
3
|
-
gem 'activerecord', '>= 3.2', :require => 'active_record'
|
4
|
-
gem 'big_spoon', '>= 0.2.1'
|
5
|
-
|
6
3
|
group :development do
|
7
4
|
gem 'jeweler'
|
8
5
|
end
|
@@ -12,7 +9,7 @@ group :test do
|
|
12
9
|
gem 'fuubar'
|
13
10
|
gem 'guard-rspec'
|
14
11
|
gem 'pg', '>= 0.11'
|
15
|
-
gem 'rspec',
|
12
|
+
gem 'rspec', '>= 2.6'
|
16
13
|
gem 'simplecov', :require => false
|
17
14
|
gem 'terminal-notifier'
|
18
15
|
end
|
data/README.markdown
CHANGED
@@ -1,54 +1,139 @@
|
|
1
|
-
PGCrypto for ActiveRecord::Base
|
2
|
-
===
|
1
|
+
# PGCrypto for ActiveRecord::Base
|
3
2
|
|
4
|
-
**PGCrypto** adds seamless column-level encryption to your ActiveRecord::Base subclasses.
|
5
|
-
so I make no promises as to its efficacy in the real world beyond my tiny, Rails-3.2-based utopia.
|
3
|
+
**PGCrypto** adds seamless column-level encryption to your ActiveRecord::Base subclasses.
|
6
4
|
|
7
|
-
|
8
|
-
-
|
5
|
+
#### **WARNING TO 0.3.x USERS**:
|
9
6
|
|
10
|
-
PGCrypto
|
11
|
-
|
12
|
-
|
7
|
+
PGCrypto's architecture has changed significantly as of 0.4.0. **PLEASE** read both the installation and upgrading sections below before you upgrade.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Installing PGCrypto is pretty simple, but I'm going to give you the TL;DR first because the instructions can look more daunting than they are.
|
12
|
+
|
13
|
+
### TL;DR Install
|
14
|
+
|
15
|
+
1. Add it to your Gemfile and bundle: `gem "pgcrypto"`
|
16
|
+
2. Change `adapter: postgresql` to `adapter: pgcrypto` in `config/database.yml`.
|
17
|
+
3. Generate some files using `rails generate pgcrypto:install`.
|
18
|
+
4. Add encryptable columns using `add_column :users, :social_security_number, :binary`
|
19
|
+
5. Run pending migrations: `rake db:migrate`
|
20
|
+
6. Tell a model that it has an encrypted column using `has_encrypted_column :social_security_number`
|
21
|
+
7. Profit.
|
22
|
+
|
23
|
+
### Full Install Instructions
|
24
|
+
|
25
|
+
1. Add pgcrypto to your Gemfile and run `bundle install`.
|
13
26
|
|
14
|
-
1. Add pgcrypto to your Gemfile:
|
15
|
-
|
16
27
|
gem "pgcrypto"
|
17
28
|
|
18
|
-
2.
|
19
|
-
|
20
|
-
|
29
|
+
2. Update your database adapter. If you were previously using the `postgresl` adapter, you should now be using a `pgcrypto` adapter, like so:
|
30
|
+
|
31
|
+
#### `config/database.yml`:
|
32
|
+
|
33
|
+
common: &common
|
34
|
+
adapter: pgcrypto
|
35
|
+
min_messages: warning
|
36
|
+
|
37
|
+
development:
|
38
|
+
<<: *common
|
39
|
+
database: my_app_development
|
40
|
+
host: localhost
|
41
|
+
|
42
|
+
test: &test
|
43
|
+
<<: *common
|
44
|
+
database: my_app_test
|
45
|
+
host: localhost
|
21
46
|
|
22
|
-
|
47
|
+
production:
|
48
|
+
<<: *common
|
49
|
+
host: whatevs
|
50
|
+
database: my_app_production
|
51
|
+
username: whatevs
|
52
|
+
password: totally_not_my_app
|
23
53
|
|
24
|
-
|
25
|
-
rake db:migrate
|
54
|
+
**NOTE:** if you are already using a PostgreSQL-descendant as an adapter (for example, the awesome [PostGIS adapter](https://github.com/rgeo/activerecord-postgis-adapter)), you'll need to read through the "Alternate Adapters" section below. But **don't panic**, it's 100% supported by PGCrypto.
|
26
55
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
56
|
+
3. Generate the required files using the included generator.
|
57
|
+
|
58
|
+
rails generate pgcrypto:install
|
59
|
+
|
60
|
+
|
61
|
+
4. Edit the new initializer to point to your public and private GPG keys:
|
62
|
+
|
63
|
+
#### `config/initializers/pgcrypto.rb`:
|
64
|
+
|
65
|
+
PGCrypto.keys[:private] = {path: "~/.keys/private.key"}
|
66
|
+
PGCrypto.keys[:public] = {path: "~/.keys/public.key"}
|
67
|
+
|
68
|
+
5. Add PGCrypto columns to your models in a migration. Something like the following:
|
69
|
+
|
70
|
+
rails generate migration add_social_security_number_to_users
|
71
|
+
|
72
|
+
And in the migration:
|
73
|
+
|
74
|
+
class AddSocialSecurityNumberToUsers < ActiveRecord::Migration
|
75
|
+
def change
|
76
|
+
add_column :users, :social_security_number, :binary
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
6. Tell the User class to encrypt and decrypt the `social_security_number` attribute on the fly:
|
31
81
|
|
32
|
-
5. Tell the User class to encrypt and decrypt the `social_security_number` attribute on the fly:
|
33
|
-
|
34
82
|
class User < ActiveRecord::Base
|
35
83
|
# ... all kinds of neat stuff ...
|
36
84
|
|
37
|
-
|
85
|
+
has_encrypted_column :social_security_number
|
38
86
|
|
39
87
|
# ... some other fun stuff
|
40
88
|
end
|
41
89
|
|
42
|
-
|
43
|
-
|
44
|
-
User.create!(:
|
90
|
+
7. Profit
|
91
|
+
|
92
|
+
User.create!(social_security_number: "466-99-1234") #=> #<User with stuff>
|
45
93
|
User.last.social_security_number #=> "466-99-1234"
|
46
94
|
|
47
95
|
BAM. It looks innocuous on your end, but on the back end that beast is storing the social security number in
|
48
96
|
a GPG-encrypted column that can only be decrypted with your secure key.
|
49
97
|
|
50
|
-
|
51
|
-
|
98
|
+
### Rails 3.x and PostgreSQL extensions
|
99
|
+
|
100
|
+
PGCrypto will load the `pgcrypto` extension into your database if you haven't already, but this change will NOT get propagated
|
101
|
+
to your schema.rb file, so... go figure. You'll have to `CREATE EXTENSION IF NOT EXISTS pgcrypto` any database built from the
|
102
|
+
schema file (**HINT** that means your test databases).
|
103
|
+
|
104
|
+
|
105
|
+
## Upgrading from 0.3.x
|
106
|
+
|
107
|
+
If you've been on 0.3.x branch, the most important change is that **PGCrypto now uses database columns on models directly**. This means you don't need the `pgcrypto_columns` table anymore. Follow these steps to migrate your new app over!
|
108
|
+
|
109
|
+
1. BACK UP YOUR PRODUCTION DATABASE.
|
110
|
+
|
111
|
+
2. In `config/database.yml`, change `adapter: postgresql` to `adapter: pgcrypto`
|
112
|
+
|
113
|
+
2. Generate the upgrade files:
|
114
|
+
|
115
|
+
`rails generate pgcrypto:upgrade`
|
116
|
+
|
117
|
+
3. Run the migration that gets generated. It will do three things:
|
118
|
+
1. It will add encrypted columns directly to tables whose records have corresponding columns in the `pgcrypto_columns` table.
|
119
|
+
2. It will move values from `pgcrypto_columns` into the appropriate columns on the parent models.
|
120
|
+
3. It will drop the `pgcrypto_columns` table.
|
121
|
+
|
122
|
+
4. The `pgcrypto` method is being deprecated in favor of the more declarative `has_encrypted_column`. Any model that calls `pgcrypto` will start generating deprecation warnings. So g'head and update your models.
|
123
|
+
|
124
|
+
### Manual upgrade
|
125
|
+
|
126
|
+
If you don't trust my auto-generated migration, follow these steps:
|
127
|
+
|
128
|
+
1. Add columns directly to models' tables:
|
129
|
+
|
130
|
+
add_column :users, :social_security_number, :pgcrypto
|
131
|
+
|
132
|
+
2. Run `rake pgcrypto:upgrade_columns` to copy `PGCrypto::Column` values directly onto your tables' new columns.
|
133
|
+
|
134
|
+
3. Generate a migration to drop the `pgcrypto_columns` table.
|
135
|
+
|
136
|
+
## Keys
|
52
137
|
|
53
138
|
If you want to bundle your public key with your application, PGCrypto will automatically load `RAILS_ROOT/.pgcrypto`,
|
54
139
|
so feel free to put your public key in there. You can also tell PGCrypto about your keys in a number of fun ways.
|
@@ -67,8 +152,17 @@ storage on your server, since if you're using this library you presumably care a
|
|
67
152
|
|
68
153
|
PGCrypto.keys[:private] = {:value => ENV['PRIVATE_KEY'], :password => ENV['PRIVATE_KEY_PASSWORD']}
|
69
154
|
|
70
|
-
|
71
|
-
|
155
|
+
## Alternate Adapters
|
156
|
+
|
157
|
+
If you're already using an adapter that isn't the PostgreSQL adapter, you'll want to tell PGCrypto so it can make sure it supports your extra stuff. The easiest way to do this is to tell it which adapter it should inherit from.
|
158
|
+
|
159
|
+
In `config/initializers/pgcrypto.rb`, add:
|
160
|
+
|
161
|
+
PGCrypto.base_adapter = ActiveRecord::ConnectionAdapters:PostGISAdapter
|
162
|
+
|
163
|
+
...or whatever your adapter is. Then make sure you're telling `config/database.yml` to use `adapter: pgcrypto`.
|
164
|
+
|
165
|
+
## Warranty (or lack thereof)
|
72
166
|
|
73
167
|
As I mentioned before, this library is one HUGE hack. This is just scratching the surface of keeping your data secure.
|
74
168
|
For example, if you don't protect your log files, anyone who can read them can get your private and public keys and
|
@@ -78,7 +172,6 @@ alongside those private and public keys.
|
|
78
172
|
Basically, this will make it easy to start with asymmetric, GPG-based, column-level encryption in PostgreSQL. But that's about
|
79
173
|
it; the rest is up to you.
|
80
174
|
|
81
|
-
**As such,** the author and Delightful Widgets Inc. offer ***ABSOLUTELY NO GODDAMN WARRANTY***.
|
82
|
-
Rails 3.2 world, but YMMV if your version of Arel or ActiveRecord are ahead or behind ours. Sorry, folks.
|
175
|
+
**As such,** the author and Delightful Widgets Inc. offer ***ABSOLUTELY NO GODDAMN WARRANTY***. Sorry, folks.
|
83
176
|
|
84
177
|
Copyright (C) 2012 Delightful Widgets, Inc. Built by Flip Sasser, Monkeypatcher Extraordinaire!
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.1
|
@@ -0,0 +1,20 @@
|
|
1
|
+
ActiveRecord::Base.class_eval do
|
2
|
+
def self.pgcrypto_connection(config) # :nodoc:
|
3
|
+
config = config.symbolize_keys
|
4
|
+
host = config[:host]
|
5
|
+
port = config[:port] || 5432
|
6
|
+
username = config[:username].to_s if config[:username]
|
7
|
+
password = config[:password].to_s if config[:password]
|
8
|
+
|
9
|
+
if config.key?(:database)
|
10
|
+
database = config[:database]
|
11
|
+
else
|
12
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
13
|
+
end
|
14
|
+
|
15
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
16
|
+
# so just pass a nil connection object for the time being.
|
17
|
+
PGCrypto::Adapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionHandling
|
3
|
+
|
4
|
+
def pgcrypto_connection(config, *args, &block)
|
5
|
+
conn_params = config.symbolize_keys
|
6
|
+
|
7
|
+
conn_params.delete_if { |_, v| v.nil? }
|
8
|
+
|
9
|
+
# Map ActiveRecords param names to PGs.
|
10
|
+
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
|
11
|
+
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
|
12
|
+
|
13
|
+
# Forward only valid config params to PGconn.connect.
|
14
|
+
conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
|
15
|
+
|
16
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
17
|
+
# so just pass a nil connection object for the time being.
|
18
|
+
PGCrypto::Adapter.new(nil, logger, conn_params, config)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/pgcrypto.rb
CHANGED
@@ -1,118 +1,28 @@
|
|
1
|
-
require '
|
2
|
-
require 'pgcrypto/
|
3
|
-
require 'pgcrypto/arel'
|
4
|
-
require 'pgcrypto/column'
|
1
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
2
|
+
require 'pgcrypto/has_encrypted_column'
|
5
3
|
require 'pgcrypto/key'
|
4
|
+
require 'pgcrypto/key_manager'
|
6
5
|
require 'pgcrypto/table_manager'
|
7
6
|
|
8
7
|
module PGCrypto
|
9
|
-
|
10
|
-
|
11
|
-
(@table_manager ||= TableManager.new)[key]
|
12
|
-
end
|
13
|
-
|
14
|
-
def keys
|
15
|
-
@keys ||= KeyManager.new
|
16
|
-
end
|
8
|
+
def self.[](key)
|
9
|
+
(@table_manager ||= TableManager.new)[key]
|
17
10
|
end
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def pgcrypto(*pgcrypto_column_names)
|
23
|
-
options = pgcrypto_column_names.last.is_a?(Hash) ? pgcrypto_column_names.pop : {}
|
24
|
-
options = {:include => false, :type => :pgp}.merge(options)
|
25
|
-
|
26
|
-
has_many :pgcrypto_columns, :as => :owner, :autosave => true, :class_name => 'PGCrypto::Column', :dependent => :delete_all
|
27
|
-
|
28
|
-
hooks do
|
29
|
-
before(:reload) do
|
30
|
-
self.class.pgcrpyto_columns.each do |column_name, options|
|
31
|
-
reset_attribute! column_name
|
32
|
-
changed_attributes.delete(column_name)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
pgcrypto_column_names.map(&:to_s).each do |column_name|
|
38
|
-
# Stash the encryption type in our module so various monkeypatches can access it later!
|
39
|
-
PGCrypto[table_name][column_name] = options.symbolize_keys
|
40
|
-
|
41
|
-
# Add dynamic attribute readers/writers for ActiveModel APIs
|
42
|
-
# define_attribute_method column_name
|
43
|
-
|
44
|
-
# Add attribute readers/writers to keep this baby as fluid and clean as possible.
|
45
|
-
start_line = __LINE__; pgcrypto_methods = <<-PGCRYPTO_METHODS
|
46
|
-
def #{column_name}
|
47
|
-
return @_pgcrypto_#{column_name}.try(:value) if defined?(@_pgcrypto_#{column_name})
|
48
|
-
@_pgcrypto_#{column_name} ||= select_pgcrypto_column(:#{column_name})
|
49
|
-
@_pgcrypto_#{column_name}.try(:value)
|
50
|
-
end
|
51
|
-
|
52
|
-
# We write the attribute directly to its child value. Neato!
|
53
|
-
def #{column_name}=(value)
|
54
|
-
attribute_will_change!(:#{column_name}) if value != @_pgcrypto_#{column_name}.try(:value)
|
55
|
-
if value.nil?
|
56
|
-
pgcrypto_columns.select{|column| column.name == "#{column_name}"}.each(&:mark_for_destruction)
|
57
|
-
remove_instance_variable("@_pgcrypto_#{column_name}") if defined?(@_pgcrypto_#{column_name})
|
58
|
-
else
|
59
|
-
@_pgcrypto_#{column_name} ||= pgcrypto_columns.select{|column| column.name == "#{column_name}"}.first || pgcrypto_columns.new(:name => "#{column_name}")
|
60
|
-
pgcrypto_columns.push(@_pgcrypto_#{column_name})
|
61
|
-
@_pgcrypto_#{column_name}.value = value
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def #{column_name}_changed?
|
66
|
-
changed.include?(:#{column_name})
|
67
|
-
end
|
68
|
-
PGCRYPTO_METHODS
|
69
|
-
|
70
|
-
class_eval pgcrypto_methods, __FILE__, start_line
|
71
|
-
end
|
72
|
-
|
73
|
-
# If any columns are set to be included in the parent record's finder,
|
74
|
-
# we'll go ahead and add 'em!
|
75
|
-
if PGCrypto[table_name].any?{|column, options| options[:include] }
|
76
|
-
default_scope includes(:pgcrypto_columns)
|
77
|
-
end
|
78
|
-
end
|
12
|
+
def self.base_adapter
|
13
|
+
@base_adapter ||= ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
14
|
+
end
|
79
15
|
|
80
|
-
|
81
|
-
|
82
|
-
|
16
|
+
def self.base_adapter=(base_adapter)
|
17
|
+
@base_adapter = base_adapter
|
18
|
+
rebuild_adapter! if respond_to?(:rebuild_adapter!)
|
83
19
|
end
|
84
20
|
|
85
|
-
|
86
|
-
|
87
|
-
return nil if new_record?
|
88
|
-
# Now here's the fun part. We want the selector on PGCrypto columns to do the decryption
|
89
|
-
# for us, so we have override the SELECT and add a JOIN to build out the decrypted value
|
90
|
-
# whenever it's requested.
|
91
|
-
options = PGCrypto[self.class.table_name][column_name]
|
92
|
-
pgcrypto_column_finder = pgcrypto_columns
|
93
|
-
if key = PGCrypto.keys[:private]
|
94
|
-
pgcrypto_column_finder = pgcrypto_column_finder.select([
|
95
|
-
%w(id owner_id owner_type owner_table).map {|column| %("#{PGCrypto::Column.table_name}"."#{column}")},
|
96
|
-
%[pgp_pub_decrypt("#{PGCrypto::Column.table_name}"."value", pgcrypto_keys.#{key.name}#{key.password?}) AS "value"]
|
97
|
-
].flatten).joins(%[CROSS JOIN (SELECT #{key.dearmored} AS "#{key.name}") AS pgcrypto_keys])
|
98
|
-
end
|
99
|
-
pgcrypto_column_finder.where(:name => column_name).first
|
100
|
-
rescue ActiveRecord::StatementInvalid => e
|
101
|
-
case e.message
|
102
|
-
when /^PGError: ERROR: Wrong key or corrupt data/
|
103
|
-
# If a column has been corrupted, we'll return nil and let the DBA
|
104
|
-
# figure out WTF the is going on
|
105
|
-
logger.error(e.message.split("\n").first)
|
106
|
-
nil
|
107
|
-
else
|
108
|
-
raise e
|
109
|
-
end
|
110
|
-
end
|
21
|
+
def self.keys
|
22
|
+
@keys ||= KeyManager.new
|
111
23
|
end
|
112
24
|
end
|
113
25
|
|
114
26
|
PGCrypto.keys[:public] = {:path => '.pgcrypto'} if File.file?('.pgcrypto')
|
115
|
-
|
116
|
-
|
117
|
-
ActiveRecord::Base.send :include, PGCrypto::InstanceMethods
|
118
|
-
end
|
27
|
+
|
28
|
+
require 'pgcrypto/railtie' if defined? Rails::Railtie
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'pgcrypto'
|
2
|
+
|
3
|
+
module PGCrypto
|
4
|
+
def self.build_adapter!
|
5
|
+
Class.new(PGCrypto.base_adapter) do
|
6
|
+
include PGCrypto::AdapterMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.rebuild_adapter!
|
11
|
+
remove_const(:Adapter) if const_defined? :Adapter
|
12
|
+
const_set(:Adapter, build_adapter!)
|
13
|
+
end
|
14
|
+
|
15
|
+
module AdapterMethods
|
16
|
+
ADAPTER_NAME = 'PGCrypto'
|
17
|
+
|
18
|
+
def quote(*args, &block)
|
19
|
+
if args.first.is_a?(Arel::Nodes::SqlLiteral)
|
20
|
+
args.first
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_sql(arel, *args)
|
27
|
+
case arel
|
28
|
+
when Arel::InsertManager
|
29
|
+
pgcrypto_insert(arel)
|
30
|
+
when Arel::SelectManager
|
31
|
+
pgcrypto_select(arel)
|
32
|
+
when Arel::UpdateManager
|
33
|
+
pgcrypto_update(arel)
|
34
|
+
end
|
35
|
+
super(arel, *args)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def pgcrypto_decrypt_column(table_name, column_name, key)
|
41
|
+
table = Arel::Table.new(table_name)
|
42
|
+
column = Arel::Attribute.new(table, column_name)
|
43
|
+
key_dearmored = Arel::Nodes::SqlLiteral.new("#{key.dearmored}#{key.password?}")
|
44
|
+
Arel::Nodes::NamedFunction.new('pgp_pub_decrypt', [column, key_dearmored])
|
45
|
+
end
|
46
|
+
|
47
|
+
def pgcrypto_encrypt_string(string, key)
|
48
|
+
if string.is_a?(String)
|
49
|
+
string = quote(string)
|
50
|
+
else
|
51
|
+
string = quote_string(string)
|
52
|
+
end
|
53
|
+
encryption_instruction = %[pgp_pub_encrypt(#{string}, #{key.dearmored})]
|
54
|
+
Arel::Nodes::SqlLiteral.new(encryption_instruction)
|
55
|
+
end
|
56
|
+
|
57
|
+
def pgcrypto_insert(arel)
|
58
|
+
if table = PGCrypto[arel.ast.relation.name.to_s]
|
59
|
+
arel.ast.columns.each_with_index do |column, i|
|
60
|
+
if options = table[column.name.to_sym]
|
61
|
+
key = options[:key] || PGCrypto.keys[:public]
|
62
|
+
next unless key
|
63
|
+
# Encrypt encryptable columns
|
64
|
+
value = arel.ast.values.expressions[i]
|
65
|
+
arel.ast.values.expressions[i] = pgcrypto_encrypt_string(value, key)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def pgcrypto_select(arel)
|
72
|
+
# We start by looping through each "core," which is just a
|
73
|
+
# SelectStatement and correcting plain-text queries against an encrypted
|
74
|
+
# column...
|
75
|
+
arel.ast.cores.each do |core|
|
76
|
+
next unless core.is_a?(Arel::Nodes::SelectCore)
|
77
|
+
|
78
|
+
pgcrypto_update_selects(core, core.projections) if core.projections
|
79
|
+
pgcrypto_update_selects(core, core.having) if core.having
|
80
|
+
|
81
|
+
# Loop through each WHERE to determine whether or not we need to refer
|
82
|
+
# to its decrypted counterpart
|
83
|
+
pgcrypto_update_wheres(core)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def pgcrypto_update(arel)
|
88
|
+
if table = PGCrypto[arel.ast.relation.name.to_s]
|
89
|
+
# Find all columns with encryption instructions and encrypt them
|
90
|
+
arel.ast.values.each do |value|
|
91
|
+
if value.respond_to?(:left) && options = table[value.left.name]
|
92
|
+
key = options[:key] || PGCrypto.keys[:public]
|
93
|
+
next unless key
|
94
|
+
|
95
|
+
if value.right.nil?
|
96
|
+
value.right = Arel::Nodes::SqlLiteral.new('NULL')
|
97
|
+
else
|
98
|
+
value.right = pgcrypto_encrypt_string(value.right, key)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def pgcrypto_update_selects(core, selects)
|
106
|
+
table_name = core.source.left.name
|
107
|
+
columns = PGCrypto[table_name]
|
108
|
+
return if columns.empty?
|
109
|
+
|
110
|
+
untouched_columns = columns.keys.map(&:to_s)
|
111
|
+
|
112
|
+
selects.each_with_index do |select, i|
|
113
|
+
next unless select.respond_to?(:name)
|
114
|
+
|
115
|
+
select_name = select.name.to_s
|
116
|
+
if untouched_columns.include?(select_name)
|
117
|
+
key = columns[select_name.to_sym][:private] || PGCrypto.keys[:private]
|
118
|
+
next unless key
|
119
|
+
decrypt = pgcrypto_decrypt_column(table_name, select_name, key)
|
120
|
+
selects[i] = decrypt.as(select_name)
|
121
|
+
untouched_columns.delete(select_name)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
splat_projection = selects.find { |select| select.respond_to?(:name) && select.name == '*' }
|
126
|
+
if untouched_columns.any? && splat_projection
|
127
|
+
untouched_columns.each do |column|
|
128
|
+
key = columns[column.to_sym][:private] || PGCrypto.keys[:private]
|
129
|
+
next unless key
|
130
|
+
decrypt = pgcrypto_decrypt_column(table_name, column, key)
|
131
|
+
core.projections.push(decrypt.as(column))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def pgcrypto_update_wheres(core)
|
137
|
+
table_name = core.source.left.name
|
138
|
+
columns = PGCrypto[table_name]
|
139
|
+
return if columns.empty?
|
140
|
+
|
141
|
+
core.wheres.each do |where|
|
142
|
+
if where.respond_to?(:children)
|
143
|
+
# Loop through the children to replace them with a decrypted
|
144
|
+
# counterpart
|
145
|
+
where.children.each do |child|
|
146
|
+
next unless child.respond_to?(:left) && options = columns[child.left.name.to_s]
|
147
|
+
key = options[:private] || PGCrypto.keys[:private]
|
148
|
+
child.left = pgcrypto_decrypt_column(table_name, child.left.name, key)
|
149
|
+
if child.right.is_a?(String)
|
150
|
+
# Prevent ActiveRecord from re-casting this as binary text
|
151
|
+
child.right = Arel::Nodes::SqlLiteral.new("'#{quote_string(child.right)}'")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
Adapter = build_adapter!
|
161
|
+
|
162
|
+
end
|