pgcrypto 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,15 @@
1
+ 0.2.0
2
+ * Overhauled key system. Unfortunately, for performance reasons
3
+ and due to the insanely hacked nature of PGCrypto, multiple keys
4
+ are NO LONGER SUPPORTED. I'm working to bring them back, but
5
+ this was the only solution to get fully performant and functional
6
+ without any disasters.
7
+
8
+ 0.1.2
9
+ * Added automatic installation of the pgcrypto extension if'n it
10
+ doesn't already exist. Helpful, but doesn't fully make the
11
+ `rake db:test:prepare` cut yet. Still working on that bit...
12
+
1
13
  0.1.1
2
14
  * Rebuilt the WHERE clause stuff to make sure finders AND setters
3
15
  both worked. It's fragile and hackish at this time, but we'll get
data/README.markdown CHANGED
@@ -7,13 +7,15 @@ so I make no promises as to its efficacy in the real world beyond my tiny, Rails
7
7
  Installation
8
8
  -
9
9
 
10
- You need to have PGCrypto installed before this guy will work. [LMGTFY](http://lmgtfy.com/?q=how+to+install+pgcrypto).
10
+ PGCrypto will load the `pgcrypto` extension into your database if you haven't already, but this change will NOT get propagated
11
+ to your schema.rb file, so... go figure. You'll have to `CREATE EXTENSION IF NOT EXISTS pgcrypto` any database built from the
12
+ schema file (**HINT** that means your test databases). Anyway, do the following.
11
13
 
12
- 1. Add this to your Gemfile:
14
+ 1. Add pgcrypto to your Gemfile:
13
15
 
14
16
  gem "pgcrypto"
15
17
 
16
- 2. Do this now:
18
+ 2. Then bundle it:
17
19
 
18
20
  bundle
19
21
 
@@ -22,7 +24,7 @@ You need to have PGCrypto installed before this guy will work. [LMGTFY](http://l
22
24
  rails g pgcrypto:install
23
25
  rake db:migrate
24
26
 
25
- 4. Edit the initializer in `config/initializers/pgcrypto.rb` to point out your public and private GPG keys:
27
+ 4. Edit the initializer in `config/initializers/pgcrypto.rb` to point to your public and private GPG keys:
26
28
 
27
29
  PGCrypto.keys[:private] = {:path => "~/.keys/private.key"}
28
30
  PGCrypto.keys[:public] = {:path => "~/.keys/public.key"}
@@ -48,7 +50,7 @@ a GPG-encrypted column that can only be decrypted with your secure key.
48
50
  Keys
49
51
  -
50
52
 
51
- If you want to bundle your public key with your application, PGCrypto will automatically load `#{Rails.root}/.pgcrypto`,
53
+ If you want to bundle your public key with your application, PGCrypto will automatically load `RAILS_ROOT/.pgcrypto`,
52
54
  so feel free to put your public key in there. You can also tell PGCrypto about your keys in a number of fun ways.
53
55
  The most straightforward is to assign the actual content of the key manually:
54
56
 
@@ -60,23 +62,12 @@ You can also give it more specific stuff:
60
62
 
61
63
  This is especially important if you password protect your private key files (and you SHOULD, for the record)!
62
64
 
63
- You can also specify different keys for different purposes:
64
-
65
- PGCrypto.keys[:user_public] = {:path => '.user_public.key'}
66
- PGCrypto.keys[:user_private] = {:path => '.user_private.key'}
67
-
68
- If you do that, just tell PGCrypto which keys to use on which columns, using an optional hash on the end of the `pgcrypto` call:
69
-
70
- class User < ActiveRecord::Base
71
- pgcrypto :social_security_number, :private_key => :user_private, :public_key => :user_public
72
- end
73
-
74
- I recommend deploy-time passing of your private key and password, to ensure it
75
- doesn't wind up in any long-term storage on the server:
65
+ I recommend deploy-time passing of your private key and password, to ensure it doesn't wind up in any long-term
66
+ storage on your server, since if you're using this library you presumably care a little bit about security:
76
67
 
77
68
  PGCrypto.keys[:private] = {:value => ENV['PRIVATE_KEY'], :password => ENV['PRIVATE_KEY_PASSWORD']}
78
69
 
79
- Warranty
70
+ Warranty (or lack thereof)
80
71
  -
81
72
 
82
73
  As I mentioned before, this library is one HUGE hack. This is just scratching the surface of keeping your data secure.
@@ -90,10 +81,4 @@ it; the rest is up to you.
90
81
  **As such,** the author and Delightful Widgets Inc. offer ***ABSOLUTELY NO GODDAMN WARRANTY***. As I mentioned, this works great in our
91
82
  Rails 3.2 world, but YMMV if your version of Arel or ActiveRecord are ahead or behind ours. Sorry, folks.
92
83
 
93
- WTF NO TESTS?!!
94
- -
95
-
96
- Nope. We built this inside of a production application, and used its test suite to debug everything. Since this is really just
97
- a preview release, I haven't written a suite for it yet. Sorry.
98
-
99
84
  Copyright (C) 2012 Delightful Widgets, Inc. Built by Flip Sasser, Monkeypatcher Extraordinaire!
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -3,7 +3,3 @@
3
3
 
4
4
  # You can also specify the file contents directly:
5
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'}
@@ -6,7 +6,9 @@ class InstallPgcrypto < ActiveRecord::Migration
6
6
  t.string :name, :limit => 32
7
7
  t.binary :value
8
8
  end
9
- add_index :pgcrypto_columns, [:owner_id, :owner_type, :name], :name => :pgcrypto_column_finder
9
+ add_index :pgcrypto_columns, [:owner_id, :owner_type, :name], :name => :pgcrypto_type_finder
10
+ add_index :pgcrypto_columns, [:owner_id, :owner_table, :name], :name => :pgcrypto_table_finder
11
+ execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
10
12
  end
11
13
 
12
14
  def down
@@ -8,7 +8,7 @@ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
8
8
  def to_sql(arel, *args)
9
9
  case arel
10
10
  when Arel::InsertManager
11
- pgcrypto_tweak_insert(arel, *args)
11
+ pgcrypto_tweak_insert(arel)
12
12
  when Arel::SelectManager
13
13
  pgcrypto_tweak_select(arel)
14
14
  when Arel::UpdateManager
@@ -18,83 +18,71 @@ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
18
18
  end
19
19
 
20
20
  private
21
- def pgcrypto_tweak_insert(arel, *args)
22
- if arel.ast.relation.name == PGCrypto::Column.table_name && (binds = args.last).is_a?(Array)
21
+ def pgcrypto_tweak_insert(arel)
22
+ if arel.ast.relation.name.to_s == PGCrypto::Column.table_name.to_s
23
+ return unless key = PGCrypto.keys[:public]
23
24
  arel.ast.columns.each_with_index do |column, i|
24
25
  if column.name == 'value'
25
- model_column, model_class_name = binds.select {|column, value| column.name == 'owner_type' }.first
26
- model_class = Object.const_get(model_class_name)
27
- column_column, model_column_name = binds.select {|column, value| column.name == 'name' }.first
28
- options = PGCrypto[model_class.table_name][model_column_name]
29
- if options && key = PGCrypto.keys[options[:public_key] || :public]
30
- value = arel.ast.values.expressions[i]
31
- quoted_value = quote_string(value)
32
- encryption_instruction = %[pgp_pub_encrypt(#{quoted_value}, #{key.dearmored})]
33
- arel.ast.values.expressions[i] = Arel::Nodes::SqlLiteral.new(encryption_instruction)
34
- end
26
+ value = arel.ast.values.expressions[i]
27
+ quoted_value = quote_string(value)
28
+ encryption_instruction = %[pgp_pub_encrypt(#{quoted_value}, #{key.dearmored})]
29
+ arel.ast.values.expressions[i] = Arel::Nodes::SqlLiteral.new(encryption_instruction)
35
30
  end
36
31
  end
37
32
  end
38
33
  end
39
34
 
40
35
  def pgcrypto_tweak_select(arel)
36
+ return unless key = PGCrypto.keys[:private]
41
37
  # We start by looping through each "core," which is just
42
38
  # a SelectStatement and correcting plain-text queries
43
39
  # against an encrypted column...
44
- joins = {}
40
+ joins = []
45
41
  table_name = nil
46
42
  arel.ast.cores.each do |core|
47
43
  # Yeah, I'm lazy. Whatevs.
48
- next unless core.is_a?(Arel::Nodes::SelectCore) && !(columns = PGCrypto[table_name = core.source.left.name]).empty?
44
+ next unless core.is_a?(Arel::Nodes::SelectCore)
45
+
46
+ encrypted_columns = PGCrypto[table_name = core.source.left.name]
47
+ next if encrypted_columns.empty?
49
48
 
50
49
  # We loop through each WHERE specification to determine whether or not the
51
50
  # PGCrypto column should be JOIN'd upon; in which case, we, like, do it.
52
51
  core.wheres.each do |where|
53
52
  # Now loop through the children to encrypt them for the SELECT
54
53
  where.children.each do |child|
55
- if options = columns[child.left.name]
56
- if key = PGCrypto.keys[options[:private_key] || :private]
57
- join_name = "pgcrypto_column_#{child.left.name}"
58
- joins[join_name] ||= {:column => child.left.name, :key => "#{key.dearmored} AS #{key.name}_key"}
59
- child.left = Arel::Nodes::SqlLiteral.new(%[pgp_pub_decrypt("#{join_name}"."value", keys.#{key.name}_key)])
60
- end
61
- end
54
+ next unless encrypted_columns[child.left.name.to_s]
55
+ joins.push(child.left.name.to_s) unless joins.include?(child.left.name.to_s)
56
+ child.left = Arel::Nodes::SqlLiteral.new(%[
57
+ pgp_pub_decrypt("#{PGCrypto::Column.table_name}_#{child.left.name}"."value", pgcrypto_keys.#{key.name})
58
+ ])
62
59
  end if where.respond_to?(:children)
63
60
  end
64
61
  end
65
- unless joins.empty?
66
- key_joins = []
67
- joins.each do |key_name, join|
68
- key_joins.push(join[:key])
69
- column = quote_string(join[:column].to_s)
62
+ if joins.any?
63
+ arel.join(Arel::Nodes::SqlLiteral.new("CROSS JOIN (SELECT #{key.dearmored} AS #{key.name}) AS pgcrypto_keys"))
64
+ joins.each do |column|
65
+ column = quote_string(column)
66
+ as_table = "#{PGCrypto::Column.table_name}_#{column}"
70
67
  arel.join(Arel::Nodes::SqlLiteral.new(%[
71
- JOIN "pgcrypto_columns" AS "pgcrypto_column_#{column}" ON
72
- "pgcrypto_column_#{column}"."owner_id" = "#{table_name}"."id"
73
- AND "pgcrypto_column_#{column}"."owner_table" = '#{quote_string(table_name)}'
74
- AND "pgcrypto_column_#{column}"."name" = '#{column}'
68
+ JOIN "#{PGCrypto::Column.table_name}" AS "#{as_table}" ON "#{as_table}"."owner_id" = "#{table_name}"."id" AND "#{as_table}"."owner_table" = '#{quote_string(table_name)}' AND "#{as_table}"."name" = '#{column}'
75
69
  ]))
76
70
  end
77
- arel.join(Arel::Nodes::SqlLiteral.new("CROSS JOIN (SELECT #{key_joins.join(', ')}) AS keys"))
78
71
  end
79
72
  end
80
73
 
81
74
  def pgcrypto_tweak_update(arel)
82
- if arel.ast.relation.name == PGCrypto::Column.table_name
75
+ if arel.ast.relation.name.to_s == PGCrypto::Column.table_name.to_s
83
76
  # Loop through the assignments and make sure we take care of that whole
84
77
  # NULL value thing!
85
78
  value = arel.ast.values.select{|value| value.respond_to?(:left) && value.left.name == 'value' }.first
86
- id = arel.ast.wheres.map { |where| where.children.select { |child| child.left.name == 'id' }.first }.first.right
87
79
  if value.right.nil?
88
80
  value.right = Arel::Nodes::SqlLiteral.new('NULL')
89
- else column = PGCrypto::Column.select([:id, :owner_id, :owner_type, :name]).find(id)
90
- model_class = Object.const_get(column.owner_type)
91
- options = PGCrypto[model_class.table_name][column.name]
92
- if key = PGCrypto.keys[options[:public_key] || :public]
93
- quoted_right = quote_string(value.right)
94
- encryption_instruction = %[pgp_pub_encrypt('#{quoted_right}', #{key.dearmored})]
95
- value.right = Arel::Nodes::SqlLiteral.new(encryption_instruction)
96
- end
81
+ elsif key = PGCrypto.keys[:public]
82
+ quoted_right = quote_string(value.right)
83
+ encryption_instruction = %[pgp_pub_encrypt('#{quoted_right}', #{key.dearmored})]
84
+ value.right = Arel::Nodes::SqlLiteral.new(encryption_instruction)
97
85
  end
98
86
  end
99
87
  end
100
- end
88
+ end
data/lib/pgcrypto.rb CHANGED
@@ -39,7 +39,7 @@ module PGCrypto
39
39
  # We write the attribute directly to its child value. Neato!
40
40
  def #{column_name}=(value)
41
41
  if value.nil?
42
- pgcrypto_columns.where(:name => "#{column_name}").mark_for_destruction
42
+ pgcrypto_columns.select{|column| column.name == "#{column_name}"}.each(&:mark_for_destruction)
43
43
  remove_instance_variable("@_pgcrypto_#{column_name}") if defined?(@_pgcrypto_#{column_name})
44
44
  else
45
45
  @_pgcrypto_#{column_name} ||= pgcrypto_columns.select{|column| column.name == "#{column_name}"}.first || pgcrypto_columns.new(:name => "#{column_name}")
@@ -66,11 +66,11 @@ module PGCrypto
66
66
  # whenever it's requested.
67
67
  options = PGCrypto[self.class.table_name][column_name]
68
68
  pgcrypto_column_finder = pgcrypto_columns
69
- if key = PGCrypto.keys[options[:private_key] || :private]
69
+ if key = PGCrypto.keys[:private]
70
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])
71
+ %("#{PGCrypto::Column.table_name}"."id"),
72
+ %[pgp_pub_decrypt("#{PGCrypto::Column.table_name}"."value", pgcrypto_keys.#{key.name}#{", '#{key.password}'" if key.password}) AS "value"]
73
+ ]).joins(%[CROSS JOIN (SELECT #{key.dearmored} AS "#{key.name}") AS pgcrypto_keys])
74
74
  end
75
75
  pgcrypto_column_finder.where(:name => column_name).first
76
76
  rescue ActiveRecord::StatementInvalid => e
data/pgcrypto.gemspec CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "pgcrypto"
8
- s.version = "0.1.1"
8
+ s.version = "0.2.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"]
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.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2012-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
16
- requirement: &70275631246800 !ruby/object:Gem::Requirement
16
+ requirement: &70147572219000 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: '3.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70275631246800
24
+ version_requirements: *70147572219000
25
25
  description: ! "\n PGCrypto is an ActiveRecord::Base extension that allows you
26
26
  to asymmetrically\n encrypt PostgreSQL columns with as little trouble as possible.
27
27
  It's totally\n freaking rad.\n "