pgcrypto 0.1.1 → 0.2.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,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 "