pgcrypto 0.0.1

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/.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: []