pgcrypto 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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