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.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.travis.yml +22 -13
- data/Appraisals +6 -16
- data/README.md +43 -21
- data/Rakefile +7 -3
- data/crypt_keeper.gemspec +9 -9
- data/gemfiles/activerecord_4_2.gemfile +8 -0
- data/gemfiles/activerecord_5_1.gemfile +8 -0
- data/lib/crypt_keeper/helper.rb +31 -21
- data/lib/crypt_keeper/log_subscriber/mysql_aes.rb +11 -11
- data/lib/crypt_keeper/log_subscriber/postgres_pgp.rb +29 -10
- data/lib/crypt_keeper/model.rb +64 -17
- data/lib/crypt_keeper/provider/aes_new.rb +1 -1
- data/lib/crypt_keeper/provider/base.rb +21 -0
- data/lib/crypt_keeper/provider/mysql_aes_new.rb +2 -2
- data/lib/crypt_keeper/provider/postgres_base.rb +28 -0
- data/lib/crypt_keeper/provider/postgres_pgp.rb +27 -8
- data/lib/crypt_keeper/provider/postgres_pgp_public_key.rb +4 -20
- data/lib/crypt_keeper/version.rb +1 -1
- data/lib/crypt_keeper.rb +11 -2
- data/spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb +50 -0
- data/spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb +78 -0
- data/spec/crypt_keeper/model_spec.rb +178 -0
- data/spec/crypt_keeper/provider/aes_new_spec.rb +41 -0
- data/spec/crypt_keeper/provider/mysql_aes_new_spec.rb +50 -0
- data/spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb +67 -0
- data/spec/crypt_keeper/provider/postgres_pgp_spec.rb +87 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/active_record.rb +36 -26
- data/spec/support/encryptors.rb +9 -3
- data/spec/support/logging.rb +89 -0
- metadata +84 -92
- data/bin/crypt_keeper +0 -99
- data/gemfiles/activerecord_3_1.gemfile +0 -8
- data/gemfiles/activerecord_3_1.gemfile.lock +0 -109
- data/gemfiles/activerecord_3_2.gemfile +0 -8
- data/gemfiles/activerecord_3_2.gemfile.lock +0 -109
- data/gemfiles/activerecord_4_0.gemfile +0 -8
- data/gemfiles/activerecord_4_0.gemfile.lock +0 -117
- data/gemfiles/activerecord_4_1.gemfile +0 -8
- data/gemfiles/activerecord_4_1.gemfile.lock +0 -117
- data/lib/crypt_keeper/provider/aes.rb +0 -66
- data/lib/crypt_keeper/provider/mysql_aes.rb +0 -47
- data/spec/log_subscriber/postgres_pgp_spec.rb +0 -98
- data/spec/model_spec.rb +0 -95
- data/spec/provider/aes_new_spec.rb +0 -45
- data/spec/provider/aes_spec.rb +0 -67
- data/spec/provider/mysql_aes_new_spec.rb +0 -52
- data/spec/provider/mysql_aes_spec.rb +0 -35
- data/spec/provider/postgres_pgp_public_key_spec.rb +0 -70
- 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
|
-
|
|
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
|
-
|
|
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), ?) = ?)",
|
|
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(?), ?)",
|
|
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
|
data/lib/crypt_keeper/version.rb
CHANGED
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/
|
|
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
|