require "date" require "openssl" require "base64" require "octothorpe" require "pod4" require "pod4/encrypting" require "pod4/null_interface" require "pod4/errors" describe "(Model with Encryption)" do ## # Encrypt / decrypt # def encrypt(key, iv=nil, plaintext) cipher = OpenSSL::Cipher.new(iv ? Pod4::Encrypting::CIPHER_IV : Pod4::Encrypting::CIPHER_NO_IV) cipher.encrypt cipher.key = key cipher.iv = iv if iv answer = (plaintext.empty? ? "" : cipher.update(plaintext) ) + cipher.final Base64.strict_encode64(answer) end let(:encryption_key) { "dflkasdgklajndgnalkghlgasdgasdghaalsdg" } let(:medical_model_class) do # model with an IV column Class.new Pod4::Model do include Pod4::Encrypting attr_columns :id, :nhs_no # note, we don't bother to name encrypted columns encrypted_columns :name, :ailment, :prescription set_key "dflkasdgklajndgnalkghlgasdgasdghaalsdg" set_iv_column :nonce set_interface NullInterface.new(:id, :nhs_no, :name, :ailment, :prescription, :nonce, []) end end let(:medical_model_bad_class) do # model with an IV column and a very very short key Class.new Pod4::Model do include Pod4::Encrypting attr_columns :id, :nhs_no # note, we don't bother to name encrypted columns encrypted_columns :name, :ailment, :prescription set_key "d" set_iv_column :nonce set_interface NullInterface.new(:id, :nhs_no, :name, :ailment, :prescription, :nonce, []) end end let(:diary_model_class) do # model without an IV column Class.new Pod4::Model do include Pod4::Encrypting attr_columns :id, :date, :heading, :text encrypted_columns :heading, :text set_key "dflkasdgklajndgnalkghlgasdgasdghaalsdg" set_interface NullInterface.new(:id, :date, :heading, :text, []) end end let(:m40) do m = medical_model_class.new m.id = 40 m.nhs_no = "123456" m.name = "fred" m.ailment = "sore toe" m.prescription = "suck thumb" m end let(:d44) do d = diary_model_class.new d.id = 44 d.date = Date.new(2018,4,14) d.heading = "fred" d.text = "sore toe" d end let(:m40_record) { m40.interface.read(40) } let(:d44_record) { d44.interface.read(44) } describe "Model::CIPHER_IV" do it "should be a valid string to initialize OpenSSL::Cipher" do expect{ OpenSSL::Cipher.new(medical_model_class::CIPHER_IV) }.not_to raise_exception end end # of Model::CIPHER_IV describe "Model::CIPHER_NO_IV" do it "should be a valid string to initialize OpenSSL::Cipher" do expect{ OpenSSL::Cipher.new(medical_model_class::CIPHER_NO_IV) }.not_to raise_exception end end # of Model::CIPHER_NO_IV describe "Model.set_key" do it "requires a string" do expect( medical_model_class ).to respond_to(:set_key).with(1).argument end end # of Model.set_key describe "Model.encryption_key" do it "returns the key passed to set_key" do expect( medical_model_class.encryption_key ).to eq encryption_key end end # of Model.encryption_key describe "Model.set_iv_column" do it "requires an IV column" do expect( medical_model_class ).to respond_to(:set_iv_column).with(1).argument end it "exposes the encryption_iv attribute, just like attr_accessor" do m20 = medical_model_class.new(20) expect( m20 ).to respond_to(:nonce) expect( m20 ).to respond_to(:nonce=) end # See also: Model#encryption_iv end # of Model.set_iv_column describe "Model.encryption_iv_column" do it "returns the column passed to set_iv_column" do expect( medical_model_class.encryption_iv_column ).to eq :nonce end end # of Model.encryption_iv_column describe "Model.encrypted_columns" do it "requires a list of columns" do expect( medical_model_class ).to respond_to(:encrypted_columns).with(1).argument end it "adds the columns to the model's column list, just like attr_columns" do expect( medical_model_class.columns ). to match_array( %i|id nonce nhs_no name ailment prescription| ) expect( diary_model_class.columns ).to match_array( %i|id date heading text| ) end it "exposes the columns just like attr_accessor" do m20 = medical_model_class.new(20) expect( m20 ).to respond_to(:name) expect( m20 ).to respond_to(:ailment) expect( m20 ).to respond_to(:prescription) expect( m20 ).to respond_to(:name=) expect( m20 ).to respond_to(:ailment=) expect( m20 ).to respond_to(:prescription=) end end # of Model.encrypted_columns describe "Model.encryption_columns" do it "exposes the columns given in encrypted_columns" do expect( medical_model_class.encryption_columns ). to match_array( %i|name ailment prescription| ) end end # of Model.encryption_columns describe "(Creating a record)" do context "when we don't have an IV column" do it "scrambles the encrypted columns and leaves the others alone" do d44.create expect( d44_record.>>.date ).to eq d44.date expect( d44_record.>>.heading ).not_to eq d44.heading expect( d44_record.>>.text ).not_to eq d44.text end end context "when we have an IV column" do it "writes an IV to the nonce field" do m40.create expect( m40_record.>>.nonce ).not_to be_nil recordiv = Base64.strict_decode64(m40_record.>>.nonce) expect( recordiv ).to eq m40.nonce expect( recordiv ).to eq m40.encryption_iv end it "scrambles the encrypted columns and leaves the others alone" do m40.create expect( m40_record.>>.nhs_no ).to eq m40.nhs_no expect( m40_record.>>.name ).not_to eq m40.name expect( m40_record.>>.ailment ).not_to eq m40.ailment expect( m40_record.>>.prescription ).not_to eq m40.prescription end end end # of (Creating a record) describe "(reading a record)" do context "when we have no IV column" do before(:each) { d44.create } it "decrypts the columns" do d = diary_model_class.new(44).read expect( d.date ).to eq Date.new(2018,4,14) expect( d.heading ).to eq "fred" expect( d.text ).to eq "sore toe" end end context "when we have an IV column" do before(:each) { m40.create } it "returns the IV field as encryption_iv" do m = medical_model_class.new(40).read expect( m.encryption_iv ).not_to be_nil expect( m.encryption_iv ).to eq m.nonce end it "decrypts the columns" do m = medical_model_class.new(40).read expect( m.nhs_no ).to eq "123456" expect( m.name ).to eq "fred" expect( m.ailment ).to eq "sore toe" expect( m.prescription ).to eq "suck thumb" end end end # of (reading a record) describe "Model#(encryption_iv field)" do it "returns nil for a new model object" do m = medical_model_class.new m.id = 50 expect( m.nonce ).to be_nil end it "returns the nonce for an existing record" do expect( m40.nonce ).to eq m40_record.>>.nonce end end # of Model#(encryption_iv field) describe "Model#encryption_iv" do it "returns nil for a new model object" do m = medical_model_class.new m.id = 50 expect( m.encryption_iv ).to be_nil end it "returns nil if we don't have an IV set" do d44.create expect( d44.encryption_iv ).to be_nil end it "returns the nonce for an existing record" do expect( m40.encryption_iv ).to eq m40_record.>>.nonce end it "returns the same as the actual IV field" do expect( m40.encryption_iv ).to eq m40.nonce end end # of Model#encryption_iv describe "Model#map_to_interface" do it "raises Pod4Error if there is an encryption problem, eg, key too short" do bad = medical_model_bad_class.new bad.id = 999 bad.nhs_no = "12345" bad.name = "alice" bad.ailment = "tiny key" bad.prescription = "raise an exception" expect{ bad.map_to_interface }.to raise_exception Pod4::Pod4Error end it "doesn't freak out when asked to encrypt an empty column" do bad = medical_model_class.new bad.id = 998 bad.nhs_no = "12345" bad.name = "" bad.ailment = nil expect{ bad.map_to_interface }.not_to raise_exception end context "when we don't have an IV column" do it "encrypts only the encryptable columns for the interface" do ot = d44.map_to_interface expect( ot.>>.date ).to eq Date.new(2018,4,14) expect( ot.>>.heading ).to eq encrypt(encryption_key, "fred") expect( ot.>>.text ).to eq encrypt(encryption_key, "sore toe") end end context "when we have an IV column" do before(:each) { m40.create } it "sets the IV column on create" do expect( m40_record.>>.nonce ).not_to be_nil end it "encrypts only the encryptable columns for the interface" do ot = m40.map_to_interface iv = m40.nonce expect( ot.>>.nhs_no ).to eq "123456" expect( ot.>>.name ).to eq encrypt(encryption_key, iv, "fred") expect( ot.>>.ailment ).to eq encrypt(encryption_key, iv, "sore toe") expect( ot.>>.prescription ).to eq encrypt(encryption_key, iv, "suck thumb") end end end # of Model#map_to_interface describe "Model#map_to_model" do context "when we don't have an IV column" do it "decrypts only the encryptable columns for the model" do d44.create d = diary_model_class.new(44) expect( d44_record.>>.heading ).not_to eq "fred" expect( d44_record.>>.text ).not_to eq "sore toe" d.read expect( d.date ).to eq Date.new(2018,4,14) expect( d.heading ).to eq "fred" expect( d.text ).to eq "sore toe" end it "successfully decrypts an empty column" do d44.text = "" d44.create d = diary_model_class.new(44) d.read expect( d.text ).to eq "" end end context "when we have an IV column" do it "decrypts only the encryptable columns for the model" do m40.create m = medical_model_class.new(40) expect( m40_record.>>.name ).not_to eq "fred" expect( m40_record.>>.ailment ).not_to eq "sore toe" expect( m40_record.>>.prescription ).not_to eq "suck thumb" m.read expect( m.nhs_no ).to eq "123456" expect( m.name ).to eq "fred" expect( m.ailment ).to eq "sore toe" expect( m.prescription ).to eq "suck thumb" end it "successfully decrypts an empty column" do m40.ailment = "" m40.prescription = nil m40.create m = medical_model_class.new(40) m.read expect( m.ailment ).to eq "" expect( m.prescription ).to be_nil end end end # of Model#map_to_model end