├── VERSION ├── .rspec ├── .gitignore ├── .simplecov ├── lib ├── pgcrypto │ ├── generators │ │ ├── install │ │ │ ├── templates │ │ │ │ ├── migration.rb │ │ │ │ └── initializer.rb │ │ │ ├── USAGE │ │ │ └── install_generator.rb │ │ ├── upgrade │ │ │ ├── USAGE │ │ │ ├── upgrade_generator.rb │ │ │ └── templates │ │ │ │ └── migration.rb │ │ └── base_generator.rb │ ├── table.rb │ ├── table_manager.rb │ ├── key_manager.rb │ ├── railtie.rb │ ├── column.rb │ ├── column_converter.rb │ ├── has_encrypted_column.rb │ ├── key.rb │ └── adapter.rb ├── active_record │ └── connection_adapters │ │ ├── pgcrypto_adapter.rb │ │ └── pgcrypto_adapter │ │ ├── rails_3.rb │ │ └── rails_4.rb ├── tasks │ └── pgcrypto.rake └── pgcrypto.rb ├── Guardfile ├── Gemfile ├── Rakefile ├── LICENSE ├── spec ├── support │ ├── public.key │ ├── private.key │ ├── public.password.key │ └── private.password.key ├── spec_helper.rb └── lib │ └── pgcrypto_spec.rb ├── pgcrypto.gemspec ├── CHANGES.md └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.1 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --backtrace 2 | --colour 3 | --format Fuubar 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .DS_Store 3 | coverage/ 4 | Gemfile.lock -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start do 2 | add_filter "test.rb" 3 | add_filter "/spec/" 4 | end 5 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/install/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class InstallPgcrypto < ActiveRecord::Migration 2 | def change 3 | enable_extension "pgcrypto" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/pgcrypto_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/version' 2 | require 'pgcrypto/adapter' 3 | 4 | require "active_record/connection_adapters/pgcrypto_adapter/rails_#{ActiveRecord::VERSION::MAJOR}" 5 | -------------------------------------------------------------------------------- /lib/pgcrypto/table.rb: -------------------------------------------------------------------------------- 1 | module PGCrypto 2 | class Table < Hash 3 | def [](key) 4 | super(key.to_sym) 5 | end 6 | 7 | def []=(key, value) 8 | super key.to_sym, value 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/pgcrypto/table_manager.rb: -------------------------------------------------------------------------------- 1 | require 'pgcrypto/table' 2 | 3 | module PGCrypto 4 | class TableManager < Table 5 | def [](key) 6 | return {} unless key 7 | super(key) || self[key] = Table.new 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/upgrade/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates the PGCrypto upgrade-from-0.3.x migration 3 | 4 | Example: 5 | rails generate pgcrypto:install 6 | 7 | This will create: 8 | db/migrate/XXXXXX_upgrade_pgcrypto_to_0_4_0.rb 9 | -------------------------------------------------------------------------------- /lib/tasks/pgcrypto.rake: -------------------------------------------------------------------------------- 1 | namespace :pgcrypto do 2 | desc "Migrate PGCrypto 0.3.x-style columns to 0.4 style" 3 | task migrate_old_columns: :environment do 4 | require 'pgcrypto/column_converter' 5 | PGCrypto::ColumnConverter.migrate! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/pgcrypto/key_manager.rb: -------------------------------------------------------------------------------- 1 | module PGCrypto 2 | class KeyManager < Hash 3 | def []=(key, value) 4 | unless value.is_a?(Key) 5 | value = Key.new(value) 6 | end 7 | value.name = key 8 | super key, value 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/install/templates/initializer.rb: -------------------------------------------------------------------------------- 1 | # Uncomment the line below and point it to your private key 2 | # PGCrypto.keys[:private] = {:path => 'path/to/private/keyfile'} 3 | 4 | # You can also specify the file contents directly: 5 | # PGCrypto.keys[:private] = ENV['PRIVATE_KEY'] 6 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates the PGCrypto installation migration and key configuration files 3 | 4 | Example: 5 | rails generate pgcrypto:install 6 | 7 | This will create: 8 | config/initializers/pgcrypto.rb 9 | db/migrate/XXXXXX_install_pgcrypto.rb 10 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | notification :terminal_notifier 4 | 5 | guard 'rspec', :version => 2 do 6 | watch(%r{^spec/.+_spec\.rb$}) 7 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 8 | watch('spec/spec_helper.rb') { "spec" } 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | group :development do 4 | gem 'jeweler' 5 | end 6 | 7 | group :test do 8 | gem 'database_cleaner', '>= 0.7' 9 | gem 'fuubar' 10 | gem 'guard-rspec' 11 | gem 'pg', '>= 0.11' 12 | gem 'rspec', '>= 2.6' 13 | gem 'simplecov', :require => false 14 | gem 'terminal-notifier' 15 | end 16 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/base_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/migration' 2 | require 'rails/generators/active_record/migration' 3 | 4 | class BaseGenerator < Rails::Generators::Base 5 | include Rails::Generators::Migration 6 | extend ActiveRecord::Generators::Migration 7 | 8 | def self.next_migration_number(*args) 9 | Time.now.utc.strftime("%Y%m%d%H%M%S").to_i.to_s 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/upgrade/upgrade_generator.rb: -------------------------------------------------------------------------------- 1 | require 'pgcrypto/generators/base_generator' 2 | 3 | module Pgcrypto 4 | module Generators 5 | class UpgradeGenerator < BaseGenerator 6 | 7 | source_root File.expand_path('../templates', __FILE__) 8 | 9 | def copy_migration 10 | migration_template("migration.rb", "db/migrate/upgrade_pgcrypto_to_0_4_0.rb") 11 | end 12 | 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/pgcrypto/railtie.rb: -------------------------------------------------------------------------------- 1 | module PGCrypto 2 | class Railtie < Rails::Railtie 3 | generators do 4 | require 'pgcrypto/generators/install/install_generator' 5 | require 'pgcrypto/generators/upgrade/upgrade_generator' 6 | end 7 | 8 | rake_tasks do 9 | tasks = File.join(File.dirname(__FILE__), '../tasks/*.rake') 10 | Dir[tasks].each do |file| 11 | load file 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/pgcrypto/column.rb: -------------------------------------------------------------------------------- 1 | module PGCrypto 2 | class Column < ActiveRecord::Base 3 | 4 | self.table_name = 'pgcrypto_columns' 5 | 6 | belongs_to :owner, polymorphic: true 7 | 8 | has_encrypted_column :value 9 | 10 | def self.tables_and_columns 11 | tables_and_columns = [] 12 | select('DISTINCT owner_type, name').each do |column| 13 | tables_and_columns.push [column.owner_type.constantize.table_name, column.name] 14 | end 15 | tables_and_columns 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'pgcrypto/generators/base_generator' 2 | 3 | module Pgcrypto 4 | module Generators 5 | class InstallGenerator < BaseGenerator 6 | 7 | source_root File.expand_path('../templates', __FILE__) 8 | 9 | def copy_migration 10 | migration_template("migration.rb", "db/migrate/install_pgcrypto") 11 | end 12 | 13 | def create_initializer 14 | copy_file("initializer.rb", "config/initializers/pgcrypto.rb") 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/pgcrypto/column_converter.rb: -------------------------------------------------------------------------------- 1 | require 'pgcrypto/column' 2 | 3 | module PGCrypto 4 | class ColumnConverter 5 | 6 | def self.migrate! 7 | new.migrate! 8 | end 9 | 10 | def migrate! 11 | PGCrypto::Column.find_each(batch_size: 100) do |column| 12 | migrate_column(column) 13 | end 14 | end 15 | 16 | private 17 | 18 | def migrate_column(column) 19 | if column.owner 20 | column.owner.update_column(column.name, column.value) 21 | puts "Migrated #{column.owner}##{column.name}" 22 | end 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | begin 4 | require 'jeweler' 5 | Jeweler::Tasks.new do |gemspec| 6 | gemspec.name = "pgcrypto" 7 | gemspec.summary = "A transparent ActiveRecord::Base extension for encrypted columns" 8 | gemspec.description = %{ 9 | PGCrypto is an ActiveRecord::Base extension that allows you to asymmetrically 10 | encrypt PostgreSQL columns with as little trouble as possible. It's totally 11 | freaking rad. 12 | } 13 | gemspec.email = "flip@x451.com" 14 | gemspec.homepage = "http://github.com/Plinq/pgcrypto" 15 | gemspec.authors = ["Flip Sasser"] 16 | end 17 | rescue LoadError 18 | end 19 | -------------------------------------------------------------------------------- /lib/pgcrypto/generators/upgrade/templates/migration.rb: -------------------------------------------------------------------------------- 1 | require 'pgcrypto/column' 2 | require 'pgcrypto/column_converter' 3 | 4 | class UpgradePgcryptoTo040 < ActiveRecord::Migration 5 | def up 6 | # Add columns based on the ones we already know exist 7 | PGCrypto::Column.tables_and_columns do |table, column| 8 | add_column table, column, :binary 9 | end 10 | 11 | # Migrate column data 12 | PGCrypto::ColumnConverter.migrate! 13 | 14 | # Drop the old, now-unused columns table 15 | # COMMENT THIS IN IF YOU REALLY WANT IT 16 | # drop_table :pgcrypto_columns 17 | end 18 | 19 | def down 20 | raise ActiveRecord::IrreversibleMigration 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/pgcrypto/has_encrypted_column.rb: -------------------------------------------------------------------------------- 1 | module PGCrypto 2 | module HasEncryptedColumn 3 | def has_encrypted_column(*column_names) 4 | options = column_names.extract_options! 5 | options.reverse_merge(type: :pgp) 6 | 7 | column_names.each do |column_name| 8 | # Stash the encryption type in our module 9 | PGCrypto[table_name][column_name.to_s] ||= options.symbolize_keys 10 | end 11 | end 12 | 13 | def pgcrypto(*args) 14 | if defined? Rails 15 | Rails.logger.debug "[DEPRECATION WARNING] `pgcrypto' is deprecated. Please use `has_encrypted_column' instead!" 16 | end 17 | has_encrypted_column(*args) 18 | end 19 | 20 | end 21 | end 22 | 23 | if defined? ActiveRecord::Base 24 | ActiveRecord::Base.extend PGCrypto::HasEncryptedColumn 25 | end 26 | -------------------------------------------------------------------------------- /lib/pgcrypto.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/postgresql_adapter' 2 | require 'pgcrypto/has_encrypted_column' 3 | require 'pgcrypto/key' 4 | require 'pgcrypto/key_manager' 5 | require 'pgcrypto/table_manager' 6 | 7 | module PGCrypto 8 | def self.[](key) 9 | (@table_manager ||= TableManager.new)[key] 10 | end 11 | 12 | def self.base_adapter 13 | @base_adapter ||= ActiveRecord::ConnectionAdapters::PostgreSQLAdapter 14 | end 15 | 16 | def self.base_adapter=(base_adapter) 17 | @base_adapter = base_adapter 18 | rebuild_adapter! if respond_to?(:rebuild_adapter!) 19 | end 20 | 21 | def self.keys 22 | @keys ||= KeyManager.new 23 | end 24 | end 25 | 26 | PGCrypto.keys[:public] = {:path => '.pgcrypto'} if File.file?('.pgcrypto') 27 | 28 | require 'pgcrypto/railtie' if defined? Rails::Railtie 29 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/pgcrypto_adapter/rails_3.rb: -------------------------------------------------------------------------------- 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 | 21 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/pgcrypto_adapter/rails_4.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Phillip Sasser 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/support/public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.4.12 (Darwin) 3 | 4 | mI0ET3J1UAEEAN9OYtT4bX1XPyK6BNl+7FgbYpYjn5QS9bQA8TRcX1H1tFxrzib5 5 | NsyaGzSEda8f7nGY+AE3xq3HU8AmPrktm58tCKldvhktg6LY5Y+Hu6aUxqhKvBhQ 6 | jtIfEv/OpGR3AeKuJzFOV+q/iU9zuTEOPB+R53ycNZxWQKt5lUtPDBR/ABEBAAG0 7 | NEZsaXAgU2Fzc2VyIChEZWxpZ2h0ZnVsIFdpZGdldHMpIDxmbGlwQGdldHBsaW5x 8 | LmNvbT6IuAQTAQIAIgUCT3J1UAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA 9 | CgkQSOpAHKUTK5++NQQAk9PY+UxBu2J4xX/XqF34eSaWFr/sXZwCWYzHZfnDGeQs 10 | WQfr8czYTl9jsAlruXxIdsCm20bi1h52uknm0Yxc2Oti3dIo6lckT878/7+1Z/KQ 11 | arE7yT4F1JqzJivTgDAADOuDcET4gMUpELXN6htrmnA8ghCNdLivTE/8zZguiEi4 12 | jQRPcnVQAQQA9MEq2zMJtfWLZ2HfkTYkHemesc4XB+xCLfldNdmUwQ4o+GXfxe6V 13 | XYCcKPpyx+qXSP6q7OxR8QKk8wbIord2/w6iIkn+ULvHQw1KMaCQW3JuQPB+IMWA 14 | 3kY8eQ3uibc2vk3ZFMtlU9HCxcRBPnMsCaBXYH1EkQ0+JVyuAotFOksAEQEAAYif 15 | BBgBAgAJBQJPcnVQAhsMAAoJEEjqQBylEyuf/DoEAM3THz28/QsZGmfArZnwBUhe 16 | KVw83F7xySrHHRchTlfXd3oO280KWEi7Y3Z8DQzb89Hv8wbz7MahktlS8LQP5ZQ2 17 | rdjmFBvYrskgBU81hUf5HTn/W1Yawf/yH94f0++V1Dy2NBXiOAsmrHn882tJvpZ1 18 | JSagpCuA5EFA1ekeEGvq 19 | =XKvD 20 | -----END PGP PUBLIC KEY BLOCK----- 21 | -------------------------------------------------------------------------------- /lib/pgcrypto/key.rb: -------------------------------------------------------------------------------- 1 | module PGCrypto 2 | class Key 3 | attr_accessor :name, :password, :value 4 | attr_reader :path 5 | attr_writer :armored 6 | 7 | def armored? 8 | @armored 9 | end 10 | 11 | def dearmored 12 | "#{'dearmor(' if armored?}'#{self}'#{')' if armored?}" 13 | end 14 | 15 | def encrypt(value) 16 | %[pgp_pub_encrypt('#{value}', #{dearmored})] 17 | end 18 | 19 | def initialize(options = {}) 20 | if options.is_a?(String) 21 | self.value = options 22 | elsif options.is_a?(Hash) 23 | options.each do |key, value| 24 | send("#{key}=", value) 25 | end 26 | end 27 | end 28 | 29 | def password? 30 | ", '#{password}'" if password 31 | end 32 | 33 | def path=(keyfile) 34 | keyfile = File.expand_path(keyfile) 35 | raise PGCrypto::Error, "#{keyfile} does not exist!" unless File.file?(keyfile) 36 | @path = keyfile 37 | self.value = File.read(keyfile) 38 | end 39 | 40 | def to_s 41 | value 42 | end 43 | 44 | def value=(key) 45 | if key =~ /^-----BEGIN PGP / 46 | self.armored = true 47 | else 48 | self.armored = false 49 | end 50 | @value = key.dup.freeze 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/support/private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v1.4.12 (Darwin) 3 | 4 | lQHYBE9ydVABBADfTmLU+G19Vz8iugTZfuxYG2KWI5+UEvW0APE0XF9R9bRca84m 5 | +TbMmhs0hHWvH+5xmPgBN8atx1PAJj65LZufLQipXb4ZLYOi2OWPh7umlMaoSrwY 6 | UI7SHxL/zqRkdwHiricxTlfqv4lPc7kxDjwfked8nDWcVkCreZVLTwwUfwARAQAB 7 | AAP6Ay+UG2O79CjVfsJWpV+5MXyaiHfTpAItPTcyOcQDnCC8RQFIvKebj4m3T6WA 8 | JFJ5TWeYSjQognwwhrJE/NFYwgLH698fEw3TEHyvXm6hdb3Aqv7wCc52pZx9E3OL 9 | AvSQArTypTO4WsGsYIBVbpvsQervA03gxEo025q7QGPQYoECAOcC9eTlKznjDj09 10 | 5AOueWE1s3NZDlESGkq6qEM+xIHurEvgX7jHLe0bRQWfo/Z/D5uplTrFkPTgtA/a 11 | lwKej78CAPd2DYocgmCQ44e1Dc8Z8XSeKuMmmZqbFszDr8+4LM40irmFeiSb1S6p 12 | rj1h2WKFhnar568tMSvxrZmu5dZ7q0EB/1kVXLAJK6PS7IhOnFMbMMQcWmQLfQ7I 13 | pgEuUF6DG9drSbj8r5oU+PUejGmurl5ZqVWbCGHnlbTU/QsRhp8VvdegMLQ0Rmxp 14 | cCBTYXNzZXIgKERlbGlnaHRmdWwgV2lkZ2V0cykgPGZsaXBAZ2V0cGxpbnEuY29t 15 | Poi4BBMBAgAiBQJPcnVQAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBI 16 | 6kAcpRMrn741BACT09j5TEG7YnjFf9eoXfh5JpYWv+xdnAJZjMdl+cMZ5CxZB+vx 17 | zNhOX2OwCWu5fEh2wKbbRuLWHna6SebRjFzY62Ld0ijqVyRPzvz/v7Vn8pBqsTvJ 18 | PgXUmrMmK9OAMAAM64NwRPiAxSkQtc3qG2uacDyCEI10uK9MT/zNmC6ISJ0B2ARP 19 | cnVQAQQA9MEq2zMJtfWLZ2HfkTYkHemesc4XB+xCLfldNdmUwQ4o+GXfxe6VXYCc 20 | KPpyx+qXSP6q7OxR8QKk8wbIord2/w6iIkn+ULvHQw1KMaCQW3JuQPB+IMWA3kY8 21 | eQ3uibc2vk3ZFMtlU9HCxcRBPnMsCaBXYH1EkQ0+JVyuAotFOksAEQEAAQAD+gJY 22 | HhfdLGDv4G08KXIdTlZANRMBGGss6GtcgcSKXqh3540nG9Z4+2xBI/3Br4fzp43j 23 | ub5gLkUixqXfPdyB6Yokc+U0AQVuKZPXbFAROigD2gzpnDWaLeByL4Ua6FV6zNsl 24 | gTLIS++r81KJBkiw0yhgqfNt44Gbta7y6nPXdMGRAgD1j+wyfvrhoAGgNkZR1h6U 25 | A+enlaK5+TdeDFljJKo8w3io/s9abOLnVBE4rRsdZtJmK5q0rjeMNKzOsohk9EHR 26 | AgD/KHTbvTsvrH6kcKpdCHGjyEnYAndxVbjoclHR7NRz6QlhqdNKGI0FbwmPcf7p 27 | WOSXJX6j8u4dHrx1wZTcDMVbAf9/VsMOkzYnSQEzDV2ixunvkYmfNuD1NbYERgyG 28 | 4Zd+TWKLPx98y/+VW8js5FkH46XgoOsYkEGjl3ZJb5kPth1rog2InwQYAQIACQUC 29 | T3J1UAIbDAAKCRBI6kAcpRMrn/w6BADN0x89vP0LGRpnwK2Z8AVIXilcPNxe8ckq 30 | xx0XIU5X13d6DtvNClhIu2N2fA0M2/PR7/MG8+zGoZLZUvC0D+WUNq3Y5hQb2K7J 31 | IAVPNYVH+R05/1tWGsH/8h/eH9PvldQ8tjQV4jgLJqx5/PNrSb6WdSUmoKQrgORB 32 | QNXpHhBr6g== 33 | =sN2M 34 | -----END PGP PRIVATE KEY BLOCK----- 35 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'simplecov' 3 | 4 | # Add lib/ to the load path 5 | $LOAD_PATH.unshift(File.expand_path(File.join('..', 'lib'), File.dirname(__FILE__))) 6 | 7 | require 'database_cleaner' 8 | require 'pry' 9 | 10 | gem 'activerecord', ENV.fetch('ACTIVE_RECORD_VERSION', '>= 4.0') 11 | require 'active_record' 12 | require 'pgcrypto' 13 | 14 | RSpec.configure do |config| 15 | database_config = { 16 | adapter: 'pgcrypto', 17 | database: '__pgcrypto_gem_test', 18 | encoding: 'utf8', 19 | host: 'localhost' 20 | } 21 | postgres_config = database_config.merge(:database => 'postgres', :schema_search_path => 'public') 22 | 23 | # Set up the database to handle pgcrypto functions and the schema for 24 | # our tests 25 | config.before :suite do 26 | # Connect to the local postgres schema database 27 | ActiveRecord::Base.establish_connection(postgres_config) 28 | 29 | # Create the test database if we can 30 | ActiveRecord::Base.connection.create_database(database_config[:database]) rescue nil 31 | 32 | # Now connect to the newly created database 33 | ActiveRecord::Base.establish_connection(database_config) 34 | 35 | silence_stream(STDOUT) do 36 | # ...and load in the pgcrypto extension 37 | ActiveRecord::Base.connection.execute(%[CREATE EXTENSION pgcrypto]) rescue nil 38 | 39 | # ...and then set up the pgcrypto_columns and pgcrypto_test_models fun 40 | ActiveRecord::Schema.define do 41 | create_table :pgcrypto_test_models, :force => true do |t| 42 | t.string :name, :limit => 32 43 | t.binary :encrypted_text 44 | end 45 | end 46 | end 47 | 48 | ActiveRecord::Base.establish_connection(database_config) 49 | 50 | DatabaseCleaner.strategy = :transaction 51 | end 52 | 53 | config.before :each do 54 | DatabaseCleaner.start 55 | 56 | class PGCryptoTestModel < ActiveRecord::Base 57 | self.table_name = :pgcrypto_test_models 58 | has_encrypted_column :encrypted_text 59 | end 60 | end 61 | 62 | config.after :each do 63 | DatabaseCleaner.clean 64 | end 65 | 66 | config.after :suite do 67 | # Drop the database when we exit 68 | ActiveRecord::Base.establish_connection(postgres_config) 69 | ActiveRecord::Base.connection.drop_database(database_config[:database]) rescue nil 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /spec/support/public.password.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.4.12 (Darwin) 3 | 4 | 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 37 | LYrUQ/uHaSPVpl6O5g== 38 | =XIMO 39 | -----END PGP PUBLIC KEY BLOCK----- 40 | -------------------------------------------------------------------------------- /spec/support/private.password.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v1.4.12 (Darwin) 3 | 4 | lQN5BFA089kRCACUMwRcvav4F5EV0VSdSgGNve97JEmNMpf17C/jhBXLjCHurg/i 5 | +rG3IVkls0UJ3gBcZa0jTLw9p2MA6JSIudweV2OL12xBSiVDTJfisEH6cSvMLjUp 6 | TETbV5lx1lnuPh1sQ3/hT9hO4vVMLBb/DKTGQqK15kuDoEwp/i/ReCcstkRZWWxV 7 | jrJ9oX9x8cUU4Bds3HTBwWskn5f9TETMIMcwdij1wj/xGL24OoGC2qoOWZuSG+Gf 8 | fIPzA6eymbTMNajOhbEM84KqQYJdx+JUhUjdfkoLq3cWjPZENzt3dwpRnjUaI/JY 9 | E2wO068zByuP5xvobGP/IzhN632B0R49mmU/AQDFugt5L6mERuxXAwpuH1ff532p 10 | wxcFAzvM6nK3IR19CQf+M54olw/iRvGGxP0E7kTGI8lF+8Ant/mc7eSbY0Tx328N 11 | VgijIBdMCZtq8qh0Keyo/czNcDiZnaqo56wKvOcdasWOpu+mH+xCZ9Sm60lUxT+E 12 | SW94fuAfB8n44W4iSSO98/qr5wG4yjXXv4CkOMStsdq5Gw6sMz7iM7LXDUmbXKjM 13 | kuccSmX/DDaQg5upNF8FjsKa3Rdcj1XWVo9ymCVKfskR49OO8qCMYtq338CTQpLY 14 | 0uK+Ee1aQ2r0PgiTYFye9Bj26jhvuoHSBPifzfy0Ot0LgeXqOQATG2bFg0UxHbaC 15 | 6uI+HPgCD1Sn3u488TcgwC35j9Pgd2HM4isJF7X+CwgAkhTnvGSwroXsS+/L+lXJ 16 | 6cronHTFi3vFTJAgYhxkwW7E9sA89SvwURXupj1VVjwcJblXUwytGsFkzvj7VC21 17 | sg+GdkGTF0UwYmXBTbZaAo1ALL3Dk39v1WrAs75c6l7i7Rh739ITybXpf4OleErF 18 | l+4ZCwydO8MlKu9TheEBksBE3jiUTV4+667BwczwdRyU5KQINU19LFgFaTcnaJk3 19 | YjQKZVeoW886TtZCvJRZEg0S75Pc6aUncnZXL7sWcZPZt+2+k1ujLkNWLXN5u8Ay 20 | +PMCtcJFo/xbZtyz+kqVIU+LecQeN46mXMhPb9f99a1Dm84wkU8z5GxI6IsjDR56 21 | Kv4DAwIwCb01ALE6MWBBcR67+YzhaWwMsZCHmPZb4Ti9oGXZ7whxzzCSAEHOXmad 22 | ROBo83H+KwRsx3TRugAPMz895KRGFqhMYTIBALQbRmxpcCBTYXNzZXIgPGZsaXBA 23 | eDQ1MS5jb20+iHoEExEIACIFAlA089kCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B 24 | AheAAAoJEKOB+ElM1IbKwGQA/2dgzwKu0kzfI8Bz2/wckVQQ6DIXA0AIP5Ekdqje 25 | YX2oAPsG3Ckap0WVMffYGkZoBgFl07hHjwX7sJE19v/mAQR5rp0CYwRQNPPZEAgA 26 | 8uI6PUMoe7E8FD0nd7/hCvCzVaYpRFF6DGGJ7ZMHb3cmnEECjRoucLlgVd6dh4B2 27 | 6WG68KcQaRXy1i9cQ8HZdbY4bOkKlCFRk4RtiARwM4oPsrUfGDGnVj5OgUMNqybW 28 | vq7bg7+3y4FSjMoKQ+EsQeIoAC+kmsMT+oaRXXP4TUV20sjZ3dAtBaRGB+Gl1Chj 29 | Hx7df6UL2xTQgOTcdjsASNDQLdc27geIQPGLwbZK6ojKCU4nz618Zb3Z08tkK/mZ 30 | n5qLyIMID3Zi4hy98BMMfWi4mZ2wbRLTo38UDcl1EqKbCKNUOotQDOyd2EQaPlPM 31 | +/OLevm1hJQHxeIF8Xgv4wADBQf+MK5pBLBNB9o//vIeOLxnA6bDRcxEZHpMLq8U 32 | FH0O4VAt7naeBdBKT8kAfiwAZroikMQsGTr1eY2hMZsfQ3cOGqZgW1tBnaiPIofj 33 | cntOx2Zs/G8wQebdINPRjKyQQ3IqpUWpCds7b2Wg4dSpySEFSIa40MlpUREpzZAP 34 | 8VmfCAXO2VCtEMtQkDlzd3ieb5bNTzrb6Bcvg2bpP7uemgg9MnMXjZ+bzlwsfFNK 35 | Ug6eRYrR7RMvDL/2n7EHRO7R+twkMTVg/mORQa46joaRFg3xmUjOLILCsHSOrFUD 36 | e5bUr46XTkzi0BjKQi5MfmvDA9xuOwM093yKAmO4Y9B6Toh7n/4DAwIwCb01ALE6 37 | MWBPEAIGaGRaotx1cV6A/86DX4BcY6o1IiqkuYHwZpf0fIIFx2/5pPaIKjnTkNMh 38 | NycuT5turNvT2Tq1O2lIA7Fj1HpqYHTv/8+QiGEEGBEIAAkFAlA089kCGwwACgkQ 39 | o4H4SUzUhsrM0wD6AtwrjECMDaZ4HpP6kd/B2SVlosKp6h7tAXgIcvpZw7UBAIg1 40 | yWPio8RK5wB+oEJ46eaCMnmMciWnX19c/mjxzpOu 41 | =+qjx 42 | -----END PGP PRIVATE KEY BLOCK----- -------------------------------------------------------------------------------- /pgcrypto.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: pgcrypto 0.4.0 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "pgcrypto" 9 | s.version = "0.4.0" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Flip Sasser"] 14 | s.date = "2014-08-24" 15 | s.description = "\n PGCrypto is an ActiveRecord::Base extension that allows you to asymmetrically\n encrypt PostgreSQL columns with as little trouble as possible. It's totally\n freaking rad.\n " 16 | s.email = "flip@x451.com" 17 | s.extra_rdoc_files = [ 18 | "LICENSE", 19 | "README.markdown" 20 | ] 21 | s.files = [ 22 | ".rspec", 23 | ".simplecov", 24 | "CHANGES.md", 25 | "Gemfile", 26 | "Guardfile", 27 | "LICENSE", 28 | "README.markdown", 29 | "Rakefile", 30 | "VERSION", 31 | "lib/active_record/connection_adapters/pgcrypto_adapter.rb", 32 | "lib/active_record/connection_adapters/pgcrypto_adapter/rails_3.rb", 33 | "lib/active_record/connection_adapters/pgcrypto_adapter/rails_4.rb", 34 | "lib/pgcrypto.rb", 35 | "lib/pgcrypto/adapter.rb", 36 | "lib/pgcrypto/column.rb", 37 | "lib/pgcrypto/column_converter.rb", 38 | "lib/pgcrypto/generators/base_generator.rb", 39 | "lib/pgcrypto/generators/install/USAGE", 40 | "lib/pgcrypto/generators/install/install_generator.rb", 41 | "lib/pgcrypto/generators/install/templates/initializer.rb", 42 | "lib/pgcrypto/generators/install/templates/migration.rb", 43 | "lib/pgcrypto/generators/upgrade/USAGE", 44 | "lib/pgcrypto/generators/upgrade/templates/migration.rb", 45 | "lib/pgcrypto/generators/upgrade/upgrade_generator.rb", 46 | "lib/pgcrypto/has_encrypted_column.rb", 47 | "lib/pgcrypto/key.rb", 48 | "lib/pgcrypto/key_manager.rb", 49 | "lib/pgcrypto/railtie.rb", 50 | "lib/pgcrypto/table.rb", 51 | "lib/pgcrypto/table_manager.rb", 52 | "lib/tasks/pgcrypto.rake", 53 | "pgcrypto.gemspec", 54 | "spec/lib/pgcrypto_spec.rb", 55 | "spec/spec_helper.rb", 56 | "spec/support/private.key", 57 | "spec/support/private.password.key", 58 | "spec/support/public.key", 59 | "spec/support/public.password.key" 60 | ] 61 | s.homepage = "http://github.com/Plinq/pgcrypto" 62 | s.rubygems_version = "2.4.1" 63 | s.summary = "A transparent ActiveRecord::Base extension for encrypted columns" 64 | 65 | if s.respond_to? :specification_version then 66 | s.specification_version = 4 67 | 68 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 69 | s.add_development_dependency(%q, [">= 0"]) 70 | else 71 | s.add_dependency(%q, [">= 0"]) 72 | end 73 | else 74 | s.add_dependency(%q, [">= 0"]) 75 | end 76 | end 77 | 78 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | ## 0.4.0 3 | - Refactored EVERYTHING to support encryption directly 4 | on columns. See README for upgrade instructions. 5 | 6 | ## 0.3.5 7 | - Fixed ActiveRecord dependency issue (now handles any 8 | version of ActiveRecord from 3.2 to current) 9 | 10 | ## 0.3.4 11 | - Rewrote specs and found a bug in password-protected keys; 12 | fixed now! Thanks again to Brett for helping me find the 13 | problem. 14 | 15 | ## 0.3.3 16 | - Solved a mass-assignment issue thanks to Brett Levine 17 | 18 | ## 0.3.2 19 | - Upgrade big_spoon dependency to solve annoying generator bug. 20 | 21 | ## 0.3.1 22 | - Finally solved a large bug with a very simple fix. Looks 23 | like we're approaching stability, folks! 24 | 25 | ## 0.3.0 26 | - Finally solved a large bug with a very simple fix. Looks 27 | like we're approaching stability, folks! 28 | 29 | ## 0.2.7 30 | - So, about that library I arrogantly included? Turns out we 31 | should require it in code before expecting anything of it. 32 | 33 | ## 0.2.6 34 | - We now use the BigSpoon library to hook into reload, because 35 | it's SO MUCH MORE AWESOME THAN THE GARBAGE MOST PEOPLE USE. 36 | Welp. It's been nice coding with y'all. 37 | 38 | ## 0.2.5 39 | - PGCrypto now hooks into ActiveRecord::Base#reload in order to 40 | reset encrypted column values as expected when calling reload. 41 | 42 | ## 0.2.2 43 | - Don't try to load columns on new records; should further reduce 44 | unnecessary database calls! 45 | 46 | ## 0.2.1 47 | - Added ActiveModel::Dirty support, so you can now call *_changed? 48 | on your models tracking pgcrypto columns. 49 | 50 | ## 0.2.0 51 | - Overhauled key system. Unfortunately, for performance reasons 52 | and due to the insanely hacked nature of PGCrypto, multiple keys 53 | are NO LONGER SUPPORTED. I'm working to bring them back, but 54 | this was the only solution to get fully performant and functional 55 | without any disasters. 56 | 57 | ## 0.1.2 58 | - Added automatic installation of the pgcrypto extension if'n it 59 | doesn't already exist. Helpful, but doesn't fully make the 60 | `rake db:test:prepare` cut yet. Still working on that bit... 61 | 62 | ## 0.1.1 63 | - Rebuilt the WHERE clause stuff to make sure finders AND setters 64 | both worked. It's fragile and hackish at this time, but we'll get 65 | there, folks! 66 | 67 | ## 0.1.0 68 | - Overhauled the underpinnings to rely on a separate column. Adds 69 | on-demand loading of encrypted attributes from a central table 70 | which provides dramatic speed improvements when a record's 71 | encrypted attributes aren't needed most of the time. 72 | 73 | ## 0.0.4 74 | - Compatibility fix between ActiveRecord ## 3.2.1 and ## 3.2.2 75 | 76 | ## 0.0.3 77 | - Fixed a join bug on SELECT statements 78 | 79 | ## 0.0.2 80 | - Fixed a number of key-related bugs discovered in testing with our 81 | second production app with encrypted columns. Also duck-typed AREL 82 | statement types in a few places. 83 | 84 | ## 0.0.1 85 | - INSERT, SELECT, and UPDATE statements are working. But I wrote this 86 | while testing with a production app, and thus haven't written 87 | specific tests, so don't get your panties in a twist. 88 | -------------------------------------------------------------------------------- /spec/lib/pgcrypto_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # require 'logger' 4 | # ActiveRecord::Base.logger = Logger.new(STDOUT) 5 | 6 | specs = proc do 7 | 8 | let(:stored_raw) { 9 | connection = PGCryptoTestModel.connection 10 | result = connection.select_one("SELECT encrypted_text FROM pgcrypto_test_models LIMIT 1") 11 | result['encrypted_text'] 12 | } 13 | 14 | # Default test text 15 | let(:text) { "text to encrypt" } 16 | # That text as it appears un-encrypted in a binary column - we'll compare 17 | # this to what gets set to ensure the text is properly encrypted 18 | let(:text_raw) { "\\x7465787420746f20656e6372797074" } 19 | 20 | let(:text_2) { "something else entirely" } 21 | let(:text_2_raw) { "\\x736f6d657468696e6720656c736520656e746972656c79" } 22 | 23 | it "extends ActiveRecord::Base" do 24 | expect(PGCryptoTestModel).to respond_to(:has_encrypted_column) 25 | expect(PGCryptoTestModel).to respond_to(:pgcrypto) 26 | end 27 | 28 | it "encrypts text on insert" do 29 | PGCryptoTestModel.create!(name: 'foobar', encrypted_text: text) 30 | expect(stored_raw).not_to eq(text_raw) 31 | expect(PGCryptoTestModel.last.name).to eq('foobar') 32 | end 33 | 34 | it "encrypts new text on update" do 35 | PGCryptoTestModel.create.tap do |model| 36 | model.encrypted_text = text 37 | model.save! 38 | end 39 | expect(stored_raw).not_to eq(text_raw) 40 | end 41 | 42 | it "encrypts changed text on update" do 43 | PGCryptoTestModel.create!(encrypted_text: text).tap do |model| 44 | model.update_attributes!(encrypted_text: text_2) 45 | end 46 | expect(stored_raw).not_to eq(text_2_raw) 47 | end 48 | 49 | it "keeps plaintext versions of the encrypted text" do 50 | model = PGCryptoTestModel.create!(encrypted_text: text) 51 | expect(model.encrypted_text).to eq(text) 52 | end 53 | 54 | it "decrypts text when it is selected" do 55 | model = PGCryptoTestModel.create!(encrypted_text: text) 56 | expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq(text) 57 | end 58 | 59 | it "retrieves decrypted text after update" do 60 | model = PGCryptoTestModel.create!(:encrypted_text => 'i will update') 61 | expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq('i will update') 62 | model.update_attributes!(encrypted_text: 'i updated', name: 'testy mctesterson') 63 | expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq('i updated') 64 | end 65 | 66 | it "retrieves decrypted text without update" do 67 | model = PGCryptoTestModel.create!(:encrypted_text => 'i will update') 68 | expect(PGCryptoTestModel.find(model.id).encrypted_text).to eq('i will update') 69 | model.encrypted_text = 'i updated' 70 | expect(model.encrypted_text).to eq('i updated') 71 | end 72 | 73 | it "supports querying encrypted columns transparently" do 74 | model = PGCryptoTestModel.create!(:encrypted_text => 'i am findable!') 75 | expect(PGCryptoTestModel.where(encrypted_text: model.encrypted_text)).to eq([model]) 76 | end 77 | 78 | it "tracks changes" do 79 | model = PGCryptoTestModel.create!(:encrypted_text => 'i am clean') 80 | model.encrypted_text = "now i'm not!" 81 | expect(model.encrypted_text_changed?).to be_truthy 82 | end 83 | 84 | it "is not dirty if attributes are unchanged" do 85 | model = PGCryptoTestModel.create!(:encrypted_text => 'i am clean') 86 | model.encrypted_text = 'i am clean' 87 | expect(model.encrypted_text_changed?).not_to be_truthy 88 | end 89 | 90 | it "reloads with the class" do 91 | model = PGCryptoTestModel.create!(:encrypted_text => 'i am clean') 92 | model.encrypted_text = 'i am dirty' 93 | model.reload 94 | expect(model.encrypted_text).to eq('i am clean') 95 | expect(model.encrypted_text_changed?).not_to be_truthy 96 | end 97 | 98 | it "decrypts direct selects" do 99 | model = PGCryptoTestModel.create!(:encrypted_text => 'to be selected...') 100 | expect(PGCryptoTestModel.select([:id, :encrypted_text]).where(id: model.id).first).to eq(model) 101 | end 102 | end 103 | 104 | keypath = File.expand_path(File.join(File.dirname(__FILE__), '..', 'support')) 105 | describe PGCrypto do 106 | describe "without password-protected keys" do 107 | before :all do 108 | PGCrypto.keys[:private] = {:path => File.join(keypath, 'private.key')} 109 | PGCrypto.keys[:public] = {:path => File.join(keypath, 'public.key')} 110 | end 111 | 112 | instance_eval(&specs) 113 | end 114 | 115 | describe "with password-protected keys" do 116 | before :each do 117 | PGCrypto.keys[:private] = {:path => File.join(keypath, 'private.password.key'), :password => 'password'} 118 | PGCrypto.keys[:public] = {:path => File.join(keypath, 'public.password.key')} 119 | end 120 | 121 | instance_eval(&specs) 122 | end 123 | 124 | describe "with the PostGIS adapter" do 125 | before :all do 126 | gem 'activerecord-postgis-adapter', ActiveRecord::VERSION::MAJOR == 3 ? '< 0.7' : '>= 1.1' 127 | require 'activerecord-postgis-adapter' 128 | PGCrypto.keys[:private] = {:path => File.join(keypath, 'private.key')} 129 | PGCrypto.keys[:public] = {:path => File.join(keypath, 'public.key')} 130 | PGCrypto.base_adapter = ActiveRecord::ConnectionAdapters::PostGISAdapter::MainAdapter 131 | end 132 | 133 | instance_eval(&specs) 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/pgcrypto/adapter.rb: -------------------------------------------------------------------------------- 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 163 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # PGCrypto for ActiveRecord::Base 2 | 3 | **PGCrypto** adds seamless column-level encryption to your ActiveRecord::Base subclasses. 4 | 5 | #### **WARNING TO 0.3.x USERS**: 6 | 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`. 26 | 27 | gem "pgcrypto" 28 | 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 46 | 47 | production: 48 | <<: *common 49 | host: whatevs 50 | database: my_app_production 51 | username: whatevs 52 | password: totally_not_my_app 53 | 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. 55 | 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: 81 | 82 | class User < ActiveRecord::Base 83 | # ... all kinds of neat stuff ... 84 | 85 | has_encrypted_column :social_security_number 86 | 87 | # ... some other fun stuff 88 | end 89 | 90 | 7. Profit 91 | 92 | User.create!(social_security_number: "466-99-1234") #=> # 93 | User.last.social_security_number #=> "466-99-1234" 94 | 95 | BAM. It looks innocuous on your end, but on the back end that beast is storing the social security number in 96 | a GPG-encrypted column that can only be decrypted with your secure key. 97 | 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 137 | 138 | If you want to bundle your public key with your application, PGCrypto will automatically load `RAILS_ROOT/.pgcrypto`, 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. 140 | The most straightforward is to assign the actual content of the key manually: 141 | 142 | PGCrypto.keys[:private] = "-----BEGIN PGP PRIVATE KEY BLOCK----- ..." 143 | 144 | You can also give it more specific stuff: 145 | 146 | PGCrypto.keys[:private] = {:path => ".private.key", :armored => false, :password => "myKeyPASSwhichizneededBRO"} 147 | 148 | This is especially important if you password protect your private key files (and you SHOULD, for the record)! 149 | 150 | I recommend deploy-time passing of your private key and password, to ensure it doesn't wind up in any long-term 151 | storage on your server, since if you're using this library you presumably care a little bit about security: 152 | 153 | PGCrypto.keys[:private] = {:value => ENV['PRIVATE_KEY'], :password => ENV['PRIVATE_KEY_PASSWORD']} 154 | 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) 166 | 167 | As I mentioned before, this library is one HUGE hack. This is just scratching the surface of keeping your data secure. 168 | For example, if you don't protect your log files, anyone who can read them can get your private and public keys and 169 | decrypt whatever the hell they want. You'll also have to scrub your logs, because un-encrypted data is displayed right 170 | alongside those private and public keys. 171 | 172 | Basically, this will make it easy to start with asymmetric, GPG-based, column-level encryption in PostgreSQL. But that's about 173 | it; the rest is up to you. 174 | 175 | **As such,** the author and Delightful Widgets Inc. offer ***ABSOLUTELY NO GODDAMN WARRANTY***. Sorry, folks. 176 | 177 | Copyright (C) 2012 Delightful Widgets, Inc. Built by Flip Sasser, Monkeypatcher Extraordinaire! 178 | --------------------------------------------------------------------------------