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 +12 -0
- data/README.markdown +10 -25
- data/VERSION +1 -1
- data/lib/generators/pgcrypto/install/templates/initializer.rb +0 -4
- data/lib/generators/pgcrypto/install/templates/migration.rb +3 -1
- data/lib/pgcrypto/active_record.rb +31 -43
- data/lib/pgcrypto.rb +5 -5
- data/pgcrypto.gemspec +1 -1
- metadata +3 -3
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
|
-
|
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
|
14
|
+
1. Add pgcrypto to your Gemfile:
|
13
15
|
|
14
16
|
gem "pgcrypto"
|
15
17
|
|
16
|
-
2.
|
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
|
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
|
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
|
-
|
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
|
+
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 => :
|
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
|
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
|
22
|
-
if arel.ast.relation.name == PGCrypto::Column.table_name
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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)
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
66
|
-
|
67
|
-
joins.each do |
|
68
|
-
|
69
|
-
|
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 "
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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.
|
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[
|
69
|
+
if key = PGCrypto.keys[:private]
|
70
70
|
pgcrypto_column_finder = pgcrypto_column_finder.select([
|
71
|
-
|
72
|
-
%[pgp_pub_decrypt("
|
73
|
-
]).joins(%[CROSS JOIN (SELECT #{key.dearmored} AS "#{key.name}
|
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
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.
|
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: &
|
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: *
|
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 "
|