diff --git a/spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb b/spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb new file mode 100644 index 0000000..bc6a543 --- /dev/null +++ b/spec/crypt_keeper/log_subscriber/mysql_aes_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe CryptKeeper::LogSubscriber::MysqlAes do + before do + CryptKeeper.silence_logs = false + end + + use_mysql + + context "AES encryption" do + # Fire the ActiveSupport.on_load + before do + CryptKeeper::Provider::MysqlAesNew.new key: 'secret', salt: 'salt' + end + + let(:input_query) do + "SELECT aes_encrypt('encrypt_value', 'encrypt_key'), aes_decrypt('decrypt_value', 'decrypt_key') FROM DUAL;" + end + + let(:output_query) do + "SELECT aes_encrypt([FILTERED]) FROM DUAL;" + end + + let(:input_search_query) do + "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((aes_decrypt('f'), 'tool') = 'blah')) AND secret = 'testing'" + end + + let(:output_search_query) do + "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((aes_decrypt([FILTERED]) AND secret = 'testing'" + end + + it "filters aes functions" do + should_log_scrubbed_query(input: input_query, output: output_query) + end + + it "filters aes functions in lowercase" do + should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED')) + end + + it "filters aes functions when searching" do + should_log_scrubbed_query(input: input_search_query, output: output_search_query) + end + + it "forces string encodings" do + input_query = "SELECT aes_encrypt('hi \255', 'test') FROM DUAL;" + + should_log_scrubbed_query(input: input_query, output: output_query) + end + + it "skips logging if CryptKeeper.silence_logs is set" do + CryptKeeper.silence_logs = true + + should_not_log_query(input_query) + end + end +end diff --git a/spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb b/spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb new file mode 100644 index 0000000..0b22e7b --- /dev/null +++ b/spec/crypt_keeper/log_subscriber/postgres_pgp_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe CryptKeeper::LogSubscriber::PostgresPgp do + before do + CryptKeeper.silence_logs = false + end + + use_postgres + + context "Symmetric encryption" do + # Fire the ActiveSupport.on_load + before do + CryptKeeper::Provider::PostgresPgp.new key: 'secret' + end + + let(:input_query) do + "SELECT pgp_sym_encrypt('encrypt_value', 'encrypt_key'), pgp_sym_decrypt('decrypt_value', 'decrypt_key') FROM DUAL;" + end + + let(:output_query) do + "SELECT encrypt([FILTERED]) FROM DUAL;" + end + + let(:input_search_query) do + "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((pgp_sym_decrypt('f'), 'tool') = 'blah')) AND secret = 'testing'" + end + + let(:output_search_query) do + "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE decrypt([FILTERED]) AND secret = 'testing'" + end + + it "filters pgp functions" do + should_log_scrubbed_query(input: input_query, output: output_query) + end + + it "filters pgp functions in lowercase" do + should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED')) + end + + it "filters pgp functions when searching" do + should_log_scrubbed_query(input: input_search_query, output: output_search_query) + end + + it "forces string encodings" do + input_query = "SELECT pgp_sym_encrypt('hi \255', 'test') FROM DUAL;" + + should_log_scrubbed_query(input: input_query, output: output_query) + end + + it "skips logging if CryptKeeper.silence_logs is set" do + CryptKeeper.silence_logs = true + + should_not_log_query(input_query) + end + end + + context "Public key encryption" do + let(:public_key) do + IO.read(File.join(SPEC_ROOT, 'fixtures', 'public.asc')) + end + + let(:private_key) do + IO.read(File.join(SPEC_ROOT, 'fixtures', 'private.asc')) + end + + # Fire the ActiveSupport.on_load + before do + CryptKeeper::Provider::PostgresPgpPublicKey.new key: 'secret', public_key: public_key, private_key: private_key + end + + let(:input_query) do + "SELECT pgp_pub_encrypt('test', dearmor('#{public_key} + '))" + end + + let(:output_query) do + "SELECT encrypt([FILTERED])" + end + + it "filters pgp functions" do + should_log_scrubbed_query(input: input_query, output: output_query) + end + + it "filters pgp functions in lowercase" do + should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED')) + end + + it "skips logging if CryptKeeper.silence_logs is set" do + CryptKeeper.silence_logs = true + + should_not_log_query(input_query) + end + end +end diff --git a/spec/crypt_keeper/model_spec.rb b/spec/crypt_keeper/model_spec.rb new file mode 100644 index 0000000..eac2806 --- /dev/null +++ b/spec/crypt_keeper/model_spec.rb @@ -0,0 +1,167 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe CryptKeeper::Model do + use_sqlite + + subject { create_model } + + after do + CryptKeeper.stub_encryption = false + end + + describe "#crypt_keeper" do + context "Fields" do + it "enables encryption for the given fields" do + subject.crypt_keeper :storage, :secret, encryptor: :fake_encryptor + expect(subject.crypt_keeper_fields).to eq([:storage, :secret]) + end + + it "raises an exception for missing field" do + msg = "Column :none does not exist" + subject.crypt_keeper :none, encryptor: :fake_encryptor + expect { subject.new.save }.to raise_error(ArgumentError, msg) + end + + it "raises an exception for non text fields" do + msg = "Column :name must be of type 'text' to be used for encryption" + subject.crypt_keeper :name, encryptor: :fake_encryptor + expect { subject.new.save }.to raise_error(ArgumentError, msg) + end + end + + context "Options" do + it "accepts the class name as a string" do + subject.crypt_keeper :storage, :secret, key1: 1, key2: 2, encryptor: "FakeEncryptor" + expect(subject.send(:encryptor_klass)).to eq(CryptKeeper::Provider::FakeEncryptor) + end + + it "raises an error on missing encryptor" do + expect { subject.crypt_keeper :storage, :secret }. + to raise_error(RuntimeError, /You must specify a valid encryptor/) + end + end + end + + context "Encryption and Decryption" do + let(:plain_text) { 'plain_text' } + let(:cipher_text) { 'tooltxet_nialp' } + + subject { create_encrypted_model :storage, passphrase: 'tool', encryptor: :encryptor } + + it "encrypts the data" do + expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:encrypt).with('testing') + subject.create!(storage: 'testing') + end + + it "decrypts the data" do + record = subject.create!(storage: 'testing') + expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:decrypt).at_least(1).times.with('toolgnitset') + subject.find(record.id).storage + end + + it "returns the plaintext on decrypt" do + record = subject.create!(storage: 'testing') + expect(subject.find(record.id).storage).to eq('testing') + end + + it "does not encrypt or decrypt nil" do + data = subject.create!(storage: nil) + expect(data.storage).to be_nil + end + + it "does not encrypt or decrypt empty strings" do + data = subject.create!(storage: "") + expect(data.storage).to be_empty + end + + it "converts numbers to strings" do + data = subject.create!(storage: 1) + expect(data.reload.storage).to eq("1") + end + + it "does not decrypt when stubbing is enabled" do + CryptKeeper.stub_encryption = true + record = subject.create!(storage: "testing") + expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt) + subject.find(record.id).storage + end + + it "does not decrypt when stubbing is enabled after model is created" do + record = subject.create!(storage: "testing") + CryptKeeper.stub_encryption = true + expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt) + subject.find(record.id).storage + end + end + + context "Search" do + subject { create_encrypted_model :storage, passphrase: 'tool', encryptor: :search_encryptor } + + it "searches if supported" do + expect { subject.search_by_plaintext(:storage, 'test1') }.to_not raise_error + end + + it "complains about bad columns" do + expect { subject.search_by_plaintext(:what, 'test1') }.to raise_error(/what is not a crypt_keeper field/) + end + end + + context "Encodings" do + subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' } + + it "forces the encoding on decrypt" do + record = subject.create!(storage: 'Tromsø') + record.reload + expect(record.storage).to eql('Tromsø') + end + + it "converts from other encodings" do + plaintext = "\xC2\xA92011 AACR".force_encoding('ASCII-8BIT') + record = subject.create!(storage: plaintext) + record.reload + expect(record.storage.encoding.name).to eql('UTF-8') + end + end + + context "Initial Table Encryption" do + subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new } + + before do + subject.delete_all + c = create_model + 5.times { |i| c.create! storage: "testing#{i}" } + end + + it "encrypts the table" do + expect { subject.first(5).map(&:storage) }.to raise_error(OpenSSL::Cipher::CipherError) + subject.encrypt_table! + expect { subject.first(5).map(&:storage) }.not_to raise_error + end + end + + context "Table Decryption (Reverse of Initial Table Encryption)" do + subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new } + let!(:storage_entries) { 5.times.map { |i| "testing#{i}" } } + + before do + subject.delete_all + storage_entries.each { |entry| subject.create! storage: entry} + end + + it "decrypts the table" do + subject.decrypt_table! + expect( create_model.first(5).map(&:storage) ).to eq( storage_entries ) + end + end + + context "Missing Attributes" do + subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' } + + it "doesn't attempt decryption of missing attributes" do + subject.create!(storage: 'blah') + expect { subject.select(:id).first }.to_not raise_error + end + end +end diff --git a/spec/crypt_keeper/provider/aes_new_spec.rb b/spec/crypt_keeper/provider/aes_new_spec.rb new file mode 100644 index 0000000..5b09bc4 --- /dev/null +++ b/spec/crypt_keeper/provider/aes_new_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe CryptKeeper::Provider::AesNew do + subject { described_class.new(key: 'cake', salt: 'salt') } + + describe "#initialize" do + let(:digested_key) do + ::Armor.digest('cake', 'salt') + end + + specify { expect(subject.key).to eq(digested_key) } + specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } + end + + describe "#encrypt" do + let(:encrypted) do + subject.encrypt 'string' + end + + specify { expect(encrypted).to_not eq('string') } + specify { expect(encrypted).to_not be_blank } + end + + describe "#decrypt" do + let(:decrypted) do + subject.decrypt "V02ebRU2wLk25AizasROVg==$kE+IpRaUNdBfYqR+WjMqvA==" + end + + specify { expect(decrypted).to eq('string') } + end + + describe "#search" do + let(:records) do + [{ name: 'Bob' }, { name: 'Tim' }] + end + + it "finds the matching record" do + expect(subject.search(records, :name, 'Bob')).to eql([records.first]) + end + end +end diff --git a/spec/crypt_keeper/provider/mysql_aes_new_spec.rb b/spec/crypt_keeper/provider/mysql_aes_new_spec.rb new file mode 100644 index 0000000..1c9e6d2 --- /dev/null +++ b/spec/crypt_keeper/provider/mysql_aes_new_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe CryptKeeper::Provider::MysqlAesNew do + use_mysql + + let(:plain_text) { 'test' } + + # MySQL stores AES encrypted strings in binary which you can't paste + # into a spec :). This is a Base64 encoded string of 'test' AES encrypted + # by AES_ENCRYPT() + let(:cipher_text) do + "fBN8i7bx/DGAA4NJ4EWi0A==" + end + + subject { described_class.new key: ENCRYPTION_PASSWORD, salt: 'salt' } + + specify { expect(subject.key).to eq("825e8c5e8ca394818b307b22b8cb7d3df2735e9c1e5838b476e7719135a4f499f2133022c1a0e8597c9ac1507b0f0c44328a40049f9704fab3598c5dec120724") } + + describe "#initialize" do + specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } + specify { expect { described_class.new(key: 'blah') }.to raise_error(ArgumentError, "Missing :salt") } + end + + describe "#encrypt" do + specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) } + specify { expect(subject.encrypt(plain_text)).to_not be_blank } + end + + describe "#decrypt" do + specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) } + end + + describe "#search" do + subject { mysql_model } + + it "finds the matching record" do + subject.create!(storage: 'blah2') + match = subject.create!(storage: 'blah') + expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match) + end + + it "keeps the scope" do + subject.create!(storage: 'blah') + subject.create!(storage: 'blah') + + scope = subject.limit(1) + expect(scope.search_by_plaintext(:storage, 'blah').count).to eq(1) + end + end +end diff --git a/spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb b/spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb new file mode 100644 index 0000000..63f7313 --- /dev/null +++ b/spec/crypt_keeper/provider/postgres_pgp_public_key_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe CryptKeeper::Provider::PostgresPgpPublicKey do + use_postgres + + let(:cipher_text) { '\xc1c04c036c401ad086beb9e3010800987d6c4ccd974322190caa75a3a01aba37bc1970182c4c1d3faec98edf186780520f0586101f286e0626096a1eca91a229ed4d4058a6913a8d13cdf49f29ea44e2b96d10347f9b1b860bb3c959f000a3b1b415a95d2cd07af8c74aa6df8cd10ab06b6a6f7db69cdf3185466d68c5b66b95b813acdfb3ddfb021cac92e0967d67e90df73332f27970c1d2b9a56ac74f602d4107b163ed73ef89fca560d9a0a0d2bc7a74005f29fa27babfbaf950ac07b1c809049db4ab126be4824cf76416c278571f7064f638edf830a1ae5ee1ab544d35fce0f974f21b9dcbbea3986077d27b0de34144dc23f369f471090b57e067a056901e680493ddf2a6b29e4af3462387d235010259556079d07daa249b6703e2bc79345da556cfb46f228cad40a8a5b569ac46f08865f9176acf89129a3e0ceb2a7b1991012f65' } + + let(:integer_cipher_text) { '\xc1c04c036c401ad086beb9e30107ff59e674ba05958eb053c2427b44355e0f333f1726e18a0b851130130510c648f580b13b3f6a223eb26e397008596867c5a511a4f5bfbf2ecc852d8929814480d63166e525fa2b259b6a8d4474b5b1373b4e1a4fe70a491d25442e1c0046fd3d69466ad30153c8d8d920e9b4260d4e4e421ef3ead162b3aba5d85408c4ef9f9d342b5655c7568d1bdc61c27ddb419133bf091f22f42e7bc91ec6d279b7b25b87ea65119568b85ae81079dd0a6a7258b58fb219c6cc4580f33cb46de97770a1eb0880bdf87426fd0529576a1e791e521d9b3c426e393e63d83321f319b00f9dc4027ea5a81dd57c0f5ba868fb86d73179c34f2287c437266e8becc072b45a929562d2320194be54464e03854635d0f7d7fb10813adbc6efe51efa9095a9bacc2a03fb5c41d1c1896384e4f36b100c0f00e81d4cff7d' } + + let(:integer_plain_text) { 1 } + let(:plain_text) { 'test' } + + let(:public_key) do + IO.read(File.join(SPEC_ROOT, 'fixtures', 'public.asc')) + end + + let(:private_key) do + IO.read(File.join(SPEC_ROOT, 'fixtures', 'private.asc')) + end + + subject { described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key, private_key: private_key } + + + specify { expect(subject.key).to eq(ENCRYPTION_PASSWORD) } + + describe "#initialize" do + specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } + end + + describe "#encrypt" do + context "Strings" do + specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) } + specify { expect(subject.encrypt(plain_text)).to_not be_empty } + + it "does not double encrypt" do + pgp = described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key + expect(pgp.encrypt(cipher_text)).to eq(cipher_text) + end + end + + context "Integers" do + specify { expect(subject.encrypt(integer_plain_text)).to_not eq(integer_plain_text) } + specify { expect(subject.encrypt(integer_plain_text)).to_not be_empty } + end + end + + describe "#decrypt" do + specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) } + specify { expect(subject.decrypt(integer_cipher_text)).to eq(integer_plain_text.to_s) } + + it "does not decrypt w/o private key" do + pgp = described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key + expect(pgp.decrypt(cipher_text)).to eq(cipher_text) + end + end + + describe "#encrypted?" do + it "returns true for encrypted strings" do + expect(subject.encrypted?(cipher_text)).to be_truthy + end + + it "returns false for non-encrypted strings" do + expect(subject.encrypted?(plain_text)).to be_falsey + end + end +end diff --git a/spec/crypt_keeper/provider/postgres_pgp_spec.rb b/spec/crypt_keeper/provider/postgres_pgp_spec.rb new file mode 100644 index 0000000..6cd628d --- /dev/null +++ b/spec/crypt_keeper/provider/postgres_pgp_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe CryptKeeper::Provider::PostgresPgp do + use_postgres + + let(:cipher_text) { '\xc30d04070302f1a092093988b26873d235017203ce086a53fce1925dc39b4e972e534f192d10b94af3dcf8589abc1f828456f5d3e20b225d56006ffd1e312e3b8a492a6010e9' } + let(:plain_text) { 'test' } + + let(:integer_cipher_text) { '\xc30d04070302c8d266353bcf2fc07dd23201153f9d9c32fbb3c36b9b0db137bf8b6c609172210d89ded63f11dff23d1ddbf5111c0266549dde26175c4425e06bb4bd6f' } + + let(:integer_plain_text) { 1 } + + subject { described_class.new key: ENCRYPTION_PASSWORD } + + specify { expect(subject.key).to eq(ENCRYPTION_PASSWORD) } + + describe "#initialize" do + specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } + end + + describe "#encrypt" do + context "Strings" do + specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) } + specify { expect(subject.encrypt(plain_text)).to_not be_empty } + end + + context "Integers" do + specify { expect(subject.encrypt(integer_plain_text)).to_not eq(integer_plain_text) } + specify { expect(subject.encrypt(integer_plain_text)).to_not be_empty } + end + end + + describe "#decrypt" do + specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) } + specify { expect(subject.decrypt(integer_cipher_text)).to eq(integer_plain_text.to_s) } + end + + describe "#search" do + subject { postgres_model } + + it "finds the matching record" do + subject.create!(storage: 'blah2') + match = subject.create!(storage: 'blah') + expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match) + end + end + + describe "Custom pgcrypto options" do + let(:pgcrypto_options) { 'compress-level=0' } + + subject { described_class.new key: 'candy', pgcrypto_options: pgcrypto_options } + + it "reads and writes" do + queries = logged_queries do + encrypted = subject.encrypt(plain_text) + expect(subject.decrypt(encrypted)).to eq(plain_text) + end + + expect(queries).to_not be_empty + + queries.select { |query| query.include?("pgp_sym_encrypt") }.each do |q| + expect(q).to include(pgcrypto_options) + end + end + end +end diff --git a/spec/log_subscriber/mysql_aes_spec.rb b/spec/log_subscriber/mysql_aes_spec.rb deleted file mode 100644 index d5a56f9..0000000 --- a/spec/log_subscriber/mysql_aes_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'spec_helper' - -module CryptKeeper::LogSubscriber - describe MysqlAes do - before do - CryptKeeper.silence_logs = false - end - - use_mysql - - context "AES encryption" do - # Fire the ActiveSupport.on_load - before do - CryptKeeper::Provider::MysqlAesNew.new key: 'secret', salt: 'salt' - end - - let(:input_query) do - "SELECT aes_encrypt('encrypt_value', 'encrypt_key'), aes_decrypt('decrypt_value', 'decrypt_key') FROM DUAL;" - end - - let(:output_query) do - "SELECT aes_encrypt([FILTERED]) FROM DUAL;" - end - - let(:input_search_query) do - "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((aes_decrypt('f'), 'tool') = 'blah')) AND secret = 'testing'" - end - - let(:output_search_query) do - "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((aes_decrypt([FILTERED]) AND secret = 'testing'" - end - - it "filters aes functions" do - should_log_scrubbed_query(input: input_query, output: output_query) - end - - it "filters aes functions in lowercase" do - should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED')) - end - - it "filters aes functions when searching" do - should_log_scrubbed_query(input: input_search_query, output: output_search_query) - end - - it "forces string encodings" do - input_query = "SELECT aes_encrypt('hi \255', 'test') FROM DUAL;" - - should_log_scrubbed_query(input: input_query, output: output_query) - end - - it "skips logging if CryptKeeper.silence_logs is set" do - CryptKeeper.silence_logs = true - - should_not_log_query(input_query) - end - end - end -end diff --git a/spec/log_subscriber/postgres_pgp_spec.rb b/spec/log_subscriber/postgres_pgp_spec.rb deleted file mode 100644 index 1a56e5c..0000000 --- a/spec/log_subscriber/postgres_pgp_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'spec_helper' - -module CryptKeeper::LogSubscriber - describe PostgresPgp do - before do - CryptKeeper.silence_logs = false - end - - use_postgres - - context "Symmetric encryption" do - # Fire the ActiveSupport.on_load - before do - CryptKeeper::Provider::PostgresPgp.new key: 'secret' - end - - let(:input_query) do - "SELECT pgp_sym_encrypt('encrypt_value', 'encrypt_key'), pgp_sym_decrypt('decrypt_value', 'decrypt_key') FROM DUAL;" - end - - let(:output_query) do - "SELECT encrypt([FILTERED]) FROM DUAL;" - end - - let(:input_search_query) do - "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE ((pgp_sym_decrypt('f'), 'tool') = 'blah')) AND secret = 'testing'" - end - - let(:output_search_query) do - "SELECT \"sensitive_data\".* FROM \"sensitive_data\" WHERE decrypt([FILTERED]) AND secret = 'testing'" - end - - it "filters pgp functions" do - should_log_scrubbed_query(input: input_query, output: output_query) - end - - it "filters pgp functions in lowercase" do - should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED')) - end - - it "filters pgp functions when searching" do - should_log_scrubbed_query(input: input_search_query, output: output_search_query) - end - - it "forces string encodings" do - input_query = "SELECT pgp_sym_encrypt('hi \255', 'test') FROM DUAL;" - - should_log_scrubbed_query(input: input_query, output: output_query) - end - - it "skips logging if CryptKeeper.silence_logs is set" do - CryptKeeper.silence_logs = true - - should_not_log_query(input_query) - end - end - - context "Public key encryption" do - let(:public_key) do - IO.read(File.join(SPEC_ROOT, 'fixtures', 'public.asc')) - end - - let(:private_key) do - IO.read(File.join(SPEC_ROOT, 'fixtures', 'private.asc')) - end - - # Fire the ActiveSupport.on_load - before do - CryptKeeper::Provider::PostgresPgpPublicKey.new key: 'secret', public_key: public_key, private_key: private_key - end - - let(:input_query) do - "SELECT pgp_pub_encrypt('test', dearmor('#{public_key} - '))" - end - - let(:output_query) do - "SELECT encrypt([FILTERED])" - end - - it "filters pgp functions" do - should_log_scrubbed_query(input: input_query, output: output_query) - end - - it "filters pgp functions in lowercase" do - should_log_scrubbed_query(input: input_query.downcase, output: output_query.downcase.gsub(/filtered/, 'FILTERED')) - end - - it "skips logging if CryptKeeper.silence_logs is set" do - CryptKeeper.silence_logs = true - - should_not_log_query(input_query) - end - end - end -end diff --git a/spec/model_spec.rb b/spec/model_spec.rb deleted file mode 100644 index 113336f..0000000 --- a/spec/model_spec.rb +++ /dev/null @@ -1,169 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' - -module CryptKeeper - describe Model do - use_sqlite - - subject { create_model } - - after do - CryptKeeper.stub_encryption = false - end - - describe "#crypt_keeper" do - context "Fields" do - it "enables encryption for the given fields" do - subject.crypt_keeper :storage, :secret, encryptor: :fake_encryptor - expect(subject.crypt_keeper_fields).to eq([:storage, :secret]) - end - - it "raises an exception for missing field" do - msg = "Column :none does not exist" - subject.crypt_keeper :none, encryptor: :fake_encryptor - expect { subject.new.save }.to raise_error(ArgumentError, msg) - end - - it "raises an exception for non text fields" do - msg = "Column :name must be of type 'text' to be used for encryption" - subject.crypt_keeper :name, encryptor: :fake_encryptor - expect { subject.new.save }.to raise_error(ArgumentError, msg) - end - end - - context "Options" do - it "accepts the class name as a string" do - subject.crypt_keeper :storage, :secret, key1: 1, key2: 2, encryptor: "FakeEncryptor" - expect(subject.send(:encryptor_klass)).to eq(CryptKeeper::Provider::FakeEncryptor) - end - - it "raises an error on missing encryptor" do - expect { subject.crypt_keeper :storage, :secret }. - to raise_error(RuntimeError, /You must specify a valid encryptor/) - end - end - end - - context "Encryption and Decryption" do - let(:plain_text) { 'plain_text' } - let(:cipher_text) { 'tooltxet_nialp' } - - subject { create_encrypted_model :storage, passphrase: 'tool', encryptor: :encryptor } - - it "encrypts the data" do - expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:encrypt).with('testing') - subject.create!(storage: 'testing') - end - - it "decrypts the data" do - record = subject.create!(storage: 'testing') - expect_any_instance_of(CryptKeeper::Provider::Encryptor).to receive(:decrypt).at_least(1).times.with('toolgnitset') - subject.find(record.id).storage - end - - it "returns the plaintext on decrypt" do - record = subject.create!(storage: 'testing') - expect(subject.find(record.id).storage).to eq('testing') - end - - it "does not encrypt or decrypt nil" do - data = subject.create!(storage: nil) - expect(data.storage).to be_nil - end - - it "does not encrypt or decrypt empty strings" do - data = subject.create!(storage: "") - expect(data.storage).to be_empty - end - - it "converts numbers to strings" do - data = subject.create!(storage: 1) - expect(data.reload.storage).to eq("1") - end - - it "does not decrypt when stubbing is enabled" do - CryptKeeper.stub_encryption = true - record = subject.create!(storage: "testing") - expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt) - subject.find(record.id).storage - end - - it "does not decrypt when stubbing is enabled after model is created" do - record = subject.create!(storage: "testing") - CryptKeeper.stub_encryption = true - expect_any_instance_of(CryptKeeper::Provider::Encryptor).to_not receive(:decrypt) - subject.find(record.id).storage - end - end - - context "Search" do - subject { create_encrypted_model :storage, passphrase: 'tool', encryptor: :search_encryptor } - - it "searches if supported" do - expect { subject.search_by_plaintext(:storage, 'test1') }.to_not raise_error - end - - it "complains about bad columns" do - expect { subject.search_by_plaintext(:what, 'test1') }.to raise_error(/what is not a crypt_keeper field/) - end - end - - context "Encodings" do - subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' } - - it "forces the encoding on decrypt" do - record = subject.create!(storage: 'Tromsø') - record.reload - expect(record.storage).to eql('Tromsø') - end - - it "converts from other encodings" do - plaintext = "\xC2\xA92011 AACR".force_encoding('ASCII-8BIT') - record = subject.create!(storage: plaintext) - record.reload - expect(record.storage.encoding.name).to eql('UTF-8') - end - end - - context "Initial Table Encryption" do - subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new } - - before do - subject.delete_all - c = create_model - 5.times { |i| c.create! storage: "testing#{i}" } - end - - it "encrypts the table" do - expect { subject.first(5).map(&:storage) }.to raise_error(OpenSSL::Cipher::CipherError) - subject.encrypt_table! - expect { subject.first(5).map(&:storage) }.not_to raise_error - end - end - - context "Table Decryption (Reverse of Initial Table Encryption)" do - subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new } - let!(:storage_entries) { 5.times.map { |i| "testing#{i}" } } - - before do - subject.delete_all - storage_entries.each { |entry| subject.create! storage: entry} - end - - it "decrypts the table" do - subject.decrypt_table! - expect( create_model.first(5).map(&:storage) ).to eq( storage_entries ) - end - end - - context "Missing Attributes" do - subject { create_encrypted_model :storage, key: 'tool', salt: 'salt', encryptor: :aes_new, encoding: 'utf-8' } - - it "doesn't attempt decryption of missing attributes" do - subject.create!(storage: 'blah') - expect { subject.select(:id).first }.to_not raise_error - end - end - end -end diff --git a/spec/provider/aes_new_spec.rb b/spec/provider/aes_new_spec.rb deleted file mode 100644 index afae74f..0000000 --- a/spec/provider/aes_new_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'spec_helper' - -module CryptKeeper - module Provider - describe AesNew do - subject { described_class.new(key: 'cake', salt: 'salt') } - - describe "#initialize" do - let(:digested_key) do - ::Armor.digest('cake', 'salt') - end - - specify { expect(subject.key).to eq(digested_key) } - specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } - end - - describe "#encrypt" do - let(:encrypted) do - subject.encrypt 'string' - end - - specify { expect(encrypted).to_not eq('string') } - specify { expect(encrypted).to_not be_blank } - end - - describe "#decrypt" do - let(:decrypted) do - subject.decrypt "V02ebRU2wLk25AizasROVg==$kE+IpRaUNdBfYqR+WjMqvA==" - end - - specify { expect(decrypted).to eq('string') } - end - - describe "#search" do - let(:records) do - [{ name: 'Bob' }, { name: 'Tim' }] - end - - it "finds the matching record" do - expect(subject.search(records, :name, 'Bob')).to eql([records.first]) - end - end - end - end -end diff --git a/spec/provider/mysql_aes_new_spec.rb b/spec/provider/mysql_aes_new_spec.rb deleted file mode 100644 index 9fc31f3..0000000 --- a/spec/provider/mysql_aes_new_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -module CryptKeeper - module Provider - describe MysqlAesNew do - use_mysql - - let(:plain_text) { 'test' } - - # MySQL stores AES encrypted strings in binary which you can't paste - # into a spec :). This is a Base64 encoded string of 'test' AES encrypted - # by AES_ENCRYPT() - let(:cipher_text) do - "fBN8i7bx/DGAA4NJ4EWi0A==" - end - - subject { described_class.new key: ENCRYPTION_PASSWORD, salt: 'salt' } - - specify { expect(subject.key).to eq("825e8c5e8ca394818b307b22b8cb7d3df2735e9c1e5838b476e7719135a4f499f2133022c1a0e8597c9ac1507b0f0c44328a40049f9704fab3598c5dec120724") } - - describe "#initialize" do - specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } - specify { expect { described_class.new(key: 'blah') }.to raise_error(ArgumentError, "Missing :salt") } - end - - describe "#encrypt" do - specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) } - specify { expect(subject.encrypt(plain_text)).to_not be_blank } - end - - describe "#decrypt" do - specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) } - end - - describe "#search" do - subject { mysql_model } - - it "finds the matching record" do - subject.create!(storage: 'blah2') - match = subject.create!(storage: 'blah') - expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match) - end - - it "keeps the scope" do - subject.create!(storage: 'blah') - subject.create!(storage: 'blah') - - scope = subject.limit(1) - expect(scope.search_by_plaintext(:storage, 'blah').count).to eq(1) - end - end - end - end -end diff --git a/spec/provider/postgres_pgp_public_key_spec.rb b/spec/provider/postgres_pgp_public_key_spec.rb deleted file mode 100644 index 2d6958b..0000000 --- a/spec/provider/postgres_pgp_public_key_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -module CryptKeeper - module Provider - describe PostgresPgpPublicKey do - use_postgres - - let(:cipher_text) { '\xc1c04c036c401ad086beb9e3010800987d6c4ccd974322190caa75a3a01aba37bc1970182c4c1d3faec98edf186780520f0586101f286e0626096a1eca91a229ed4d4058a6913a8d13cdf49f29ea44e2b96d10347f9b1b860bb3c959f000a3b1b415a95d2cd07af8c74aa6df8cd10ab06b6a6f7db69cdf3185466d68c5b66b95b813acdfb3ddfb021cac92e0967d67e90df73332f27970c1d2b9a56ac74f602d4107b163ed73ef89fca560d9a0a0d2bc7a74005f29fa27babfbaf950ac07b1c809049db4ab126be4824cf76416c278571f7064f638edf830a1ae5ee1ab544d35fce0f974f21b9dcbbea3986077d27b0de34144dc23f369f471090b57e067a056901e680493ddf2a6b29e4af3462387d235010259556079d07daa249b6703e2bc79345da556cfb46f228cad40a8a5b569ac46f08865f9176acf89129a3e0ceb2a7b1991012f65' } - - let(:integer_cipher_text) { '\xc1c04c036c401ad086beb9e30107ff59e674ba05958eb053c2427b44355e0f333f1726e18a0b851130130510c648f580b13b3f6a223eb26e397008596867c5a511a4f5bfbf2ecc852d8929814480d63166e525fa2b259b6a8d4474b5b1373b4e1a4fe70a491d25442e1c0046fd3d69466ad30153c8d8d920e9b4260d4e4e421ef3ead162b3aba5d85408c4ef9f9d342b5655c7568d1bdc61c27ddb419133bf091f22f42e7bc91ec6d279b7b25b87ea65119568b85ae81079dd0a6a7258b58fb219c6cc4580f33cb46de97770a1eb0880bdf87426fd0529576a1e791e521d9b3c426e393e63d83321f319b00f9dc4027ea5a81dd57c0f5ba868fb86d73179c34f2287c437266e8becc072b45a929562d2320194be54464e03854635d0f7d7fb10813adbc6efe51efa9095a9bacc2a03fb5c41d1c1896384e4f36b100c0f00e81d4cff7d' } - - let(:integer_plain_text) { 1 } - let(:plain_text) { 'test' } - - let(:public_key) do - IO.read(File.join(SPEC_ROOT, 'fixtures', 'public.asc')) - end - - let(:private_key) do - IO.read(File.join(SPEC_ROOT, 'fixtures', 'private.asc')) - end - - subject { described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key, private_key: private_key } - - - specify { expect(subject.key).to eq(ENCRYPTION_PASSWORD) } - - describe "#initialize" do - specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } - end - - describe "#encrypt" do - context "Strings" do - specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) } - specify { expect(subject.encrypt(plain_text)).to_not be_empty } - - it "does not double encrypt" do - pgp = described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key - expect(pgp.encrypt(cipher_text)).to eq(cipher_text) - end - end - - context "Integers" do - specify { expect(subject.encrypt(integer_plain_text)).to_not eq(integer_plain_text) } - specify { expect(subject.encrypt(integer_plain_text)).to_not be_empty } - end - end - - describe "#decrypt" do - specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) } - specify { expect(subject.decrypt(integer_cipher_text)).to eq(integer_plain_text.to_s) } - - it "does not decrypt w/o private key" do - pgp = described_class.new key: ENCRYPTION_PASSWORD, public_key: public_key - expect(pgp.decrypt(cipher_text)).to eq(cipher_text) - end - end - - describe "#encrypted?" do - it "returns true for encrypted strings" do - expect(subject.encrypted?(cipher_text)).to be_truthy - end - - it "returns false for non-encrypted strings" do - expect(subject.encrypted?(plain_text)).to be_falsey - end - end - end - end -end diff --git a/spec/provider/postgres_pgp_spec.rb b/spec/provider/postgres_pgp_spec.rb deleted file mode 100644 index 930ce0e..0000000 --- a/spec/provider/postgres_pgp_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -module CryptKeeper - module Provider - describe PostgresPgp do - use_postgres - - let(:cipher_text) { '\xc30d04070302f1a092093988b26873d235017203ce086a53fce1925dc39b4e972e534f192d10b94af3dcf8589abc1f828456f5d3e20b225d56006ffd1e312e3b8a492a6010e9' } - let(:plain_text) { 'test' } - - let(:integer_cipher_text) { '\xc30d04070302c8d266353bcf2fc07dd23201153f9d9c32fbb3c36b9b0db137bf8b6c609172210d89ded63f11dff23d1ddbf5111c0266549dde26175c4425e06bb4bd6f' } - - let(:integer_plain_text) { 1 } - - subject { described_class.new key: ENCRYPTION_PASSWORD } - - specify { expect(subject.key).to eq(ENCRYPTION_PASSWORD) } - - describe "#initialize" do - specify { expect { described_class.new }.to raise_error(ArgumentError, "Missing :key") } - end - - describe "#encrypt" do - context "Strings" do - specify { expect(subject.encrypt(plain_text)).to_not eq(plain_text) } - specify { expect(subject.encrypt(plain_text)).to_not be_empty } - end - - context "Integers" do - specify { expect(subject.encrypt(integer_plain_text)).to_not eq(integer_plain_text) } - specify { expect(subject.encrypt(integer_plain_text)).to_not be_empty } - end - end - - describe "#decrypt" do - specify { expect(subject.decrypt(cipher_text)).to eq(plain_text) } - specify { expect(subject.decrypt(integer_cipher_text)).to eq(integer_plain_text.to_s) } - end - - describe "#search" do - subject { postgres_model } - - it "finds the matching record" do - subject.create!(storage: 'blah2') - match = subject.create!(storage: 'blah') - expect(subject.search_by_plaintext(:storage, 'blah').first).to eq(match) - end - end - - describe "Custom pgcrypto options" do - let(:pgcrypto_options) { 'compress-level=0' } - - subject { described_class.new key: 'candy', pgcrypto_options: pgcrypto_options } - - it "reads and writes" do - queries = logged_queries do - encrypted = subject.encrypt(plain_text) - expect(subject.decrypt(encrypted)).to eq(plain_text) - end - - expect(queries).to_not be_empty - - queries.select { |query| query.include?("pgp_sym_encrypt") }.each do |q| - expect(q).to include(pgcrypto_options) - end - end - end - end - end -end