pgcrypto 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/CHANGES ADDED
@@ -0,0 +1,4 @@
1
+ 0.0.1
2
+ * INSERT, SELECT, and UPDATE statements are working. But I wrote this
3
+ while testing with a production app, and thus haven't written
4
+ specific tests, so don't get your panties in a twist.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) Phillip Sasser
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,95 @@
1
+ PGCrypto for ActiveRecord::Base
2
+ ===
3
+
4
+ **PGCrypto** adds seamless column-level encryption to your ActiveRecord::Base subclasses. It's literally *one giant hack,*
5
+ so I make no promises as to its efficacy in the real world beyond my tiny, Rails-3.2-based utopia.
6
+
7
+ Installation
8
+ -
9
+
10
+ You need to have PGCrypto installed before this guy will work. [LMGTFY](http://lmgtfy.com/?q=how+to+install+pgcrypto).
11
+
12
+ 1. Add this to your Gemfile:
13
+
14
+ gem "pgcrypto"
15
+
16
+ 2. Do this now:
17
+
18
+ bundle
19
+
20
+ 3. Now point it to your public and private GPG keys:
21
+
22
+ PGCrypto.keys[:private] = {:path => "~/.keys/private.key"}
23
+ PGCrypto.keys[:public] = {:path => "~/.keys/public.key"}
24
+
25
+ 4. PGCrypto columns are named `attribute_encrypted` and in the `binary` format, so do something like this:
26
+
27
+ add_column :users, :social\_security\_number\_encrypted, :binary
28
+
29
+ 5. Tell the User class to encrypt and decrypt the `social_security_number` attribute on the fly:
30
+
31
+ class User < ActiveRecord::Base
32
+ # ... all kinds of neat stuff ...
33
+
34
+ pgcrypto :social_security_number
35
+
36
+ # ... some other fun stuff
37
+ end
38
+
39
+ 6. Profit
40
+
41
+ User.create!(:social_security_number => "466-99-1234") #=> #<User with stuff>
42
+ User.last.social_security_number #=> "466-99-1234"
43
+
44
+ BAM. It looks innocuous on your end, but on the back end that beast is storing the social security number in
45
+ a GPG-encrypted column that can only be decrypted with your secure key.
46
+
47
+ Keys
48
+ -
49
+
50
+ You can tell PGCrypto about your keys in a number of fun ways. The most straightforward is to assign the actual
51
+ content of the key manually:
52
+
53
+ PGCrypto.keys[:private] = "-----BEGIN PGP PRIVATE KEY BLOCK----- ..."
54
+
55
+ You can also give it more specific stuff:
56
+
57
+ PGCrypto.keys[:private] = {:path => ".private.key", :armored => true, :password => "myKeyPASSwhichizneededBRO"}
58
+
59
+ This is especially important if you password protect your private key files (and you SHOULD, for the record)!
60
+
61
+ You can also specify different keys for different purposes:
62
+
63
+ PGCrypto.keys[:user_public] = {:path => '.user_public.key'}
64
+ PGCrypto.keys[:user_private] = {:path => '.user_private.key'}
65
+
66
+ 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:
67
+
68
+ class User < ActiveRecord::Base
69
+ pgcrypto :social_security_number, :private_key => :user_private, :public_key => :user_public
70
+ end
71
+
72
+ FINALLY, if you want to bundle your public key with your application, PGCrypto will automatically load Rails.root/.pgcrypto,
73
+ so feel free to put your public key in there. I recommend deploy-time passing of your private key and password, to ensure it
74
+ doesn't wind up in any long-term storage on the server:
75
+
76
+ PGCrypto.keys[:private] = {:value => ENV['PRIVATE_KEY'], :password => ENV['PRIVATE_KEY_PASSWORD']}
77
+
78
+ Warranty
79
+ -
80
+
81
+ As I mentioned before, this library is one HUGE hack. When you're using something like this alongside data that needs to be
82
+ well protected, this is just scratching the surface. This will make it easy to follow the basics of asymmetric, GPG-based,
83
+ column-level encryption in PostgreSQL but that's about it.
84
+
85
+ As such, the author and Delightful Widgets Inc. offer ABSOLUTELY NO GODDAMN WARRANTY. As I mentioned, this works great in our
86
+ Rails 3.2 world, but YMMV if your version of Arel or ActiveRecord are ahead or behind ours. Sorry, folks.
87
+
88
+ WTF NO TESTS?!!
89
+ -
90
+
91
+ Nope. We built this inside of a production application, and used its test suite to debug everything. Since this is really just
92
+ a preview release, I haven't written a suite for it yet. Sorry.
93
+
94
+ Authored by Flip Sasser
95
+ Copyright (C) 2012 Delightful Widgets, Inc.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+
3
+ task :default => :spec
4
+
5
+ begin
6
+ require 'spec/rake/spectask'
7
+
8
+ desc "Run all examples"
9
+ Spec::Rake::SpecTask.new('spec') do |t|
10
+ t.spec_files = FileList['spec/**/*.rb']
11
+ end
12
+
13
+ desc "Run all examples with RCov"
14
+ Spec::Rake::SpecTask.new('spec:rcov') do |t|
15
+ t.spec_files = FileList['spec/**/*.rb']
16
+ t.rcov = true
17
+ t.rcov_opts = ['--exclude', 'spec,gem']
18
+ end
19
+ rescue LoadError
20
+ puts "Could not load Rspec. To run tests, use `gem install rspec`"
21
+ end
22
+
23
+ begin
24
+ require 'jeweler'
25
+ Jeweler::Tasks.new do |gemspec|
26
+ gemspec.name = "pgcrypto"
27
+ gemspec.summary = "A transparent ActiveRecord::Base extension for encrypted columns"
28
+ gemspec.description = %{
29
+ PGCrypto is an ActiveRecord::Base extension that allows you to asymmetrically
30
+ encrypt PostgreSQL columns with as little trouble as possible. It's totally
31
+ freaking rad.
32
+ }
33
+ gemspec.email = "flip@x451.com"
34
+ gemspec.homepage = "http://github.com/Plinq/pgcrypto"
35
+ gemspec.authors = ["Flip Sasser"]
36
+ end
37
+ rescue LoadError
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/lib/pgcrypto.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'pgcrypto/active_record'
2
+ require 'pgcrypto/arel'
3
+ require 'pgcrypto/key'
4
+ require 'pgcrypto/table_manager'
5
+
6
+ module PGCrypto
7
+ class << self
8
+ def [](key)
9
+ (@table_manager ||= TableManager.new)[key]
10
+ end
11
+
12
+ def keys
13
+ @keys ||= KeyManager.new
14
+ end
15
+ end
16
+
17
+ class Error < StandardError; end
18
+
19
+ module ClassMethods
20
+ def pgcrypto(*column_names)
21
+
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
30
+ # Stash the encryption type in our module so various monkeypatches can access it later!
31
+ PGCrypto[table_name][encrypted_column_name] = options.symbolize_keys!
32
+
33
+ # Add attribute readers/writers to keep this baby as fluid and clean as possible.
34
+ class_eval <<-encrypted_attribute_writer
35
+ def #{column_name}
36
+ @attributes["#{column_name}"]
37
+ end
38
+
39
+ # We write the attribute twice - once as the alias so the accessor keeps working, and once
40
+ # so the actual attribute value is dirty and will be queued up for assignment
41
+ def #{column_name}=(value)
42
+ @attributes["#{column_name}"] = value
43
+ write_attribute(:#{encrypted_column_name}, value)
44
+ end
45
+ encrypted_attribute_writer
46
+ # Did you notice how I was all, "clean as possible" before I fucked w/AR's internal
47
+ # instance variables rather than use the API? *Hilarious.*
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ PGCrypto.keys[:public] = {:path => '.pgcrypto'} if File.file?('.pgcrypto')
54
+
55
+ ActiveRecord::Base.extend PGCrypto::ClassMethods if defined? ActiveRecord::Base
@@ -0,0 +1,107 @@
1
+ require 'active_record/connection_adapters/postgresql_adapter'
2
+
3
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
4
+ alias :original_to_sql :to_sql
5
+ def to_sql(arel)
6
+ case arel
7
+ when Arel::InsertManager
8
+ pgcrypto_tweak_insert(arel)
9
+ when Arel::SelectManager
10
+ pgcrypto_tweak_select(arel)
11
+ when Arel::UpdateManager
12
+ pgcrypto_tweak_update(arel)
13
+ end
14
+ original_to_sql(arel)
15
+ end
16
+
17
+ private
18
+ def pgcrypto_tweak_insert(arel)
19
+ table = arel.ast.relation.name
20
+ unless PGCrypto[table].empty?
21
+ arel.ast.columns.each_with_index do |column, i|
22
+ if options = PGCrypto[table][column.name]
23
+ key = PGCrypto.keys[options[:public_key] || :public]
24
+ value = arel.ast.values.expressions[i]
25
+ quoted_value = quote_string(value)
26
+ encryption_instruction = %[pgp_pub_encrypt(#{quoted_value}, #{key.dearmored})]
27
+ arel.ast.values.expressions[i] = Arel::Nodes::SqlLiteral.new(encryption_instruction)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def pgcrypto_tweak_select(arel)
34
+ # We start by looping through each "core," which is just
35
+ # a SelectStatement, and tweaking both *what* it's selecting
36
+ # and correcting plain-text queries against an encrypted
37
+ # column...
38
+ joins = {}
39
+ arel.ast.cores.each do |core|
40
+ # Now we loop through each SelectStatement's actual selction -
41
+ # typically it's just '*'; and, in fact, that one of the only
42
+ # things we care about!
43
+ new_projections = []
44
+ core.projections.each do |projection|
45
+ next unless projection.respond_to?(:relation)
46
+ # The one other situation we might care about is if the projection is
47
+ # selecting a specifically encrypted column, in which case, we want to
48
+ # _wrap_ it. See how that's different?
49
+ if !PGCrypto[projection.relation.name].empty?
50
+ # Okay, so first, check if it's a broad select
51
+ if projection.name == '*'
52
+ # In this case, we want to just grap all the keys from columns in this table
53
+ # and select them fancy-like
54
+ PGCrypto[projection.relation.name].each do |column, options|
55
+ new_projections.push(pgcrypto_tweak_select_column(column, options, joins))
56
+ end
57
+ elsif options = PGCrypto[projection.relation.name][projection.name]
58
+ # And in this case, we're just selecting a single column!
59
+ new_projections.push(pgcrypto_tweak_select_column(projection.name, options, joins))
60
+ end
61
+ end
62
+ end
63
+ core.projections.push(*new_projections)
64
+
65
+ # Dios mio! What an operation! Now we'll do something similar for the WHERE statements
66
+ core.wheres.each do |where|
67
+ # Now loop through the children to encrypt them for the SELECT
68
+ where.children.each do |child|
69
+ if options = PGCrypto[child.left.relation.name]["#{child.left.name}_encrypted"]
70
+ key = PGCrypto.keys[options[:private_key] || :private]
71
+ joins[key.name] ||= "#{key.dearmored} AS #{key.name}_key"
72
+ child.left = Arel::Nodes::SqlLiteral.new("pgp_pub_decrypt(#{child.left.name}_encrypted, keys.#{key.name}_key)")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ unless joins.empty?
78
+ arel.join(Arel::Nodes::SqlLiteral.new("CROSS JOIN (SELECT #{joins.values.join(', ')}) AS keys"))
79
+ end
80
+ end
81
+
82
+ def pgcrypto_tweak_select_column(column, options, joins)
83
+ return nil unless options[:type] == :pgp
84
+ key = PGCrypto.keys[options[:private_key] || :private]
85
+ select = %[pgp_pub_decrypt(#{column}, keys.#{key.name}_key#{", '#{key.password}'" if key.password}) AS "#{column.to_s.gsub(/_encrypted$/, '')}"]
86
+ joins[key.name] ||= "#{key.dearmored} AS #{key.name}_key"
87
+ Arel::Nodes::SqlLiteral.new(select)
88
+ end
89
+
90
+ def pgcrypto_tweak_update(arel)
91
+ # Loop through the assignments and make sure we take care of that whole
92
+ # NULL value thing!
93
+ arel.ast.values.each do |value|
94
+ if options = PGCrypto[value.left.relation.name][value.left.name]
95
+ case value.right
96
+ when NilClass
97
+ value.right = Arel::Nodes::SqlLiteral.new('NULL')
98
+ else
99
+ key = PGCrypto.keys[options[:public_key] || :public]
100
+ quoted_right = quote_string(value.right)
101
+ encryption_instruction = %[pgp_pub_encrypt('#{quoted_right}', #{key.dearmored})]
102
+ value.right = Arel::Nodes::SqlLiteral.new(encryption_instruction)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,22 @@
1
+ require 'arel/visitors/postgresql'
2
+
3
+ # We override some fun stuff in the PostgreSQL visitor class inside of Arel.
4
+ # This is the _most_ direct approach to tweaking the SQL to INSERT, SELECT,
5
+ # and UPDATE values as encrypted. Unfortunately, the visitor API doesn't
6
+ # give us access to managers as well as nodes, so we have use the public
7
+ # Arel API via the connection adapter's to_sql method. Then we tweak the
8
+ # more specific bits here!
9
+
10
+ Arel::Visitors::PostgreSQL.class_eval do
11
+ alias :original_visit_Arel_Nodes_Assignment :visit_Arel_Nodes_Assignment
12
+ def visit_Arel_Nodes_Assignment(assignment)
13
+ # Hijack the normally inoccuous assignment that happens, seeing as how
14
+ # 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)}"
17
+ "#{visit(assignment.left)} = #{visit(assignment.right)}"
18
+ else
19
+ original_visit_Arel_Nodes_Assignment(assignment)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,55 @@
1
+ module PGCrypto
2
+ class KeyManager < Hash
3
+ def []=(key, value)
4
+ unless value.is_a?(Key)
5
+ value = Key.new(value)
6
+ end
7
+ value.name = key
8
+ super key, value
9
+ end
10
+ end
11
+
12
+ class Key
13
+ attr_accessor :name, :password, :value
14
+ attr_reader :path
15
+ attr_writer :armored
16
+
17
+ def armored?
18
+ @armored
19
+ end
20
+
21
+ def dearmored
22
+ "#{'dearmor(' if armored?}'#{self}'#{')' if armored?}"
23
+ end
24
+
25
+ def initialize(options = {})
26
+ if options.is_a?(String)
27
+ self.value = key
28
+ elsif options.is_a?(Hash)
29
+ options.each do |key, value|
30
+ send("#{key}=", value)
31
+ end
32
+ end
33
+ end
34
+
35
+ def path=(keyfile)
36
+ keyfile = File.expand_path(keyfile)
37
+ raise PGCrypto::Error, "\#{keyfile} does not exist!" unless File.file?(keyfile)
38
+ @path = keyfile
39
+ self.value = File.read(keyfile)
40
+ end
41
+
42
+ def to_s
43
+ value
44
+ end
45
+
46
+ def value=(key)
47
+ if key =~ /^-----BEGIN PGP /
48
+ self.armored = true
49
+ else
50
+ self.armored = false
51
+ end
52
+ @value = key.dup.freeze
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,18 @@
1
+ module PGCrypto
2
+ class Table < Hash
3
+ def [](key)
4
+ super(key.to_sym)
5
+ end
6
+
7
+ def []=(key, value)
8
+ super key.to_sym, value
9
+ end
10
+ end
11
+
12
+ class TableManager < Table
13
+ def [](key)
14
+ return {} unless key
15
+ super(key) || self[key] = Table.new
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pgcrypto
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Flip Sasser
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-24 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! "\n PGCrypto is an ActiveRecord::Base extension that allows you
15
+ to asymmetrically\n encrypt PostgreSQL columns with as little trouble as possible.
16
+ It's totally\n freaking rad.\n "
17
+ email: flip@x451.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files:
21
+ - LICENSE
22
+ - README.markdown
23
+ files:
24
+ - .rspec
25
+ - CHANGES
26
+ - LICENSE
27
+ - README.markdown
28
+ - Rakefile
29
+ - VERSION
30
+ - lib/pgcrypto.rb
31
+ - lib/pgcrypto/active_record.rb
32
+ - lib/pgcrypto/arel.rb
33
+ - lib/pgcrypto/key.rb
34
+ - lib/pgcrypto/table_manager.rb
35
+ homepage: http://github.com/Plinq/pgcrypto
36
+ licenses: []
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 1.8.10
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: A transparent ActiveRecord::Base extension for encrypted columns
59
+ test_files: []