crypt_keeper 0.16.0.pre → 1.1.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +22 -13
  4. data/Appraisals +6 -16
  5. data/README.md +43 -21
  6. data/Rakefile +7 -3
  7. data/crypt_keeper.gemspec +9 -9
  8. data/gemfiles/activerecord_4_2.gemfile +8 -0
  9. data/gemfiles/activerecord_5_1.gemfile +8 -0
  10. data/lib/crypt_keeper/helper.rb +31 -21
  11. data/lib/crypt_keeper/log_subscriber/mysql_aes.rb +11 -11
  12. data/lib/crypt_keeper/log_subscriber/postgres_pgp.rb +29 -10
  13. data/lib/crypt_keeper/model.rb +64 -17
  14. data/lib/crypt_keeper/provider/aes_new.rb +1 -1
  15. data/lib/crypt_keeper/provider/base.rb +21 -0
  16. data/lib/crypt_keeper/provider/mysql_aes_new.rb +2 -2
  17. data/lib/crypt_keeper/provider/postgres_base.rb +28 -0
  18. data/lib/crypt_keeper/provider/postgres_pgp.rb +27 -8
  19. data/lib/crypt_keeper/provider/postgres_pgp_public_key.rb +4 -20
  20. data/lib/crypt_keeper/version.rb +1 -1
  21. data/lib/crypt_keeper.rb +11 -2
  22. data/spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb +50 -0
  23. data/spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb +78 -0
  24. data/spec/crypt_keeper/model_spec.rb +178 -0
  25. data/spec/crypt_keeper/provider/aes_new_spec.rb +41 -0
  26. data/spec/crypt_keeper/provider/mysql_aes_new_spec.rb +50 -0
  27. data/spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb +67 -0
  28. data/spec/crypt_keeper/provider/postgres_pgp_spec.rb +87 -0
  29. data/spec/spec_helper.rb +0 -1
  30. data/spec/support/active_record.rb +36 -26
  31. data/spec/support/encryptors.rb +9 -3
  32. data/spec/support/logging.rb +89 -0
  33. metadata +84 -92
  34. data/bin/crypt_keeper +0 -99
  35. data/gemfiles/activerecord_3_1.gemfile +0 -8
  36. data/gemfiles/activerecord_3_1.gemfile.lock +0 -109
  37. data/gemfiles/activerecord_3_2.gemfile +0 -8
  38. data/gemfiles/activerecord_3_2.gemfile.lock +0 -109
  39. data/gemfiles/activerecord_4_0.gemfile +0 -8
  40. data/gemfiles/activerecord_4_0.gemfile.lock +0 -117
  41. data/gemfiles/activerecord_4_1.gemfile +0 -8
  42. data/gemfiles/activerecord_4_1.gemfile.lock +0 -117
  43. data/lib/crypt_keeper/provider/aes.rb +0 -66
  44. data/lib/crypt_keeper/provider/mysql_aes.rb +0 -47
  45. data/spec/log_subscriber/postgres_pgp_spec.rb +0 -98
  46. data/spec/model_spec.rb +0 -95
  47. data/spec/provider/aes_new_spec.rb +0 -45
  48. data/spec/provider/aes_spec.rb +0 -67
  49. data/spec/provider/mysql_aes_new_spec.rb +0 -52
  50. data/spec/provider/mysql_aes_spec.rb +0 -35
  51. data/spec/provider/postgres_pgp_public_key_spec.rb +0 -70
  52. data/spec/provider/postgres_pgp_spec.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 742dc47f65ba6f9c21e39dad6b34a25196e74978
4
- data.tar.gz: 4e5eaafa1b9876217c739a4c3649fc52308dc0ff
3
+ metadata.gz: 78d7a7f1ea679f9a1542a65b537a446065c9cd27
4
+ data.tar.gz: 518d78e7d4a80e07faa80b9eac9c3b268e2db2c8
5
5
  SHA512:
6
- metadata.gz: f43ec8ae9564d08e804d5539ee04fff90b072502e65128a983c4727953ad930a00cfc2f72bb50d5e6b6585c7bd43bb83199f75bd3cabffb27e317a3f56539dd6
7
- data.tar.gz: 097e420adcb718391b14476dff3a8a1c00279bcd211df9cef3f163a424bd1267d5be85b1cfa15fe1773a77cd930cc9fea1c4c7ff0d4a406ca4c7146a570f5d61
6
+ metadata.gz: 30b9ccbea99c9ac89ee4bf4c9df73fc0b95e2ae3f65aca3e9936a95bd406d271e672e73cefa2c717a2f0c057c638ca876a238c7058da2c3ad6e3f2d2d12720cb
7
+ data.tar.gz: 07f33f2caca79c5be2c45f2365601e8939b5c64200fba53c00de3e70a9d6b221a68efe47aad00fc7b42003bce2c964ce68701656966149afe4b65dfc45a4e0a7
data/.gitignore CHANGED
@@ -3,7 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
6
+ *emfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
data/.travis.yml CHANGED
@@ -1,26 +1,35 @@
1
- branches: master
2
1
  language: ruby
2
+
3
+ rvm:
4
+ - 2.1.10
5
+ - 2.2.5
6
+ - 2.3.1
7
+
3
8
  gemfile:
4
- - gemfiles/activerecord_3_1.gemfile
5
- - gemfiles/activerecord_3_2.gemfile
6
- - gemfiles/activerecord_4_0.gemfile
7
- - gemfiles/activerecord_4_1.gemfile
8
- before_install:
9
- - bundle install
9
+ - gemfiles/activerecord_4_2.gemfile
10
+ - gemfiles/activerecord_5_1.gemfile
11
+
12
+ matrix:
13
+ exclude:
14
+ - gemfile: gemfiles/activerecord_5_1.gemfile
15
+ rvm: 2.1.10
16
+
17
+ addons:
18
+ postgresql: 9.3
19
+
20
+ sudo: false
21
+
10
22
  before_script:
11
23
  - cp spec/default.database.yml spec/database.yml
12
24
  - psql -c 'CREATE DATABASE crypt_keeper_providers;' -U postgres
13
25
  - psql crypt_keeper_providers -c 'CREATE EXTENSION IF NOT EXISTS pgcrypto;' -U postgres
14
26
  - mysql -e 'CREATE DATABASE crypt_keeper_providers'
27
+
28
+ branches: master
29
+
15
30
  notifications:
16
31
  email:
17
32
  recipients:
18
33
  jmazzi@gmail.com
19
34
  on_success: never
20
35
  on_failure: always
21
- rvm:
22
- - 1.9.3
23
- - 2.0.0
24
- - 2.1.1
25
- addons:
26
- postgresql: 9.3
data/Appraisals CHANGED
@@ -1,19 +1,9 @@
1
- appraise "activerecord_3_1" do
2
- gem 'activerecord', '~> 3.1.10'
3
- gem 'activesupport', '~> 3.1.10'
1
+ appraise "activerecord_4_2" do
2
+ gem "activerecord", "~> 4.2.0"
3
+ gem "activesupport", "~> 4.2.0"
4
4
  end
5
5
 
6
- appraise "activerecord_3_2" do
7
- gem 'activerecord', '~> 3.2.14'
8
- gem 'activesupport', '~> 3.2.14'
9
- end
10
-
11
- appraise "activerecord_4_0" do
12
- gem 'activerecord', '~> 4.0.0'
13
- gem 'activesupport', '~> 4.0.0'
14
- end
15
-
16
- appraise "activerecord_4_1" do
17
- gem 'activerecord', '~> 4.1.0'
18
- gem 'activesupport', '~> 4.1.0'
6
+ appraise "activerecord_5_0" do
7
+ gem "activerecord", "~> 5.0.0"
8
+ gem "activesupport", "~> 5.0.0"
19
9
  end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://secure.travis-ci.org/jmazzi/crypt_keeper.png?branch=master)](http://travis-ci.org/jmazzi/crypt_keeper)
1
+ [![Build Status](https://secure.travis-ci.org/jmazzi/crypt_keeper.png?branch=master)](http://travis-ci.org/jmazzi/crypt_keeper) [![Gem Version](https://badge.fury.io/rb/crypt_keeper.svg)](http://badge.fury.io/rb/crypt_keeper)
2
2
 
3
3
  ![CryptKeeper](http://i.imgur.com/qf0aD.jpg)
4
4
 
@@ -15,7 +15,7 @@ is a simple class that does 3 things.
15
15
  Note: Any options defined using `crypt_keeper` will be passed to `new` as a
16
16
  hash.
17
17
 
18
- You can see an AES example [here](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes.rb).
18
+ You can see an AES example [here](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes_new.rb).
19
19
 
20
20
  ## Why?
21
21
 
@@ -27,7 +27,7 @@ simple that *just works*.
27
27
 
28
28
  ```ruby
29
29
  class MyModel < ActiveRecord::Base
30
- crypt_keeper :field, :other_field, :encryptor => :aes, :key => 'super_good_password', salt: 'salt'
30
+ crypt_keeper :field, :other_field, :encryptor => :aes_new, :key => 'super_good_password', salt: 'salt'
31
31
  end
32
32
 
33
33
  model = MyModel.new(field: 'sometext')
@@ -47,28 +47,49 @@ expected behavior, and has its use cases. An example would be migrating from
47
47
  one type of encryption to another. Using `update_column` would allow you to
48
48
  update the content without going through the current encryptor.
49
49
 
50
- ## Available Encryptors
50
+ ## Encodings
51
51
 
52
- There are three included encryptors.
52
+ You can force an encoding on the plaintext before encryption and after decryption by using the `encoding` option. This is useful when dealing with multibyte strings:
53
53
 
54
- * [AES](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes_new.rb)
55
- * Encryption is peformed using AES-256 via OpenSSL.
56
- * Passphrases are derived using [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)
54
+ ```ruby
55
+ class MyModel < ActiveRecord::Base
56
+ crypt_keeper :field, :other_field, :encryptor => :aes_new, :key => 'super_good_password', salt: 'salt', :encoding => 'UTF-8'
57
+ end
58
+
59
+ model = MyModel.new(field: 'Tromsø')
60
+ model.save! #=> Your data is now encrypted
61
+ model.field #=> 'Tromsø'
62
+ model.field.encoding #=> #<Encoding:UTF-8>
63
+ ```
64
+
65
+ ## Adding encryption to an existing table
57
66
 
58
- * [AES Legacy](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes.rb) *DEPRECATED*
67
+ If you are working with an existing table you would like to encrypt, you must use the `MyExistingModel.encrypt_table!` class method.
68
+
69
+ ```ruby
70
+ class MyExistingModel < ActiveRecord::Base
71
+ crypt_keeper :field, :other_field, :encryptor => :aes_new, :key => 'super_good_password', salt: 'salt'
72
+ end
73
+
74
+ MyExistingModel.encrypt_table!
75
+ ```
76
+
77
+ Running `encrypt_table!` will encrypt all rows in the database using the encryption method specificed by the `crypt_keeper` line in your model.
78
+
79
+ ## Supported Available Encryptors
80
+
81
+ There are four supported encryptors: `aes_new`, `mysql_aes_new`, `postgres_pgp`, `postgres_pgp_public_key`.
82
+
83
+ * [AES New](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/aes_new.rb)
59
84
  * Encryption is peformed using AES-256 via OpenSSL.
85
+ * Passphrases are derived using [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)
60
86
 
61
- * [MySQL AES](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/mysql_aes_new.rb)
87
+ * [MySQL AES New](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/mysql_aes_new.rb)
62
88
  * Encryption is peformed MySQL's native AES functions.
63
89
  * ActiveRecord logs are [automatically](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/log_subscriber/mysql_aes.rb)
64
90
  filtered for you to protect sensitive data from being logged.
65
91
  * Passphrases are derived using [PBKDF2](http://en.wikipedia.org/wiki/PBKDF2)
66
92
 
67
- * [MySQL AES](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/mysql_aes.rb) *DEPRECATED*
68
- * Encryption is peformed MySQL's native AES functions.
69
- * ActiveRecord logs are [automatically](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/log_subscriber/mysql_aes.rb)
70
- filtered for you to protect senitive data from being logged.
71
-
72
93
  * [PostgreSQL PGP](https://github.com/jmazzi/crypt_keeper/blob/master/lib/crypt_keeper/provider/postgres_pgp.rb).
73
94
  * Encryption is performed using PostgresSQL's native [PGP functions](http://www.postgresql.org/docs/9.1/static/pgcrypto.html).
74
95
  * It requires the `pgcrypto` PostgresSQL extension:
@@ -110,12 +131,13 @@ Model.where(something: 'blah').search_by_plaintext(:field, 'searchstring')
110
131
  ## Creating your own encryptor
111
132
 
112
133
  Creating your own encryptor is easy. All you have to do is create a class
113
- under the `CryptKeeper::Provider` namespace, like this:
134
+ under the `CryptKeeper::Provider` namespace, and inherit from the `Base` encryptor,
135
+ like this:
114
136
 
115
137
  ```ruby
116
138
  module CryptKeeper
117
139
  module Provider
118
- class MyEncryptor
140
+ class MyEncryptor < Base
119
141
  def initialize(options = {})
120
142
  end
121
143
 
@@ -141,11 +163,11 @@ end
141
163
 
142
164
  ## Requirements
143
165
 
144
- CryptKeeper has been tested against ActiveRecord 3.1, 3.2, 4.0, 4.1 using ruby
145
- 1.9.3, 2.0.0 and 2.1.1
166
+ CryptKeeper has been tested against ActiveRecord 4.2 and 5.0 using Ruby
167
+ 2.1.10, 2.2.5 and 2.3.1.
146
168
 
147
- ActiveRecord 4.0 is supported starting with v0.11.0.
148
- ActiveRecord 4.1 is supported starting with v0.16.0. (unreleased, use master branch)
169
+ ActiveRecord 4.2 is supported starting with v0.19.0.
170
+ ActiveRecord 5.0 is supported starting with v0.23.0.
149
171
 
150
172
  ## Installation
151
173
 
data/Rakefile CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
- require 'rspec/core/rake_task'
4
- require 'appraisal'
3
+ require "rspec/core/rake_task"
4
+ require "appraisal"
5
5
 
6
6
  RSpec::Core::RakeTask.new :spec
7
7
  Bundler::GemHelper.install_tasks
8
8
 
9
- task :default => [:spec]
9
+ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"]
10
+ task default: :appraisal
11
+ else
12
+ task default: :spec
13
+ end
data/crypt_keeper.gemspec CHANGED
@@ -4,7 +4,7 @@ require File.expand_path('../lib/crypt_keeper/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Justin Mazzi"]
6
6
  gem.email = ["jmazzi@gmail.com"]
7
- gem.description = %q{Transparent encryption for ActiveRecord that isn't over-engineered}
7
+ gem.description = %q{Transparent ActiveRecord encryption}
8
8
  gem.summary = gem.description
9
9
  gem.homepage = "http://jmazzi.github.com/crypt_keeper/"
10
10
  gem.license = 'MIT'
@@ -16,18 +16,18 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = CryptKeeper::VERSION
18
18
 
19
- gem.add_runtime_dependency 'activerecord', '>= 3.1', '< 4.2'
20
- gem.add_runtime_dependency 'activesupport', '>= 3.1', '< 4.2'
19
+ gem.add_runtime_dependency 'activerecord', '>= 4.2', '< 5.2'
20
+ gem.add_runtime_dependency 'activesupport', '>= 4.2', '< 5.2'
21
21
  gem.add_runtime_dependency 'aes', '~> 0.5.0'
22
22
  gem.add_runtime_dependency 'armor', '~> 0.0.2'
23
23
 
24
- gem.add_development_dependency 'rspec', '~> 2.13.0'
25
- gem.add_development_dependency 'guard', '~> 1.8.0'
26
- gem.add_development_dependency 'guard-rspec', '~> 2.5.4'
27
- gem.add_development_dependency 'rake', '~> 10.0.3'
24
+ gem.add_development_dependency 'rspec', '~> 3.5.0'
25
+ gem.add_development_dependency 'guard', '~> 2.6.1'
26
+ gem.add_development_dependency 'guard-rspec', '~> 4.2.9'
27
+ gem.add_development_dependency 'rake', '~> 10.3.1'
28
28
  gem.add_development_dependency 'rb-fsevent', '~> 0.9.1'
29
29
  gem.add_development_dependency 'coveralls'
30
- gem.add_development_dependency 'appraisal', '~> 0.5.2'
30
+ gem.add_development_dependency 'appraisal', '~> 2.1.0'
31
31
 
32
32
  if RUBY_PLATFORM == 'java'
33
33
  gem.add_development_dependency 'jruby-openssl', '~> 0.7.7'
@@ -36,7 +36,7 @@ Gem::Specification.new do |gem|
36
36
  gem.add_development_dependency 'activerecord-jdbcmysql-adapter'
37
37
  else
38
38
  gem.add_development_dependency 'sqlite3'
39
- gem.add_development_dependency 'pg', '~> 0.15.1'
39
+ gem.add_development_dependency 'pg', '~> 0.18.0'
40
40
  gem.add_development_dependency 'mysql2', '~> 0.3.11'
41
41
  end
42
42
  end
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.2.0"
6
+ gem "activesupport", "~> 4.2.0"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.1"
6
+ gem "activesupport", "~> 5.1"
7
+
8
+ gemspec :path => "../"
@@ -3,10 +3,38 @@ module CryptKeeper
3
3
  module SQL
4
4
  private
5
5
 
6
- # Private: Sanitize an sql query and then execute it
7
- def escape_and_execute_sql(query)
6
+ # Private: Sanitize an sql query and then execute it.
7
+ #
8
+ # query - the sql query
9
+ # new_transaction - if the query should run inside a new transaction
10
+ #
11
+ # Returns the ActiveRecord response.
12
+ def escape_and_execute_sql(query, new_transaction: false)
8
13
  query = ::ActiveRecord::Base.send :sanitize_sql_array, query
9
- ::ActiveRecord::Base.connection.execute(query).first
14
+
15
+ if CryptKeeper.silence_logs?
16
+ ::ActiveRecord::Base.logger.silence do
17
+ execute_sql(query, new_transaction: new_transaction)
18
+ end
19
+ else
20
+ execute_sql(query, new_transaction: new_transaction)
21
+ end
22
+ end
23
+
24
+ # Private: Executes the query.
25
+ #
26
+ # query - the sql query
27
+ # new_transaction - if the query should run inside a new transaction
28
+ #
29
+ # Returns an Array.
30
+ def execute_sql(query, new_transaction: false)
31
+ if new_transaction
32
+ ::ActiveRecord::Base.transaction(requires_new: true) do
33
+ ::ActiveRecord::Base.connection.execute(query).first
34
+ end
35
+ else
36
+ ::ActiveRecord::Base.connection.execute(query).first
37
+ end
10
38
  end
11
39
  end
12
40
 
@@ -17,23 +45,5 @@ module CryptKeeper
17
45
  ::Armor.digest(key, salt)
18
46
  end
19
47
  end
20
-
21
- module Serializer
22
- def dump(value)
23
- if value.blank?
24
- value
25
- else
26
- encrypt(value)
27
- end
28
- end
29
-
30
- def load(value)
31
- if value.blank?
32
- value
33
- else
34
- decrypt(value)
35
- end
36
- end
37
- end
38
48
  end
39
49
  end
@@ -4,26 +4,26 @@ require 'active_support/lazy_load_hooks'
4
4
  module CryptKeeper
5
5
  module LogSubscriber
6
6
  module MysqlAes
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- alias_method_chain :sql, :mysql_aes
11
- end
12
-
13
7
  # Public: Prevents sensitive data from being logged
14
- def sql_with_mysql_aes(event)
15
- filter = /(aes_(encrypt|decrypt))\(.*\)/i
8
+ #
9
+ # event - An ActiveSupport::Notifications::Event
10
+ #
11
+ # Returns a boolean.
12
+ def sql(event)
13
+ filter = /(aes_(encrypt|decrypt))\(.*\)/i
14
+ payload = event.payload[:sql]
15
+ .encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
16
16
 
17
- event.payload[:sql] = event.payload[:sql].gsub(filter) do |_|
17
+ event.payload[:sql] = payload.gsub(filter) do |_|
18
18
  "#{$1}([FILTERED])"
19
19
  end
20
20
 
21
- sql_without_mysql_aes(event)
21
+ super(event)
22
22
  end
23
23
  end
24
24
  end
25
25
  end
26
26
 
27
27
  ActiveSupport.on_load :crypt_keeper_mysql_aes_log do
28
- ActiveRecord::LogSubscriber.send :include, CryptKeeper::LogSubscriber::MysqlAes
28
+ ActiveRecord::LogSubscriber.prepend CryptKeeper::LogSubscriber::MysqlAes
29
29
  end
@@ -4,26 +4,45 @@ require 'active_support/lazy_load_hooks'
4
4
  module CryptKeeper
5
5
  module LogSubscriber
6
6
  module PostgresPgp
7
- extend ActiveSupport::Concern
7
+ FILTER = /(\(*)(?<operation>pgp_sym_encrypt|pgp_sym_decrypt|pgp_pub_encrypt|pgp_pub_decrypt|pgp_key_id)(\(+.*\)+)/im
8
8
 
9
- included do
10
- alias_method_chain :sql, :postgres_pgp
9
+ # Public: Prevents sensitive data from being logged
10
+ #
11
+ # event - An ActiveSupport::Notifications::Event
12
+ #
13
+ # Returns a boolean.
14
+ def sql(event)
15
+ payload = crypt_keeper_payload_parse(event.payload[:sql])
16
+ event.payload[:sql] = crypt_keeper_filter_postgres_log(payload)
17
+ super(event)
11
18
  end
12
19
 
13
- # Public: Prevents sensitive data from being logged
14
- def sql_with_postgres_pgp(event)
15
- filter = /(\(*)pgp_(sym|pub)_(?<operation>decrypt|encrypt)(\(+.*\)+)/im
20
+ private
16
21
 
17
- event.payload[:sql] = event.payload[:sql].gsub(filter) do |_|
22
+ # Private: Parses the payload to UTF.
23
+ #
24
+ # payload - the payload string
25
+ #
26
+ # Returns a string.
27
+ def crypt_keeper_payload_parse(payload)
28
+ payload.encode('UTF-8', 'binary',
29
+ invalid: :replace, undef: :replace, replace: '')
30
+ end
31
+
32
+ # Private: Filters the payload.
33
+ #
34
+ # payload - the payload string
35
+ #
36
+ # Returns a string.
37
+ def crypt_keeper_filter_postgres_log(payload)
38
+ payload.gsub(FILTER) do |_|
18
39
  "#{$~[:operation]}([FILTERED])"
19
40
  end
20
-
21
- sql_without_postgres_pgp(event)
22
41
  end
23
42
  end
24
43
  end
25
44
  end
26
45
 
27
46
  ActiveSupport.on_load :crypt_keeper_postgres_pgp_log do
28
- ActiveRecord::LogSubscriber.send :include, CryptKeeper::LogSubscriber::PostgresPgp
47
+ ActiveRecord::LogSubscriber.prepend CryptKeeper::LogSubscriber::PostgresPgp
29
48
  end
@@ -10,8 +10,9 @@ module CryptKeeper
10
10
  def ensure_valid_field!(field)
11
11
  if self.class.columns_hash["#{field}"].nil?
12
12
  raise ArgumentError, "Column :#{field} does not exist"
13
- elsif self.class.columns_hash["#{field}"].type != :text
14
- raise ArgumentError, "Column :#{field} must be of type 'text' to be used for encryption"
13
+ elsif !%i(text binary).include?(self.class.columns_hash["#{field}"].type)
14
+ raise ArgumentError, "Column :#{field} must be of type 'text' or 'binary' \
15
+ to be used for encryption"
15
16
  end
16
17
  end
17
18
 
@@ -24,6 +25,15 @@ module CryptKeeper
24
25
  end
25
26
  end
26
27
 
28
+ # Private: Force string encodings if the option is set
29
+ def force_encodings_on_fields
30
+ crypt_keeper_fields.each do |field|
31
+ if attributes.has_key?(field.to_s) && send(field).respond_to?(:force_encoding)
32
+ send(field).force_encoding(crypt_keeper_encoding)
33
+ end
34
+ end
35
+ end
36
+
27
37
  module ClassMethods
28
38
  # Public: Setup fields for encryption
29
39
  #
@@ -42,27 +52,65 @@ module CryptKeeper
42
52
  class_attribute :crypt_keeper_fields
43
53
  class_attribute :crypt_keeper_encryptor
44
54
  class_attribute :crypt_keeper_options
55
+ class_attribute :crypt_keeper_encoding
45
56
 
46
57
  self.crypt_keeper_options = args.extract_options!
47
58
  self.crypt_keeper_encryptor = crypt_keeper_options.delete(:encryptor)
59
+ self.crypt_keeper_encoding = crypt_keeper_options.delete(:encoding)
48
60
  self.crypt_keeper_fields = args
49
61
 
50
62
  ensure_valid_encryptor!
51
63
 
52
64
  before_save :enforce_column_types_callback
53
65
 
66
+ if self.crypt_keeper_encoding
67
+ after_find :force_encodings_on_fields
68
+ before_save :force_encodings_on_fields
69
+ end
70
+
54
71
  crypt_keeper_fields.each do |field|
55
- serialize field, encryptor_klass.new(crypt_keeper_options).
56
- extend(::CryptKeeper::Helper::Serializer)
72
+ serialize field, encryptor
57
73
  end
58
74
  end
59
75
 
60
76
  def search_by_plaintext(field, criteria)
61
77
  if crypt_keeper_fields.include?(field.to_sym)
62
- encryptor = encryptor_klass.new(crypt_keeper_options)
63
- encryptor.search(scoping_strategy, field.to_s, criteria)
78
+ encryptor.search(all, field.to_s, criteria)
64
79
  else
65
- raise "#{field} is not a crypt_keeper field"
80
+ raise ArgumentError, "#{field} is not a crypt_keeper field"
81
+ end
82
+ end
83
+
84
+ # Public: Encrypt a table for the first time.
85
+ def encrypt_table!
86
+ tmp_table = Class.new(ActiveRecord::Base).tap do |c|
87
+ c.table_name = self.table_name
88
+ c.inheritance_column = :type_disabled
89
+ end
90
+
91
+ transaction do
92
+ tmp_table.find_each do |r|
93
+ crypt_keeper_fields.each do |field|
94
+ r.send("#{field}=", encryptor.encrypt(r[field])) if r[field].present?
95
+ end
96
+
97
+ r.save!
98
+ end
99
+ end
100
+ end
101
+
102
+ # Public: Decrypt a table (reverse of encrypt_table!)
103
+ def decrypt_table!
104
+ tmp_table = Class.new(ActiveRecord::Base).tap { |c| c.table_name = self.table_name }
105
+
106
+ transaction do
107
+ tmp_table.find_each do |r|
108
+ crypt_keeper_fields.each do |field|
109
+ r.send("#{field}=", encryptor.decrypt(r[field])) if r[field].present?
110
+ end
111
+
112
+ r.save!
113
+ end
66
114
  end
67
115
  end
68
116
 
@@ -73,18 +121,17 @@ module CryptKeeper
73
121
  @encryptor_klass ||= "CryptKeeper::Provider::#{crypt_keeper_encryptor.to_s.camelize}".constantize
74
122
  end
75
123
 
76
- # Private: Ensure that the encryptor responds to new
77
- def ensure_valid_encryptor!
78
- unless defined?(encryptor_klass) && encryptor_klass.respond_to?(:new)
79
- raise "You must specify a valid encryptor `crypt_keeper :encryptor => :aes`"
80
- end
124
+ # Private: The encryptor instance.
125
+ def encryptor
126
+ @encryptor ||= encryptor_klass.new(crypt_keeper_options)
81
127
  end
82
128
 
83
- def scoping_strategy
84
- if ::ActiveRecord.respond_to?(:version) && ::ActiveRecord.version.segments[0] == 4
85
- all
86
- else
87
- scoped
129
+ # Private: Ensure we have a valid encryptor.
130
+ def ensure_valid_encryptor!
131
+ unless defined?(encryptor_klass) && encryptor_klass.respond_to?(:new) &&
132
+ %i(load dump).all? { |m| encryptor.respond_to?(m) }
133
+ raise ArgumentError, "You must specify a valid encryptor that implements " \
134
+ "the `load` and `dump` methods (you can inherit from CryptKeeper::Provider::Base). Example: `crypt_keeper :encryptor => :aes`"
88
135
  end
89
136
  end
90
137
  end
@@ -3,7 +3,7 @@ require 'armor'
3
3
 
4
4
  module CryptKeeper
5
5
  module Provider
6
- class AesNew
6
+ class AesNew < Base
7
7
  include CryptKeeper::Helper::DigestPassphrase
8
8
 
9
9
  # Public: The encryption key
@@ -0,0 +1,21 @@
1
+ module CryptKeeper
2
+ module Provider
3
+ class Base
4
+ def dump(value)
5
+ if value.blank? || CryptKeeper.stub_encryption?
6
+ value
7
+ else
8
+ encrypt(value.to_s)
9
+ end
10
+ end
11
+
12
+ def load(value)
13
+ if value.blank? || CryptKeeper.stub_encryption?
14
+ value
15
+ else
16
+ decrypt(value)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,7 +2,7 @@ require 'crypt_keeper/log_subscriber/mysql_aes'
2
2
 
3
3
  module CryptKeeper
4
4
  module Provider
5
- class MysqlAesNew
5
+ class MysqlAesNew < Base
6
6
  include CryptKeeper::Helper::SQL
7
7
  include CryptKeeper::Helper::DigestPassphrase
8
8
 
@@ -36,7 +36,7 @@ module CryptKeeper
36
36
  #
37
37
  # Returns an Enumerable
38
38
  def search(records, field, criteria)
39
- records.where(field.to_sym => encrypt(criteria))
39
+ records.where("#{field} = ?", encrypt(criteria))
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,28 @@
1
+ require 'crypt_keeper/log_subscriber/postgres_pgp'
2
+
3
+ module CryptKeeper
4
+ module Provider
5
+ class PostgresBase < Base
6
+ include CryptKeeper::Helper::SQL
7
+ include CryptKeeper::LogSubscriber::PostgresPgp
8
+
9
+ INVALID_DATA_ERROR = "Wrong key or corrupt data".freeze
10
+
11
+ # Public: Checks if value is already encrypted.
12
+ #
13
+ # Returns boolean
14
+ def encrypted?(value)
15
+ begin
16
+ escape_and_execute_sql(["SELECT pgp_key_id(?)", value.to_s],
17
+ new_transaction: true)['pgp_key_id'].present?
18
+ rescue ActiveRecord::StatementInvalid => e
19
+ if e.message.include?(INVALID_DATA_ERROR)
20
+ false
21
+ else
22
+ raise
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end