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
@@ -1,10 +1,6 @@
1
- require 'crypt_keeper/log_subscriber/postgres_pgp'
2
-
3
1
  module CryptKeeper
4
2
  module Provider
5
- class PostgresPgp
6
- include CryptKeeper::Helper::SQL
7
-
3
+ class PostgresPgp < PostgresBase
8
4
  attr_accessor :key
9
5
  attr_accessor :pgcrypto_options
10
6
 
@@ -25,18 +21,41 @@ module CryptKeeper
25
21
  #
26
22
  # Returns an encrypted string
27
23
  def encrypt(value)
28
- escape_and_execute_sql(["SELECT pgp_sym_encrypt(?, ?, ?)", value.to_s, key, pgcrypto_options])['pgp_sym_encrypt']
24
+ rescue_invalid_statement do
25
+ escape_and_execute_sql(["SELECT pgp_sym_encrypt(?, ?, ?)",
26
+ value.to_s, key, pgcrypto_options])['pgp_sym_encrypt']
27
+ end
29
28
  end
30
29
 
31
30
  # Public: Decrypts a string
32
31
  #
33
32
  # Returns a plaintext string
34
33
  def decrypt(value)
35
- escape_and_execute_sql(["SELECT pgp_sym_decrypt(?, ?)", value, key])['pgp_sym_decrypt']
34
+ rescue_invalid_statement do
35
+ if encrypted?(value)
36
+ escape_and_execute_sql(["SELECT pgp_sym_decrypt(?, ?)",
37
+ value, key])['pgp_sym_decrypt']
38
+ else
39
+ value
40
+ end
41
+ end
36
42
  end
37
43
 
38
44
  def search(records, field, criteria)
39
- records.where("(pgp_sym_decrypt(cast(#{field} AS bytea), ?) = ?)", key, criteria)
45
+ records.where("(pgp_sym_decrypt(cast(\"#{field}\" AS bytea), ?) = ?)",
46
+ key, criteria)
47
+ end
48
+
49
+ private
50
+
51
+ # Private: Rescues and filters invalid statement errors. Run the code
52
+ # within a block for it to be rescued.
53
+ def rescue_invalid_statement
54
+ yield
55
+ rescue ActiveRecord::StatementInvalid => e
56
+ message = crypt_keeper_payload_parse(e.message)
57
+ message = crypt_keeper_filter_postgres_log(message)
58
+ raise ActiveRecord::StatementInvalid, message
40
59
  end
41
60
  end
42
61
  end
@@ -1,10 +1,6 @@
1
- require 'crypt_keeper/log_subscriber/postgres_pgp'
2
-
3
1
  module CryptKeeper
4
2
  module Provider
5
- class PostgresPgpPublicKey
6
- include CryptKeeper::Helper::SQL
7
-
3
+ class PostgresPgpPublicKey < PostgresBase
8
4
  attr_accessor :key
9
5
 
10
6
  def initialize(options = {})
@@ -33,25 +29,13 @@ module CryptKeeper
33
29
  #
34
30
  # Returns a plaintext string
35
31
  def decrypt(value)
36
- if @private_key.present?
37
- escape_and_execute_sql(["SELECT pgp_pub_decrypt(?, dearmor(?), ?)", value, @private_key, @key])['pgp_pub_decrypt']
32
+ if @private_key.present? && encrypted?(value)
33
+ escape_and_execute_sql(["SELECT pgp_pub_decrypt(?, dearmor(?), ?)",
34
+ value, @private_key, @key])['pgp_pub_decrypt']
38
35
  else
39
36
  value
40
37
  end
41
38
  end
42
-
43
- # Public: Attempts to extract a PGP key id. If it's successful, it returns true
44
- #
45
- # Returns boolean
46
- def encrypted?(value)
47
- begin
48
- ActiveRecord::Base.transaction(requires_new: true) do
49
- escape_and_execute_sql(["SELECT pgp_key_id(?)", value.to_s])['pgp_key_id'].present?
50
- end
51
- rescue ActiveRecord::StatementInvalid
52
- false
53
- end
54
- end
55
39
  end
56
40
  end
57
41
  end
@@ -1,3 +1,3 @@
1
1
  module CryptKeeper
2
- VERSION = "0.16.0.pre"
2
+ VERSION = "1.1.1"
3
3
  end
data/lib/crypt_keeper.rb CHANGED
@@ -3,12 +3,21 @@ require 'active_record'
3
3
  require 'crypt_keeper/version'
4
4
  require 'crypt_keeper/model'
5
5
  require 'crypt_keeper/helper'
6
- require 'crypt_keeper/provider/aes'
6
+ require 'crypt_keeper/provider/base'
7
7
  require 'crypt_keeper/provider/aes_new'
8
- require 'crypt_keeper/provider/mysql_aes'
9
8
  require 'crypt_keeper/provider/mysql_aes_new'
9
+ require 'crypt_keeper/provider/postgres_base'
10
10
  require 'crypt_keeper/provider/postgres_pgp'
11
11
  require 'crypt_keeper/provider/postgres_pgp_public_key'
12
12
 
13
13
  module CryptKeeper
14
+ class << self
15
+ attr_accessor :stub_encryption
16
+ alias_method :stub_encryption?, :stub_encryption
17
+
18
+ attr_accessor :silence_logs
19
+ alias_method :silence_logs?, :silence_logs
20
+ end
14
21
  end
22
+
23
+ CryptKeeper.stub_encryption = false
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe CryptKeeper::LogSubscriber::MysqlAes do
4
+ before do
5
+ CryptKeeper.silence_logs = false
6
+ end
7
+
8
+ use_mysql
9
+
10
+ context "AES encryption" do
11
+ # Fire the ActiveSupport.on_load
12
+ before do
13
+ CryptKeeper::Provider::MysqlAesNew.new key: 'secret', salt: 'salt'
14
+ end
15
+
16
+ let(:input_query) do
17
+ "SELECT aes_encrypt('encrypt_value', 'encrypt_key'), aes_decrypt('decrypt_value', 'decrypt_key') FROM DUAL;"
18
+ end
19
+
20
+ let(:output_query) do
21
+ "SELECT aes_encrypt([FILTERED]) FROM DUAL;"
22
+ end
23
+
24
+ let(:input_search_query) do
25
+ "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((aes_decrypt('f'), 'tool') = 'blah')) AND secret = 'testing'"
26
+ end
27
+
28
+ let(:output_search_query) do
29
+ "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((aes_decrypt([FILTERED]) AND secret = 'testing'"
30
+ end
31
+
32
+ it "filters aes functions" do
33
+ should_log_scrubbed_query(input: input_query, output: output_query)
34
+ end
35
+
36
+ it "filters aes functions in lowercase" do
37
+ should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED'))
38
+ end
39
+
40
+ it "filters aes functions when searching" do
41
+ should_log_scrubbed_query(input: input_search_query, output: output_search_query)
42
+ end
43
+
44
+ it "forces string encodings" do
45
+ input_query = "SELECT aes_encrypt('hi \255', 'test') FROM DUAL;"
46
+
47
+ should_log_scrubbed_query(input: input_query, output: output_query)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe CryptKeeper::LogSubscriber::PostgresPgp do
4
+ before do
5
+ CryptKeeper.silence_logs = false
6
+ end
7
+
8
+ use_postgres
9
+
10
+ context "Symmetric encryption" do
11
+ # Fire the ActiveSupport.on_load
12
+ before do
13
+ CryptKeeper::Provider::PostgresPgp.new key: 'secret'
14
+ end
15
+
16
+ let(:queries) {
17
+ {
18
+ "SELECT pgp_sym_encrypt('encrypt_value', 'encrypt_key') FROM DUAL;" => "SELECT pgp_sym_encrypt([FILTERED]) FROM DUAL;",
19
+ "SELECT pgp_sym_decrypt('encrypt_value') FROM DUAL;" => "SELECT pgp_sym_decrypt([FILTERED]) FROM DUAL;",
20
+ "SELECT pgp_key_id('encrypt_value') FROM DUAL;" => "SELECT pgp_key_id([FILTERED]) FROM DUAL;",
21
+ "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((pgp_sym_decrypt('f'), 'tool') = 'blah')) AND secret = 'testing'" => "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE pgp_sym_decrypt([FILTERED]) AND secret = 'testing'",
22
+ }
23
+ }
24
+
25
+ it "filters pgp functions" do
26
+ queries.each do |k, v|
27
+ should_log_scrubbed_query(input: k, output: v)
28
+ end
29
+ end
30
+
31
+ it "filters pgp functions in lowercase" do
32
+ queries.each do |k, v|
33
+ should_log_scrubbed_query(input: k.downcase, output: v.downcase.gsub(/filtered/, 'FILTERED'))
34
+ end
35
+ end
36
+
37
+ it "forces string encodings" do
38
+ queries.each do |k, v|
39
+ k = "#{k}\255"
40
+ should_log_scrubbed_query(input: k, output: v)
41
+ end
42
+ end
43
+ end
44
+
45
+ context "Public key encryption" do
46
+ let(:public_key) do
47
+ IO.read(File.join(SPEC_ROOT, 'fixtures', 'public.asc'))
48
+ end
49
+
50
+ let(:private_key) do
51
+ IO.read(File.join(SPEC_ROOT, 'fixtures', 'private.asc'))
52
+ end
53
+
54
+ # Fire the ActiveSupport.on_load
55
+ before do
56
+ CryptKeeper::Provider::PostgresPgpPublicKey.new key: 'secret', public_key: public_key, private_key: private_key
57
+ end
58
+
59
+ let(:queries) {
60
+ {
61
+ "SELECT pgp_pub_encrypt('test', dearmor('#{public_key}')) FROM DUAL;" => "SELECT pgp_pub_encrypt([FILTERED]) FROM DUAL;",
62
+ "SELECT pgp_pub_decrypt('test', dearmor('#{public_key}'), '#{private_key}') FROM DUAL;" => "SELECT pgp_pub_decrypt([FILTERED]) FROM DUAL;",
63
+ }
64
+ }
65
+
66
+ it "filters pgp functions" do
67
+ queries.each do |k, v|
68
+ should_log_scrubbed_query(input: k, output: v)
69
+ end
70
+ end
71
+
72
+ it "filters pgp functions in lowercase" do
73
+ queries.each do |k, v|
74
+ should_log_scrubbed_query(input: k.downcase, output: v.downcase.gsub(/filtered/, 'FILTERED'))
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,178 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe CryptKeeper::Model do
6
+ use_sqlite
7
+
8
+ subject { create_model }
9
+
10
+ after do
11
+ CryptKeeper.stub_encryption = false
12
+ end
13
+
14
+ describe "#crypt_keeper" do
15
+ context "Fields" do
16
+ it "enables encryption for the given fields" do
17
+ subject.crypt_keeper :storage, :secret, encryptor: :fake_encryptor
18
+ expect(subject.crypt_keeper_fields).to eq([:storage, :secret])
19
+ end
20
+
21
+ it "raises an exception for missing field" do
22
+ msg = "Column :none does not exist"
23
+ subject.crypt_keeper :none, encryptor: :fake_encryptor
24
+ expect { subject.new.save }.to raise_error(ArgumentError, msg)
25
+ end
26
+
27
+ it "allows binary as a valid type" do
28
+ subject.crypt_keeper :storage, encryptor: :fake_encryptor
29
+ allow(subject.columns_hash['storage']).to receive(:type).and_return(:binary)
30
+ expect(subject.new.save).to be_truthy
31
+ end
32
+
33
+ it "raises an exception for non text or binary fields" do
34
+ msg = "Column :name must be of type 'text' or 'binary' to be used for encryption"
35
+ subject.crypt_keeper :name, encryptor: :fake_encryptor
36
+ expect { subject.new.save }.to raise_error(ArgumentError, msg)
37
+ end
38
+ end
39
+
40
+ context "Options" do
41
+ it "accepts the class name as a string" do
42
+ subject.crypt_keeper :storage, :secret, key1: 1, key2: 2, encryptor: "FakeEncryptor"
43
+ expect(subject.send(:encryptor_klass)).to eq(CryptKeeper::Provider::FakeEncryptor)
44
+ end
45
+
46
+ it "raises an error on missing encryptor" do
47
+ expect { subject.crypt_keeper :storage, :secret }.
48
+ to raise_error(ArgumentError, /You must specify a valid encryptor/)
49
+ end
50
+
51
+ it "raises an error on encryptor without base" do
52
+ expect { subject.crypt_keeper :storage, encryptor: "InvalidEncryptor" }.
53
+ to raise_error(ArgumentError, /You must specify a valid encryptor/)
54
+ end
55
+ end
56
+ end
57
+
58
+ context "Encryption and Decryption" do
59
+ let(:plain_text) { 'plain_text' }
60
+ let(:cipher_text) { 'tooltxet_nialp' }
61
+
62
+ subject { create_encrypted_model :storage, passphrase: 'tool', encryptor: :encryptor }
63
+
64
+ it "encrypts the data" do
65
+ expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:encrypt).with('testing')
66
+ subject.create!(storage: 'testing')
67
+ end
68
+
69
+ it "decrypts the data" do
70
+ record = subject.create!(storage: 'testing')
71
+ expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:decrypt).at_least(1).times.with('toolgnitset')
72
+ subject.find(record.id).storage
73
+ end
74
+
75
+ it "returns the plaintext on decrypt" do
76
+ record = subject.create!(storage: 'testing')
77
+ expect(subject.find(record.id).storage).to eq('testing')
78
+ end
79
+
80
+ it "does not encrypt or decrypt nil" do
81
+ data = subject.create!(storage: nil)
82
+ expect(data.storage).to be_nil
83
+ end
84
+
85
+ it "does not encrypt or decrypt empty strings" do
86
+ data = subject.create!(storage: "")
87
+ expect(data.storage).to be_empty
88
+ end
89
+
90
+ it "converts numbers to strings" do
91
+ data = subject.create!(storage: 1)
92
+ expect(data.reload.storage).to eq("1")
93
+ end
94
+
95
+ it "does not decrypt when stubbing is enabled" do
96
+ CryptKeeper.stub_encryption = true
97
+ record = subject.create!(storage: "testing")
98
+ expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt)
99
+ subject.find(record.id).storage
100
+ end
101
+
102
+ it "does not decrypt when stubbing is enabled after model is created" do
103
+ record = subject.create!(storage: "testing")
104
+ CryptKeeper.stub_encryption = true
105
+ expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt)
106
+ subject.find(record.id).storage
107
+ end
108
+ end
109
+
110
+ context "Search" do
111
+ subject { create_encrypted_model :storage, passphrase: 'tool', encryptor: :search_encryptor }
112
+
113
+ it "searches if supported" do
114
+ expect { subject.search_by_plaintext(:storage, 'test1') }.to_not raise_error
115
+ end
116
+
117
+ it "complains about bad columns" do
118
+ expect { subject.search_by_plaintext(:what, 'test1') }.to raise_error(/what is not a crypt_keeper field/)
119
+ end
120
+ end
121
+
122
+ context "Encodings" do
123
+ subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' }
124
+
125
+ it "forces the encoding on decrypt" do
126
+ record = subject.create!(storage: 'Tromsø')
127
+ record.reload
128
+ expect(record.storage).to eql('Tromsø')
129
+ end
130
+
131
+ it "converts from other encodings" do
132
+ plaintext = "\xC2\xA92011 AACR".force_encoding('ASCII-8BIT')
133
+ record = subject.create!(storage: plaintext)
134
+ record.reload
135
+ expect(record.storage.encoding.name).to eql('UTF-8')
136
+ end
137
+ end
138
+
139
+ context "Initial Table Encryption" do
140
+ subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new }
141
+
142
+ before do
143
+ subject.delete_all
144
+ c = create_model
145
+ 5.times { |i| c.create! storage: "testing#{i}" }
146
+ end
147
+
148
+ it "encrypts the table" do
149
+ expect { subject.first(5).map(&:storage) }.to raise_error(OpenSSL::Cipher::CipherError)
150
+ subject.encrypt_table!
151
+ expect { subject.first(5).map(&:storage) }.not_to raise_error
152
+ end
153
+ end
154
+
155
+ context "Table Decryption (Reverse of Initial Table Encryption)" do
156
+ subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new }
157
+ let!(:storage_entries) { 5.times.map { |i| "testing#{i}" } }
158
+
159
+ before do
160
+ subject.delete_all
161
+ storage_entries.each { |entry| subject.create! storage: entry}
162
+ end
163
+
164
+ it "decrypts the table" do
165
+ subject.decrypt_table!
166
+ expect( create_model.first(5).map(&:storage) ).to eq( storage_entries )
167
+ end
168
+ end
169
+
170
+ context "Missing Attributes" do
171
+ subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' }
172
+
173
+ it "doesn't attempt decryption of missing attributes" do
174
+ subject.create!(storage: 'blah')
175
+ expect { subject.select(:id).first }.to_not raise_error
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe CryptKeeper::Provider::AesNew do
4
+ subject { described_class.new(key: 'cake', salt: 'salt') }
5
+
6
+ describe "#initialize" do
7
+ let(:digested_key) do
8
+ ::Armor.digest('cake', 'salt')
9
+ end
10
+
11
+ specify { expect(subject.key).to eq(digested_key) }
12
+ specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") }
13
+ end
14
+
15
+ describe "#encrypt" do
16
+ let(:encrypted) do
17
+ subject.encrypt 'string'
18
+ end
19
+
20
+ specify { expect(encrypted).to_not eq('string') }
21
+ specify { expect(encrypted).to_not be_blank }
22
+ end
23
+
24
+ describe "#decrypt" do
25
+ let(:decrypted) do
26
+ subject.decrypt "V02ebRU2wLk25AizasROVg==$kE+IpRaUNdBfYqR+WjMqvA=="
27
+ end
28
+
29
+ specify { expect(decrypted).to eq('string') }
30
+ end
31
+
32
+ describe "#search" do
33
+ let(:records) do
34
+ [{ name: 'Bob' }, { name: 'Tim' }]
35
+ end
36
+
37
+ it "finds the matching record" do
38
+ expect(subject.search(records, :name, 'Bob')).to eql([records.first])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe CryptKeeper::Provider::MysqlAesNew do
4
+ use_mysql
5
+
6
+ let(:plain_text) { 'test' }
7
+
8
+ # MySQL stores AES encrypted strings in binary which you can't paste
9
+ # into a spec :). This is a Base64 encoded string of 'test' AES encrypted
10
+ # by AES_ENCRYPT()
11
+ let(:cipher_text) do
12
+ "fBN8i7bx/DGAA4NJ4EWi0A=="
13
+ end
14
+
15
+ subject { described_class.new key: ENCRYPTION_PASSWORD, salt: 'salt' }
16
+
17
+ specify { expect(subject.key).to eq("825e8c5e8ca394818b307b22b8cb7d3df2735e9c1e5838b476e7719135a4f499f2133022c1a0e8597c9ac1507b0f0c44328a40049f9704fab3598c5dec120724") }
18
+
19
+ describe "#initialize" do
20
+ specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") }
21
+ specify { expect { described_class.new(key: 'blah') }.to raise_error(ArgumentError, "Missing :salt") }
22
+ end
23
+
24
+ describe "#encrypt" do
25
+ specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) }
26
+ specify { expect(subject.encrypt(plain_text)).to_not be_blank }
27
+ end
28
+
29
+ describe "#decrypt" do
30
+ specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) }
31
+ end
32
+
33
+ describe "#search" do
34
+ subject { mysql_model }
35
+
36
+ it "finds the matching record" do
37
+ subject.create!(storage: 'blah2')
38
+ match = subject.create!(storage: 'blah')
39
+ expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match)
40
+ end
41
+
42
+ it "keeps the scope" do
43
+ subject.create!(storage: 'blah')
44
+ subject.create!(storage: 'blah')
45
+
46
+ scope = subject.limit(1)
47
+ expect(scope.search_by_plaintext(:storage, 'blah').count).to eq(1)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe CryptKeeper::Provider::PostgresPgpPublicKey do
4
+ use_postgres
5
+
6
+ let(:cipher_text) { '\xc1c04c036c401ad086beb9e3010800987d6c4ccd974322190caa75a3a01aba37bc1970182c4c1d3faec98edf186780520f0586101f286e0626096a1eca91a229ed4d4058a6913a8d13cdf49f29ea44e2b96d10347f9b1b860bb3c959f000a3b1b415a95d2cd07af8c74aa6df8cd10ab06b6a6f7db69cdf3185466d68c5b66b95b813acdfb3ddfb021cac92e0967d67e90df73332f27970c1d2b9a56ac74f602d4107b163ed73ef89fca560d9a0a0d2bc7a74005f29fa27babfbaf950ac07b1c809049db4ab126be4824cf76416c278571f7064f638edf830a1ae5ee1ab544d35fce0f974f21b9dcbbea3986077d27b0de34144dc23f369f471090b57e067a056901e680493ddf2a6b29e4af3462387d235010259556079d07daa249b6703e2bc79345da556cfb46f228cad40a8a5b569ac46f08865f9176acf89129a3e0ceb2a7b1991012f65' }
7
+
8
+ let(:integer_cipher_text) { '\xc1c04c036c401ad086beb9e30107ff59e674ba05958eb053c2427b44355e0f333f1726e18a0b851130130510c648f580b13b3f6a223eb26e397008596867c5a511a4f5bfbf2ecc852d8929814480d63166e525fa2b259b6a8d4474b5b1373b4e1a4fe70a491d25442e1c0046fd3d69466ad30153c8d8d920e9b4260d4e4e421ef3ead162b3aba5d85408c4ef9f9d342b5655c7568d1bdc61c27ddb419133bf091f22f42e7bc91ec6d279b7b25b87ea65119568b85ae81079dd0a6a7258b58fb219c6cc4580f33cb46de97770a1eb0880bdf87426fd0529576a1e791e521d9b3c426e393e63d83321f319b00f9dc4027ea5a81dd57c0f5ba868fb86d73179c34f2287c437266e8becc072b45a929562d2320194be54464e03854635d0f7d7fb10813adbc6efe51efa9095a9bacc2a03fb5c41d1c1896384e4f36b100c0f00e81d4cff7d' }
9
+
10
+ let(:integer_plain_text) { 1 }
11
+ let(:plain_text) { 'test' }
12
+
13
+ let(:public_key) do
14
+ IO.read(File.join(SPEC_ROOT, 'fixtures', 'public.asc'))
15
+ end
16
+
17
+ let(:private_key) do
18
+ IO.read(File.join(SPEC_ROOT, 'fixtures', 'private.asc'))
19
+ end
20
+
21
+ subject { described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key, private_key: private_key }
22
+
23
+
24
+ specify { expect(subject.key).to eq(ENCRYPTION_PASSWORD) }
25
+
26
+ describe "#initialize" do
27
+ specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") }
28
+ end
29
+
30
+ describe "#encrypt" do
31
+ context "Strings" do
32
+ specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) }
33
+ specify { expect(subject.encrypt(plain_text)).to_not be_empty }
34
+
35
+ it "does not double encrypt" do
36
+ pgp = described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key
37
+ expect(pgp.encrypt(cipher_text)).to eq(cipher_text)
38
+ end
39
+ end
40
+
41
+ context "Integers" do
42
+ specify { expect(subject.encrypt(integer_plain_text)).to_not eq(integer_plain_text) }
43
+ specify { expect(subject.encrypt(integer_plain_text)).to_not be_empty }
44
+ end
45
+ end
46
+
47
+ describe "#decrypt" do
48
+ specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) }
49
+ specify { expect(subject.decrypt(integer_cipher_text)).to eq(integer_plain_text.to_s) }
50
+ specify { expect(subject.decrypt(plain_text)).to eq(plain_text) }
51
+
52
+ it "does not decrypt w/o private key" do
53
+ pgp = described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key
54
+ expect(pgp.decrypt(cipher_text)).to eq(cipher_text)
55
+ end
56
+ end
57
+
58
+ describe "#encrypted?" do
59
+ it "returns true for encrypted strings" do
60
+ expect(subject.encrypted?(cipher_text)).to be_truthy
61
+ end
62
+
63
+ it "returns false for non-encrypted strings" do
64
+ expect(subject.encrypted?(plain_text)).to be_falsey
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe CryptKeeper::Provider::PostgresPgp do
4
+ use_postgres
5
+
6
+ let(:cipher_text) { '\xc30d04070302f1a092093988b26873d235017203ce086a53fce1925dc39b4e972e534f192d10b94af3dcf8589abc1f828456f5d3e20b225d56006ffd1e312e3b8a492a6010e9' }
7
+ let(:plain_text) { 'test' }
8
+
9
+ let(:integer_cipher_text) { '\xc30d04070302c8d266353bcf2fc07dd23201153f9d9c32fbb3c36b9b0db137bf8b6c609172210d89ded63f11dff23d1ddbf5111c0266549dde26175c4425e06bb4bd6f' }
10
+
11
+ let(:integer_plain_text) { 1 }
12
+
13
+ subject { described_class.new key: ENCRYPTION_PASSWORD }
14
+
15
+ specify { expect(subject.key).to eq(ENCRYPTION_PASSWORD) }
16
+
17
+ describe "#initialize" do
18
+ specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") }
19
+ end
20
+
21
+ describe "#encrypt" do
22
+ context "Strings" do
23
+ specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) }
24
+ specify { expect(subject.encrypt(plain_text)).to_not be_empty }
25
+ end
26
+
27
+ context "Integers" do
28
+ specify { expect(subject.encrypt(integer_plain_text)).to_not eq(integer_plain_text) }
29
+ specify { expect(subject.encrypt(integer_plain_text)).to_not be_empty }
30
+ end
31
+
32
+ it "filters StatementInvalid errors" do
33
+ subject.pgcrypto_options = "invalid"
34
+
35
+ begin
36
+ subject.encrypt(plain_text)
37
+ rescue ActiveRecord::StatementInvalid => e
38
+ expect(e.message).to_not include("invalid")
39
+ expect(e.message).to_not include(ENCRYPTION_PASSWORD)
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "#decrypt" do
45
+ specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) }
46
+ specify { expect(subject.decrypt(integer_cipher_text)).to eq(integer_plain_text.to_s) }
47
+ specify { expect(subject.decrypt(plain_text)).to eq(plain_text) }
48
+
49
+ it "filters StatementInvalid errors" do
50
+ begin
51
+ subject.decrypt("invalid")
52
+ rescue ActiveRecord::StatementInvalid => e
53
+ expect(e.message).to_not include("invalid")
54
+ expect(e.message).to_not include(ENCRYPTION_PASSWORD)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "#search" do
60
+ subject { postgres_model }
61
+
62
+ it "finds the matching record" do
63
+ subject.create!(storage: 'blah2')
64
+ match = subject.create!(storage: 'blah')
65
+ expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match)
66
+ end
67
+ end
68
+
69
+ describe "Custom pgcrypto options" do
70
+ let(:pgcrypto_options) { 'compress-level=0' }
71
+
72
+ subject { described_class.new key: 'candy', pgcrypto_options: pgcrypto_options }
73
+
74
+ it "reads and writes" do
75
+ queries = logged_queries do
76
+ encrypted = subject.encrypt(plain_text)
77
+ expect(subject.decrypt(encrypted)).to eq(plain_text)
78
+ end
79
+
80
+ expect(queries).to_not be_empty
81
+
82
+ queries.select { |query| query.include?("pgp_sym_encrypt") }.each do |q|
83
+ expect(q).to include(pgcrypto_options)
84
+ end
85
+ end
86
+ end
87
+ end