pgcrypto 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGES CHANGED
@@ -1,12 +1,21 @@
1
+ 0.1.0
2
+ * Overhauled the underpinnings to rely on a separate column. Adds
3
+ on-demand loading of encrypted attributes from a central table
4
+ which provides dramatic speed improvements when a record's
5
+ encrypted attributes aren't needed most of the time.
6
+
7
+ 0.0.4
8
+ * Compatibility fix between ActiveRecord 3.2.1 and 3.2.2
9
+
1
10
  0.0.3
2
11
  * Fixed a join bug on SELECT statements
3
12
 
4
13
  0.0.2
5
- * Fixed a number of key-related bugs discovered in testing with our
6
- second production app with encrypted columns. Also duck-typed AREL
7
- statement types in a few places.
14
+ * Fixed a number of key-related bugs discovered in testing with our
15
+ second production app with encrypted columns. Also duck-typed AREL
16
+ statement types in a few places.
8
17
 
9
18
  0.0.1
10
- * INSERT, SELECT, and UPDATE statements are working. But I wrote this
11
- while testing with a production app, and thus haven't written
19
+ * INSERT, SELECT, and UPDATE statements are working. But I wrote this
20
+ while testing with a production app, and thus haven't written
12
21
  specific tests, so don't get your panties in a twist.
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gem 'activerecord', '>= 3.2'
data/README.markdown CHANGED
@@ -17,18 +17,16 @@ You need to have PGCrypto installed before this guy will work. [LMGTFY](http://l
17
17
 
18
18
  bundle
19
19
 
20
- 3. Now point it to your public and private GPG keys:
20
+ 3. Generate the migration and the initializer:
21
+
22
+ rails g pgcrypto:install
23
+ rake db:migrate
24
+
25
+ 4. Edit the initializer in `config/initializers/pgcrypto.rb` to point out your public and private GPG keys:
21
26
 
22
27
  PGCrypto.keys[:private] = {:path => "~/.keys/private.key"}
23
28
  PGCrypto.keys[:public] = {:path => "~/.keys/public.key"}
24
29
 
25
- 4. PGCrypto columns are named `attribute_encrypted` in the binary format, so do something like this:
26
-
27
- add_column :users, :social_security_number_encrypted, :binary
28
-
29
- This will allow you to access `User#social_security_number` and store the user's social in an encrypted
30
- column called `social_security_number_encryped`.
31
-
32
30
  5. Tell the User class to encrypt and decrypt the `social_security_number` attribute on the fly:
33
31
 
34
32
  class User < ActiveRecord::Base
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.1.0
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,21 @@
1
+ require 'rails/generators/migration'
2
+ require 'rails/generators/active_record/migration'
3
+
4
+ module Pgcrypto
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ extend ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path('../templates', __FILE__)
11
+
12
+ def copy_migration
13
+ migration_template("migration.rb", "db/migrate/install_pgcrypto")
14
+ end
15
+
16
+ def create_initializer
17
+ copy_file("initializer.rb", "config/initializers/pgcrypto.rb")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
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
+
7
+ # You can also specify custom keys:
8
+ # PGCrypto.keys[:message_columns] = {:path => 'path/to/message_columns/keyfile'}
9
+ # PGCrypto.keys[:user_columns] = {:path => 'path/to/user_columns/keyfile'}
@@ -0,0 +1,14 @@
1
+ class InstallPgcrypto < ActiveRecord::Migration
2
+ def up
3
+ create_table :pgcrypto_columns do |t|
4
+ t.belongs_to :owner, :polymorphic => true
5
+ t.string :name, :limit => 64
6
+ t.binary :value
7
+ end
8
+ add_index :pgcrypto_columns, [:owner_id, :owner_type, :name], :name => :pgcrypto_column_finder
9
+ end
10
+
11
+ def down
12
+ drop_table :pgcrypto_columns
13
+ end
14
+ end
data/lib/pgcrypto.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'pgcrypto/active_record'
2
2
  require 'pgcrypto/arel'
3
+ require 'pgcrypto/column'
3
4
  require 'pgcrypto/key'
4
5
  require 'pgcrypto/table_manager'
5
6
 
@@ -17,40 +18,77 @@ module PGCrypto
17
18
  class Error < StandardError; end
18
19
 
19
20
  module ClassMethods
20
- def pgcrypto(*column_names)
21
+ def pgcrypto(*pgcrypto_column_names)
22
+ options = pgcrypto_column_names.last.is_a?(Hash) ? pgcrypto_column_names.pop : {}
23
+ options = {:include => false, :type => :pgp}.merge(options)
21
24
 
22
- options = column_names.last.is_a?(Hash) ? column_names.pop : {}
23
- options = {:type => :pgp}.merge(options)
24
-
25
- column_names.map(&:to_s).each do |column_name|
26
- encrypted_column_name = "#{column_name}_encrypted"
27
- unless columns_hash[encrypted_column_name]
28
- puts "WARNING: You defined #{column_name} as an encrypted column, but you don't have a corresponding #{encrypted_column_name} column in your database!"
29
- end
25
+ has_many :pgcrypto_columns, :as => :owner, :autosave => true, :class_name => 'PGCrypto::Column', :dependent => :delete_all
30
26
 
27
+ pgcrypto_column_names.map(&:to_s).each do |column_name|
31
28
  # Stash the encryption type in our module so various monkeypatches can access it later!
32
- PGCrypto[table_name][encrypted_column_name] = options.symbolize_keys!
29
+ PGCrypto[table_name][column_name] = options.symbolize_keys
33
30
 
34
31
  # Add attribute readers/writers to keep this baby as fluid and clean as possible.
35
- class_eval <<-encrypted_attribute_writer
32
+ start_line = __LINE__; pgcrypto_methods = <<-PGCRYPTO_METHODS
36
33
  def #{column_name}
37
- @attributes["#{column_name}"]
34
+ return @_pgcrypto_#{column_name}.try(:value) if defined?(@_pgcrypto_#{column_name})
35
+ @_pgcrypto_#{column_name} ||= select_pgcrypto_column(:#{column_name})
36
+ @_pgcrypto_#{column_name}.try(:value)
38
37
  end
39
38
 
40
- # We write the attribute twice - once as the alias so the accessor keeps working, and once
41
- # so the actual attribute value is dirty and will be queued up for assignment
39
+ # We write the attribute directly to its child value. Neato!
42
40
  def #{column_name}=(value)
43
- @attributes["#{column_name}"] = value
44
- write_attribute(:#{encrypted_column_name}, value)
41
+ if value.nil?
42
+ pgcrypto_columns.where(:name => "#{column_name}").mark_for_destruction
43
+ remove_instance_variable("@_pgcrypto_#{column_name}") if defined?(@_pgcrypto_#{column_name})
44
+ else
45
+ @_pgcrypto_#{column_name} ||= pgcrypto_columns.select{|column| column.name == "#{column_name}"}.first || pgcrypto_columns.new(:name => "#{column_name}")
46
+ @_pgcrypto_#{column_name}.value = value
47
+ end
45
48
  end
46
- encrypted_attribute_writer
47
- # Did you notice how I was all, "clean as possible" before I fucked w/AR's internal
48
- # instance variables rather than use the API? *Hilarious.*
49
+ PGCRYPTO_METHODS
50
+
51
+ class_eval pgcrypto_methods, __FILE__, start_line
52
+ end
53
+
54
+ # If any columns are set to be included in the parent record's finder,
55
+ # we'll go ahead and add 'em!
56
+ if PGCrypto[table_name].any?{|column, options| options[:include] }
57
+ default_scope includes(:pgcrypto_columns)
58
+ end
59
+ end
60
+ end
61
+
62
+ module InstanceMethods
63
+ def select_pgcrypto_column(column_name)
64
+ # Now here's the fun part. We want the selector on PGCrypto columns to do the decryption
65
+ # for us, so we have override the SELECT and add a JOIN to build out the decrypted value
66
+ # whenever it's requested.
67
+ options = PGCrypto[self.class.table_name][column_name]
68
+ pgcrypto_column_finder = pgcrypto_columns
69
+ if key = PGCrypto.keys[options[:private_key] || :private]
70
+ pgcrypto_column_finder = pgcrypto_column_finder.select([
71
+ '"pgcrypto_columns"."id"',
72
+ %[pgp_pub_decrypt("pgcrypto_columns"."value", pgcrypto_keys.#{key.name}_key#{", '#{key.password}'" if key.password}) AS "value"]
73
+ ]).joins(%[CROSS JOIN (SELECT #{key.dearmored} AS "#{key.name}_key") AS pgcrypto_keys])
74
+ end
75
+ pgcrypto_column_finder.where(:name => column_name).first
76
+ rescue ActiveRecord::StatementInvalid => e
77
+ case e.message
78
+ when /^PGError: ERROR: Wrong key or corrupt data/
79
+ # If a column has been corrupted, we'll return nil and let the DBA
80
+ # figure out WTF the is going on
81
+ logger.error(e.message.split("\n").first)
82
+ nil
83
+ else
84
+ raise e
49
85
  end
50
86
  end
51
87
  end
52
88
  end
53
89
 
54
90
  PGCrypto.keys[:public] = {:path => '.pgcrypto'} if File.file?('.pgcrypto')
55
-
56
- ActiveRecord::Base.extend PGCrypto::ClassMethods if defined? ActiveRecord::Base
91
+ if defined? ActiveRecord::Base
92
+ ActiveRecord::Base.extend PGCrypto::ClassMethods
93
+ ActiveRecord::Base.send :include, PGCrypto::InstanceMethods
94
+ end
@@ -1,26 +1,30 @@
1
1
  require 'active_record/connection_adapters/postgresql_adapter'
2
2
 
3
3
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
4
- alias :original_to_sql :to_sql
4
+ unless instance_methods.include?(:to_sql_without_pgcrypto) || instance_methods.include?('to_sql_without_pgcrypto')
5
+ alias :to_sql_without_pgcrypto :to_sql
6
+ end
7
+
5
8
  def to_sql(arel, *args)
6
9
  case arel
7
10
  when Arel::InsertManager
8
- pgcrypto_tweak_insert(arel)
9
- when Arel::SelectManager
10
- pgcrypto_tweak_select(arel)
11
+ pgcrypto_tweak_insert(arel, *args)
11
12
  when Arel::UpdateManager
12
13
  pgcrypto_tweak_update(arel)
13
14
  end
14
- original_to_sql(arel, *args)
15
+ to_sql_without_pgcrypto(arel, *args)
15
16
  end
16
17
 
17
18
  private
18
- def pgcrypto_tweak_insert(arel)
19
- table = arel.ast.relation.name
20
- unless PGCrypto[table].empty?
19
+ def pgcrypto_tweak_insert(arel, *args)
20
+ if arel.ast.relation.name == PGCrypto::Column.table_name && (binds = args.last).is_a?(Array)
21
21
  arel.ast.columns.each_with_index do |column, i|
22
- if options = PGCrypto[table][column.name]
23
- if key = PGCrypto.keys[options[:public_key] || :public]
22
+ if column.name == 'value'
23
+ model_column, model_class_name = binds.select {|column, value| column.name == 'owner_type' }.first
24
+ model_class = Object.const_get(model_class_name)
25
+ column_column, model_column_name = binds.select {|column, value| column.name == 'name' }.first
26
+ options = PGCrypto[model_class.table_name][model_column_name]
27
+ if options && key = PGCrypto.keys[options[:public_key] || :public]
24
28
  value = arel.ast.values.expressions[i]
25
29
  quoted_value = quote_string(value)
26
30
  encryption_instruction = %[pgp_pub_encrypt(#{quoted_value}, #{key.dearmored})]
@@ -31,73 +35,18 @@ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
31
35
  end
32
36
  end
33
37
 
34
- def pgcrypto_tweak_select(arel)
35
- # We start by looping through each "core," which is just
36
- # a SelectStatement, and tweaking both *what* it's selecting
37
- # and correcting plain-text queries against an encrypted
38
- # column...
39
- joins = {}
40
- arel.ast.cores.each do |core|
41
- # Now we loop through each SelectStatement's actual selction -
42
- # typically it's just '*'; and, in fact, that one of the only
43
- # things we care about!
44
- new_projections = []
45
- core.projections.each do |projection|
46
- next unless projection.respond_to?(:relation)
47
- # The one other situation we might care about is if the projection is
48
- # selecting a specifically encrypted column, in which case, we want to
49
- # _wrap_ it. See how that's different?
50
- if !PGCrypto[projection.relation.name].empty?
51
- # Okay, so first, check if it's a broad select
52
- if projection.name == '*'
53
- # In this case, we want to just grab all the keys from columns in this table
54
- # and select them fancy-like
55
- PGCrypto[projection.relation.name].each do |column, options|
56
- new_projections.push(pgcrypto_tweak_select_column(column, options, joins))
57
- end
58
- elsif options = PGCrypto[projection.relation.name][projection.name]
59
- # And in this case, we're just selecting a single column!
60
- new_projections.push(pgcrypto_tweak_select_column(projection.name, options, joins))
61
- end
62
- end
63
- end
64
- core.projections.push(*new_projections.compact)
65
-
66
- # Dios mio! What an operation! Now we'll do something similar for the WHERE statements
67
- core.wheres.each do |where|
68
- # Now loop through the children to encrypt them for the SELECT
69
- where.children.each do |child|
70
- if options = PGCrypto[child.left.relation.name]["#{child.left.name}_encrypted"]
71
- if key = PGCrypto.keys[options[:private_key] || :private]
72
- joins[key.name] ||= "#{key.dearmored} AS #{key.name}_key"
73
- child.left = Arel::Nodes::SqlLiteral.new("pgp_pub_decrypt(#{child.left.name}_encrypted, keys.#{key.name}_key)")
74
- end
75
- end
76
- end if where.respond_to?(:children)
77
- end
78
- end
79
- unless joins.empty?
80
- arel.join(Arel::Nodes::SqlLiteral.new("CROSS JOIN (SELECT #{joins.values.join(', ')}) AS keys"))
81
- end
82
- end
83
-
84
- def pgcrypto_tweak_select_column(column, options, joins)
85
- return nil unless options[:type] == :pgp
86
- if key = PGCrypto.keys[options[:private_key] || :private]
87
- select = %[pgp_pub_decrypt(#{column}, keys.#{key.name}_key#{", '#{key.password}'" if key.password}) AS "#{column.to_s.gsub(/_encrypted$/, '')}"]
88
- joins[key.name] ||= "#{key.dearmored} AS #{key.name}_key"
89
- Arel::Nodes::SqlLiteral.new(select)
90
- end
91
- end
92
-
93
38
  def pgcrypto_tweak_update(arel)
94
- # Loop through the assignments and make sure we take care of that whole
95
- # NULL value thing!
96
- arel.ast.values.each do |value|
97
- if value.respond_to?(:left) && options = PGCrypto[value.left.relation.name][value.left.name]
98
- if value.right.nil?
99
- value.right = Arel::Nodes::SqlLiteral.new('NULL')
100
- elsif key = PGCrypto.keys[options[:public_key] || :public]
39
+ if arel.ast.relation.name == PGCrypto::Column.table_name
40
+ # Loop through the assignments and make sure we take care of that whole
41
+ # NULL value thing!
42
+ value = arel.ast.values.select{|value| value.respond_to?(:left) && value.left.name == 'value' }.first
43
+ id = arel.ast.wheres.map { |where| where.children.select { |child| child.left.name == 'id' }.first }.first.right
44
+ if value.right.nil?
45
+ value.right = Arel::Nodes::SqlLiteral.new('NULL')
46
+ else column = PGCrypto::Column.select([:id, :owner_id, :owner_type, :name]).find(id)
47
+ model_class = Object.const_get(column.owner_type)
48
+ options = PGCrypto[model_class.table_name][column.name]
49
+ if key = PGCrypto.keys[options[:public_key] || :public]
101
50
  quoted_right = quote_string(value.right)
102
51
  encryption_instruction = %[pgp_pub_encrypt('#{quoted_right}', #{key.dearmored})]
103
52
  value.right = Arel::Nodes::SqlLiteral.new(encryption_instruction)
data/lib/pgcrypto/arel.rb CHANGED
@@ -8,15 +8,17 @@ require 'arel/visitors/postgresql'
8
8
  # more specific bits here!
9
9
 
10
10
  Arel::Visitors::PostgreSQL.class_eval do
11
- alias :original_visit_Arel_Nodes_Assignment :visit_Arel_Nodes_Assignment
11
+ unless instance_methods.include?(:visit_Arel_Nodes_Assignment_without_pgcrypto) || instance_methods.include?('visit_Arel_Nodes_Assignment_without_pgcrypto')
12
+ alias :visit_Arel_Nodes_Assignment_without_pgcrypto :visit_Arel_Nodes_Assignment
13
+ end
14
+
12
15
  def visit_Arel_Nodes_Assignment(assignment)
13
16
  # Hijack the normally inoccuous assignment that happens, seeing as how
14
17
  # Arel normally forwards this shit to someone else and I hate it.
15
- if PGCrypto[assignment.left.relation.name][assignment.left.name]
16
- # raise "#{visit(assignment.left)} = #{visit(assignment.right)}"
18
+ if assignment.left.relation.name == PGCrypto::Column.table_name && assignment.left.name == 'value'
17
19
  "#{visit(assignment.left)} = #{visit(assignment.right)}"
18
20
  else
19
- original_visit_Arel_Nodes_Assignment(assignment)
21
+ visit_Arel_Nodes_Assignment_without_pgcrypto(assignment)
20
22
  end
21
23
  end
22
24
  end
@@ -0,0 +1,6 @@
1
+ module PGCrypto
2
+ class Column < ActiveRecord::Base
3
+ self.table_name = 'pgcrypto_columns'
4
+ belongs_to :owner, :autosave => false, :inverse_of => :pgcrypto_columns, :polymorphic => true
5
+ end
6
+ end
data/pgcrypto.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "pgcrypto"
8
- s.version = "0.0.4"
8
+ s.version = "0.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Flip Sasser"]
12
- s.date = "2012-03-02"
12
+ s.date = "2012-03-28"
13
13
  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 "
14
14
  s.email = "flip@x451.com"
15
15
  s.extra_rdoc_files = [
@@ -19,13 +19,19 @@ Gem::Specification.new do |s|
19
19
  s.files = [
20
20
  ".rspec",
21
21
  "CHANGES",
22
+ "Gemfile",
22
23
  "LICENSE",
23
24
  "README.markdown",
24
25
  "Rakefile",
25
26
  "VERSION",
27
+ "lib/generators/pgcrypto/install/USAGE",
28
+ "lib/generators/pgcrypto/install/install_generator.rb",
29
+ "lib/generators/pgcrypto/install/templates/initializer.rb",
30
+ "lib/generators/pgcrypto/install/templates/migration.rb",
26
31
  "lib/pgcrypto.rb",
27
32
  "lib/pgcrypto/active_record.rb",
28
33
  "lib/pgcrypto/arel.rb",
34
+ "lib/pgcrypto/column.rb",
29
35
  "lib/pgcrypto/key.rb",
30
36
  "lib/pgcrypto/table_manager.rb",
31
37
  "pgcrypto.gemspec"
@@ -39,9 +45,12 @@ Gem::Specification.new do |s|
39
45
  s.specification_version = 3
40
46
 
41
47
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
48
+ s.add_runtime_dependency(%q<activerecord>, [">= 3.2"])
42
49
  else
50
+ s.add_dependency(%q<activerecord>, [">= 3.2"])
43
51
  end
44
52
  else
53
+ s.add_dependency(%q<activerecord>, [">= 3.2"])
45
54
  end
46
55
  end
47
56
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgcrypto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,19 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-02 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2012-03-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &70130059262260 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70130059262260
14
25
  description: ! "\n PGCrypto is an ActiveRecord::Base extension that allows you
15
26
  to asymmetrically\n encrypt PostgreSQL columns with as little trouble as possible.
16
27
  It's totally\n freaking rad.\n "
@@ -23,13 +34,19 @@ extra_rdoc_files:
23
34
  files:
24
35
  - .rspec
25
36
  - CHANGES
37
+ - Gemfile
26
38
  - LICENSE
27
39
  - README.markdown
28
40
  - Rakefile
29
41
  - VERSION
42
+ - lib/generators/pgcrypto/install/USAGE
43
+ - lib/generators/pgcrypto/install/install_generator.rb
44
+ - lib/generators/pgcrypto/install/templates/initializer.rb
45
+ - lib/generators/pgcrypto/install/templates/migration.rb
30
46
  - lib/pgcrypto.rb
31
47
  - lib/pgcrypto/active_record.rb
32
48
  - lib/pgcrypto/arel.rb
49
+ - lib/pgcrypto/column.rb
33
50
  - lib/pgcrypto/key.rb
34
51
  - lib/pgcrypto/table_manager.rb
35
52
  - pgcrypto.gemspec