pgcrypto 0.3.6 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +5 -1
  3. data/Gemfile +1 -4
  4. data/README.markdown +126 -33
  5. data/VERSION +1 -1
  6. data/lib/active_record/connection_adapters/pgcrypto_adapter.rb +4 -0
  7. data/lib/active_record/connection_adapters/pgcrypto_adapter/rails_3.rb +20 -0
  8. data/lib/active_record/connection_adapters/pgcrypto_adapter/rails_4.rb +22 -0
  9. data/lib/pgcrypto.rb +15 -105
  10. data/lib/pgcrypto/adapter.rb +162 -0
  11. data/lib/pgcrypto/column.rb +9 -8
  12. data/lib/pgcrypto/column_converter.rb +26 -0
  13. data/lib/pgcrypto/generators/base_generator.rb +12 -0
  14. data/lib/{generators/pgcrypto → pgcrypto/generators}/install/USAGE +0 -0
  15. data/lib/{generators/pgcrypto → pgcrypto/generators}/install/install_generator.rb +2 -5
  16. data/lib/{generators/pgcrypto → pgcrypto/generators}/install/templates/initializer.rb +0 -0
  17. data/lib/pgcrypto/generators/install/templates/migration.rb +5 -0
  18. data/lib/pgcrypto/generators/upgrade/USAGE +8 -0
  19. data/lib/pgcrypto/generators/upgrade/templates/migration.rb +22 -0
  20. data/lib/pgcrypto/generators/upgrade/upgrade_generator.rb +15 -0
  21. data/lib/pgcrypto/has_encrypted_column.rb +25 -0
  22. data/lib/pgcrypto/key.rb +0 -10
  23. data/lib/pgcrypto/key_manager.rb +11 -0
  24. data/lib/pgcrypto/railtie.rb +15 -0
  25. data/lib/pgcrypto/table.rb +11 -0
  26. data/lib/pgcrypto/table_manager.rb +2 -10
  27. data/lib/tasks/pgcrypto.rake +7 -0
  28. data/pgcrypto.gemspec +24 -17
  29. data/spec/lib/pgcrypto_spec.rb +101 -100
  30. data/spec/spec_helper.rb +27 -28
  31. metadata +20 -36
  32. data/lib/generators/pgcrypto/install/templates/migration.rb +0 -17
  33. data/lib/pgcrypto/active_record.rb +0 -88
  34. data/lib/pgcrypto/arel.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c9f559149b0f0fb92a83866b18afc506d80c7fde
4
- data.tar.gz: 0289e4d9a15a047e4c8d87155af4352d88c155a3
3
+ metadata.gz: b99c62fc6bec129aea118f72d80707481a5432af
4
+ data.tar.gz: c2a12881ae7160f7d1660470a31fc3ae508637e5
5
5
  SHA512:
6
- metadata.gz: 7967944ddd6457d57a3e202fa2a06b7e6a8b8c04cc1a17c4a41916256a87c4bd7be89eb0c0669dcc19e73810f206670894d0303587441f28f9ad5ee04f34e44b
7
- data.tar.gz: 1e64bf7e07b3bc88dc6a6afad090d1825664af6b8c8676c21137e802c8e6d05e1ba0fb26294b7a11f690dd43608cdcf3f0172092c746e8bf54400b328c72b24e
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', rspec_version = '>= 2.6'
12
+ gem 'rspec', '>= 2.6'
16
13
  gem 'simplecov', :require => false
17
14
  gem 'terminal-notifier'
18
15
  end
@@ -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. It's literally *one giant hack,*
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
- Installation
8
- -
5
+ #### **WARNING TO 0.3.x USERS**:
9
6
 
10
- PGCrypto will load the `pgcrypto` extension into your database if you haven't already, but this change will NOT get propagated
11
- to your schema.rb file, so... go figure. You'll have to `CREATE EXTENSION IF NOT EXISTS pgcrypto` any database built from the
12
- schema file (**HINT** that means your test databases). Anyway, do the following.
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. Then bundle it:
19
-
20
- bundle
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
- 3. Generate the migration and the initializer:
47
+ production:
48
+ <<: *common
49
+ host: whatevs
50
+ database: my_app_production
51
+ username: whatevs
52
+ password: totally_not_my_app
23
53
 
24
- rails g pgcrypto:install
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
- 4. Edit the initializer in `config/initializers/pgcrypto.rb` to point to your public and private GPG keys:
28
-
29
- PGCrypto.keys[:private] = {:path => "~/.keys/private.key"}
30
- PGCrypto.keys[:public] = {:path => "~/.keys/public.key"}
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
- pgcrypto :social_security_number
85
+ has_encrypted_column :social_security_number
38
86
 
39
87
  # ... some other fun stuff
40
88
  end
41
89
 
42
- 6. Profit
43
-
44
- User.create!(:social_security_number => "466-99-1234") #=> #<User with stuff>
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
- Keys
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
- Warranty (or lack thereof)
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***. As I mentioned, this works great in our
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.3.6
1
+ 0.4.1
@@ -0,0 +1,4 @@
1
+ require 'active_record/version'
2
+ require 'pgcrypto/adapter'
3
+
4
+ require "active_record/connection_adapters/pgcrypto_adapter/rails_#{ActiveRecord::VERSION::MAJOR}"
@@ -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
@@ -1,118 +1,28 @@
1
- require 'big_spoon'
2
- require 'pgcrypto/active_record'
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
- class << self
10
- def [](key)
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
- class Error < StandardError; end
20
-
21
- module ClassMethods
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
- def pgcrpyto_columns
81
- PGCrypto[table_name]
82
- end
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
- module InstanceMethods
86
- def select_pgcrypto_column(column_name)
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
- if defined? ActiveRecord::Base
116
- ActiveRecord::Base.extend PGCrypto::ClassMethods
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