Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Updated keys to support functionality as expected. This is most stabl…

…e release yet.
  • Loading branch information...
commit 080d335a61f77fdd688404145a421c73b562d773 1 parent 0a7c62d
Flip Sasser authored
View
12 CHANGES
@@ -1,3 +1,15 @@
+0.2.0
+ * Overhauled key system. Unfortunately, for performance reasons
+ and due to the insanely hacked nature of PGCrypto, multiple keys
+ are NO LONGER SUPPORTED. I'm working to bring them back, but
+ this was the only solution to get fully performant and functional
+ without any disasters.
+
+0.1.2
+ * Added automatic installation of the pgcrypto extension if'n it
+ doesn't already exist. Helpful, but doesn't fully make the
+ `rake db:test:prepare` cut yet. Still working on that bit...
+
0.1.1
* Rebuilt the WHERE clause stuff to make sure finders AND setters
both worked. It's fragile and hackish at this time, but we'll get
View
27 README.markdown
@@ -7,13 +7,15 @@ so I make no promises as to its efficacy in the real world beyond my tiny, Rails
Installation
-
-You need to have PGCrypto installed before this guy will work. [LMGTFY](http://lmgtfy.com/?q=how+to+install+pgcrypto).
+PGCrypto will load the `pgcrypto` extension into your database if you haven't already, but this change will NOT get propagated
+to your schema.rb file, so... go figure. You'll have to `CREATE EXTENSION IF NOT EXISTS pgcrypto` any database built from the
+schema file (**HINT** that means your test databases). Anyway, do the following.
-1. Add this to your Gemfile:
+1. Add pgcrypto to your Gemfile:
gem "pgcrypto"
-2. Do this now:
+2. Then bundle it:
bundle
@@ -22,7 +24,7 @@ You need to have PGCrypto installed before this guy will work. [LMGTFY](http://l
rails g pgcrypto:install
rake db:migrate
-4. Edit the initializer in `config/initializers/pgcrypto.rb` to point out your public and private GPG keys:
+4. Edit the initializer in `config/initializers/pgcrypto.rb` to point to your public and private GPG keys:
PGCrypto.keys[:private] = {:path => "~/.keys/private.key"}
PGCrypto.keys[:public] = {:path => "~/.keys/public.key"}
@@ -48,7 +50,7 @@ a GPG-encrypted column that can only be decrypted with your secure key.
Keys
-
-If you want to bundle your public key with your application, PGCrypto will automatically load `#{Rails.root}/.pgcrypto`,
+If you want to bundle your public key with your application, PGCrypto will automatically load `RAILS_ROOT/.pgcrypto`,
so feel free to put your public key in there. You can also tell PGCrypto about your keys in a number of fun ways.
The most straightforward is to assign the actual content of the key manually:
@@ -60,19 +62,8 @@ You can also give it more specific stuff:
This is especially important if you password protect your private key files (and you SHOULD, for the record)!
-You can also specify different keys for different purposes:
-
- PGCrypto.keys[:user_public] = {:path => '.user_public.key'}
- PGCrypto.keys[:user_private] = {:path => '.user_private.key'}
-
-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:
-
- class User < ActiveRecord::Base
- pgcrypto :social_security_number, :private_key => :user_private, :public_key => :user_public
- end
-
-I recommend deploy-time passing of your private key and password, to ensure it
-doesn't wind up in any long-term storage on the server:
+I recommend deploy-time passing of your private key and password, to ensure it doesn't wind up in any long-term
+storage on your server, since if you're using this library you presumably care a little bit about security:
PGCrypto.keys[:private] = {:value => ENV['PRIVATE_KEY'], :password => ENV['PRIVATE_KEY_PASSWORD']}
View
4 lib/generators/pgcrypto/install/templates/initializer.rb
@@ -3,7 +3,3 @@
# You can also specify the file contents directly:
# PGCrypto.keys[:private] = ENV['PRIVATE_KEY']
-
-# You can also specify custom keys:
-# PGCrypto.keys[:message_columns] = {:path => 'path/to/message_columns/keyfile'}
-# PGCrypto.keys[:user_columns] = {:path => 'path/to/user_columns/keyfile'}
View
4 lib/generators/pgcrypto/install/templates/migration.rb
@@ -6,7 +6,9 @@ def up
t.string :name, :limit => 32
t.binary :value
end
- add_index :pgcrypto_columns, [:owner_id, :owner_type, :name], :name => :pgcrypto_column_finder
+ add_index :pgcrypto_columns, [:owner_id, :owner_type, :name], :name => :pgcrypto_type_finder
+ add_index :pgcrypto_columns, [:owner_id, :owner_table, :name], :name => :pgcrypto_table_finder
+ execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
end
def down
View
10 lib/pgcrypto.rb
@@ -39,7 +39,7 @@ def #{column_name}
# We write the attribute directly to its child value. Neato!
def #{column_name}=(value)
if value.nil?
- pgcrypto_columns.where(:name => "#{column_name}").mark_for_destruction
+ pgcrypto_columns.select{|column| column.name == "#{column_name}"}.each(&:mark_for_destruction)
remove_instance_variable("@_pgcrypto_#{column_name}") if defined?(@_pgcrypto_#{column_name})
else
@_pgcrypto_#{column_name} ||= pgcrypto_columns.select{|column| column.name == "#{column_name}"}.first || pgcrypto_columns.new(:name => "#{column_name}")
@@ -66,11 +66,11 @@ def select_pgcrypto_column(column_name)
# whenever it's requested.
options = PGCrypto[self.class.table_name][column_name]
pgcrypto_column_finder = pgcrypto_columns
- if key = PGCrypto.keys[options[:private_key] || :private]
+ if key = PGCrypto.keys[:private]
pgcrypto_column_finder = pgcrypto_column_finder.select([
- '"pgcrypto_columns"."id"',
- %[pgp_pub_decrypt("pgcrypto_columns"."value", pgcrypto_keys.#{key.name}_key#{", '#{key.password}'" if key.password}) AS "value"]
- ]).joins(%[CROSS JOIN (SELECT #{key.dearmored} AS "#{key.name}_key") AS pgcrypto_keys])
+ %("#{PGCrypto::Column.table_name}"."id"),
+ %[pgp_pub_decrypt("#{PGCrypto::Column.table_name}"."value", pgcrypto_keys.#{key.name}#{", '#{key.password}'" if key.password}) AS "value"]
+ ]).joins(%[CROSS JOIN (SELECT #{key.dearmored} AS "#{key.name}") AS pgcrypto_keys])
end
pgcrypto_column_finder.where(:name => column_name).first
rescue ActiveRecord::StatementInvalid => e
View
74 lib/pgcrypto/active_record.rb
@@ -8,7 +8,7 @@
def to_sql(arel, *args)
case arel
when Arel::InsertManager
- pgcrypto_tweak_insert(arel, *args)
+ pgcrypto_tweak_insert(arel)
when Arel::SelectManager
pgcrypto_tweak_select(arel)
when Arel::UpdateManager
@@ -18,83 +18,71 @@ def to_sql(arel, *args)
end
private
- def pgcrypto_tweak_insert(arel, *args)
- if arel.ast.relation.name == PGCrypto::Column.table_name && (binds = args.last).is_a?(Array)
+ def pgcrypto_tweak_insert(arel)
+ if arel.ast.relation.name.to_s == PGCrypto::Column.table_name.to_s
+ return unless key = PGCrypto.keys[:public]
arel.ast.columns.each_with_index do |column, i|
if column.name == 'value'
- model_column, model_class_name = binds.select {|column, value| column.name == 'owner_type' }.first
- model_class = Object.const_get(model_class_name)
- column_column, model_column_name = binds.select {|column, value| column.name == 'name' }.first
- options = PGCrypto[model_class.table_name][model_column_name]
- if options && key = PGCrypto.keys[options[:public_key] || :public]
- value = arel.ast.values.expressions[i]
- quoted_value = quote_string(value)
- encryption_instruction = %[pgp_pub_encrypt(#{quoted_value}, #{key.dearmored})]
- arel.ast.values.expressions[i] = Arel::Nodes::SqlLiteral.new(encryption_instruction)
- end
+ value = arel.ast.values.expressions[i]
+ quoted_value = quote_string(value)
+ encryption_instruction = %[pgp_pub_encrypt(#{quoted_value}, #{key.dearmored})]
+ arel.ast.values.expressions[i] = Arel::Nodes::SqlLiteral.new(encryption_instruction)
end
end
end
end
def pgcrypto_tweak_select(arel)
+ return unless key = PGCrypto.keys[:private]
# We start by looping through each "core," which is just
# a SelectStatement and correcting plain-text queries
# against an encrypted column...
- joins = {}
+ joins = []
table_name = nil
arel.ast.cores.each do |core|
# Yeah, I'm lazy. Whatevs.
- next unless core.is_a?(Arel::Nodes::SelectCore) && !(columns = PGCrypto[table_name = core.source.left.name]).empty?
+ next unless core.is_a?(Arel::Nodes::SelectCore)
+
+ encrypted_columns = PGCrypto[table_name = core.source.left.name]
+ next if encrypted_columns.empty?
# We loop through each WHERE specification to determine whether or not the
# PGCrypto column should be JOIN'd upon; in which case, we, like, do it.
core.wheres.each do |where|
# Now loop through the children to encrypt them for the SELECT
where.children.each do |child|
- if options = columns[child.left.name]
- if key = PGCrypto.keys[options[:private_key] || :private]
- join_name = "pgcrypto_column_#{child.left.name}"
- joins[join_name] ||= {:column => child.left.name, :key => "#{key.dearmored} AS #{key.name}_key"}
- child.left = Arel::Nodes::SqlLiteral.new(%[pgp_pub_decrypt("#{join_name}"."value", keys.#{key.name}_key)])
- end
- end
+ next unless encrypted_columns[child.left.name.to_s]
+ joins.push(child.left.name.to_s) unless joins.include?(child.left.name.to_s)
+ child.left = Arel::Nodes::SqlLiteral.new(%[
+ pgp_pub_decrypt("#{PGCrypto::Column.table_name}_#{child.left.name}"."value", pgcrypto_keys.#{key.name})
+ ])
end if where.respond_to?(:children)
end
end
- unless joins.empty?
- key_joins = []
- joins.each do |key_name, join|
- key_joins.push(join[:key])
- column = quote_string(join[:column].to_s)
+ if joins.any?
+ arel.join(Arel::Nodes::SqlLiteral.new("CROSS JOIN (SELECT #{key.dearmored} AS #{key.name}) AS pgcrypto_keys"))
+ joins.each do |column|
+ column = quote_string(column)
+ as_table = "#{PGCrypto::Column.table_name}_#{column}"
arel.join(Arel::Nodes::SqlLiteral.new(%[
- JOIN "pgcrypto_columns" AS "pgcrypto_column_#{column}" ON
- "pgcrypto_column_#{column}"."owner_id" = "#{table_name}"."id"
- AND "pgcrypto_column_#{column}"."owner_table" = '#{quote_string(table_name)}'
- AND "pgcrypto_column_#{column}"."name" = '#{column}'
+ 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}'
]))
end
- arel.join(Arel::Nodes::SqlLiteral.new("CROSS JOIN (SELECT #{key_joins.join(', ')}) AS keys"))
end
end
def pgcrypto_tweak_update(arel)
- if arel.ast.relation.name == PGCrypto::Column.table_name
+ if arel.ast.relation.name.to_s == PGCrypto::Column.table_name.to_s
# Loop through the assignments and make sure we take care of that whole
# NULL value thing!
value = arel.ast.values.select{|value| value.respond_to?(:left) && value.left.name == 'value' }.first
- id = arel.ast.wheres.map { |where| where.children.select { |child| child.left.name == 'id' }.first }.first.right
if value.right.nil?
value.right = Arel::Nodes::SqlLiteral.new('NULL')
- else column = PGCrypto::Column.select([:id, :owner_id, :owner_type, :name]).find(id)
- model_class = Object.const_get(column.owner_type)
- options = PGCrypto[model_class.table_name][column.name]
- if key = PGCrypto.keys[options[:public_key] || :public]
- quoted_right = quote_string(value.right)
- encryption_instruction = %[pgp_pub_encrypt('#{quoted_right}', #{key.dearmored})]
- value.right = Arel::Nodes::SqlLiteral.new(encryption_instruction)
- end
+ elsif key = PGCrypto.keys[:public]
+ quoted_right = quote_string(value.right)
+ encryption_instruction = %[pgp_pub_encrypt('#{quoted_right}', #{key.dearmored})]
+ value.right = Arel::Nodes::SqlLiteral.new(encryption_instruction)
end
end
end
-end
+end
View
2  pgcrypto.gemspec
@@ -5,7 +5,7 @@
Gem::Specification.new do |s|
s.name = "pgcrypto"
- s.version = "0.1.1"
+ s.version = "0.2.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Flip Sasser"]
Please sign in to comment.
Something went wrong with that request. Please try again.