From 229ebf1380cdd8576e0566a00d2ae6add1838c4e Mon Sep 17 00:00:00 2001 From: Mark Moser Date: Fri, 26 Aug 2016 16:03:55 -0500 Subject: [PATCH] start encoding candidate emails - completes #57 --- app/models/candidate.rb | 2 + app/services/crypt_serializer.rb | 46 +++++++++++++++++++ .../20160826200610_encode_candidate_emails.rb | 26 +++++++++++ db/schema.rb | 2 +- test/fixtures/candidates.yml | 16 +++---- test/models/candidate_test.rb | 14 ++++++ test/services/crypt_serializer_test.rb | 26 +++++++++++ 7 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 app/services/crypt_serializer.rb create mode 100644 db/migrate/20160826200610_encode_candidate_emails.rb create mode 100644 test/services/crypt_serializer_test.rb diff --git a/app/models/candidate.rb b/app/models/candidate.rb index 8af27fa..b54bea3 100644 --- a/app/models/candidate.rb +++ b/app/models/candidate.rb @@ -4,6 +4,8 @@ class Candidate < ApplicationRecord has_many :answers belongs_to :recruiter, class_name: "User" + serialize :email, CryptSerializer + before_validation(:generate_test_hash, on: :create) validates_presence_of :recruiter_id diff --git a/app/services/crypt_serializer.rb b/app/services/crypt_serializer.rb new file mode 100644 index 0000000..bf39363 --- /dev/null +++ b/app/services/crypt_serializer.rb @@ -0,0 +1,46 @@ +require 'openssl' +require 'base64' + +class CryptSerializer + attr_reader :cipher + + class << self + # pulling from DB - return plain value + def load value + new.decrypt value + end + + # saving to DB - return encrypted value + def dump value + new.encrypt value + end + end + + def initialize + @cipher = OpenSSL::Cipher::AES.new(128, :CBC) + end + + def encrypt(value) + unless value.is_a?(String) + raise "Attribute was supposed to be a `String`, but was instead a `#{value.class}`" + end + + return value if value.nil? + + cipher.encrypt + parts = [cipher.random_key, cipher.random_iv, cipher.update(value) + cipher.final] + + Base64.urlsafe_encode64 Marshal.dump(parts) + end + + def decrypt(value) + return value if value.nil? + + parts = Marshal.load Base64.urlsafe_decode64(value) + cipher.decrypt + cipher.key = parts[0] + cipher.iv = parts[1] + + cipher.update(parts[2]) + cipher.final + end +end diff --git a/db/migrate/20160826200610_encode_candidate_emails.rb b/db/migrate/20160826200610_encode_candidate_emails.rb new file mode 100644 index 0000000..6872e22 --- /dev/null +++ b/db/migrate/20160826200610_encode_candidate_emails.rb @@ -0,0 +1,26 @@ +class EncodeCandidateEmails < ActiveRecord::Migration[5.0] + def change + sql = "select id, email from candidates;" + candidates = ActiveRecord::Base.connection.execute(sql).to_h + + candidates.each do |id, email| + sql = if base64?(email) + # going down - decrypt + "UPDATE candidates set email = '#{CryptSerializer.load email}' WHERE id = #{id};" + else + # going up - encrypt emails + "UPDATE candidates set email = '#{CryptSerializer.dump email}' WHERE id = #{id};" + end + ActiveRecord::Base.connection.execute(sql) + end + end + + private + + def base64? string + Base64.urlsafe_decode64 string + true + rescue ArgumentError + false + end +end diff --git a/db/schema.rb b/db/schema.rb index 2964332..ac3eb57 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160824183159) do +ActiveRecord::Schema.define(version: 20160826200610) do create_table "answers", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| t.integer "candidate_id" diff --git a/test/fixtures/candidates.yml b/test/fixtures/candidates.yml index 0e1aa25..c55d18f 100644 --- a/test/fixtures/candidates.yml +++ b/test/fixtures/candidates.yml @@ -2,7 +2,7 @@ roy: # Roy should have started, and is ready for a reminder name: Roy Cruz - email: roy.cruz@mailinator.com + email: <%= CryptSerializer.dump 'roy.cruz@mailinator.com' %> experience: 0-3 recruiter: recruiter quiz: fed @@ -12,7 +12,7 @@ roy: # Roy should have started, and is ready for a reminder gillian: # Gillian has not begun the test name: Gillian Anderson - email: gillian.anderson@mailinator.com + email: <%= CryptSerializer.dump 'gillian.anderson@mailinator.com' %> experience: 4-6 recruiter: recruiter quiz: fed @@ -22,7 +22,7 @@ gillian: # Gillian has not begun the test martha: # Martha has not begun the test name: Martha Watts - email: martha.watts@mailinator.com + email: <%= CryptSerializer.dump 'martha.watts@mailinator.com' %> experience: 4-6 recruiter: recruiter quiz: fed @@ -32,7 +32,7 @@ martha: # Martha has not begun the test dawn: # Dawn has completed, and been reminded, but not submitted the test name: Dawn Hopkins - email: dawn.hopkins@mailinator.com + email: <%= CryptSerializer.dump 'dawn.hopkins@mailinator.com' %> experience: 0-2 recruiter: recruiter quiz: fed @@ -42,7 +42,7 @@ dawn: # Dawn has completed, and been reminded, but not submitted the test peggy: # Peggy has completed, and been reminded, but not submitted the test name: Peggy Blisters - email: peggy.blisters@mailinator.com + email: <%= CryptSerializer.dump 'peggy.blisters@mailinator.com' %> experience: 0-2 recruiter: recruiter quiz: fed @@ -52,7 +52,7 @@ peggy: # Peggy has completed, and been reminded, but not submitted the test richard: # Richard has completed AND submitted the test name: Richard Burns - email: richard.burns@mailinator.com + email: <%= CryptSerializer.dump 'richard.burns@mailinator.com' %> experience: 15+ recruiter: recruiter quiz: fed @@ -62,10 +62,10 @@ richard: # Richard has completed AND submitted the test juan: # Juan has chosen "finish later" for live coders name: Juan Campbell - email: juan.campbell@mailinator.com + email: <%= CryptSerializer.dump 'juan.campbell@mailinator.com' %> experience: 15+ recruiter: recruiter quiz: fed completed: false reminded: true - test_hash: qKQo0l4dyol + test_hash: <%= CryptSerializer.dump 'qKQo0l4dyol diff --git a/test/models/candidate_test.rb b/test/models/candidate_test.rb index 112f8ef..5707062 100644 --- a/test/models/candidate_test.rb +++ b/test/models/candidate_test.rb @@ -9,4 +9,18 @@ class CandidateTest < ActiveSupport::TestCase assert candidate.test_hash.present? end + + test "should encrypt emails" do + email = 'test@mailinator.com' + candidate = Candidate.create(name: 'new name', + email: email, + experience: '0-3', + recruiter_id: users(:recruiter).id, + quiz_id: quizzes(:fed).id) + + sql = "select email from candidates where id = #{candidate.id};" + enc_email = ActiveRecord::Base.connection.execute(sql).first.first + + refute_equal email, enc_email + end end diff --git a/test/services/crypt_serializer_test.rb b/test/services/crypt_serializer_test.rb new file mode 100644 index 0000000..c649076 --- /dev/null +++ b/test/services/crypt_serializer_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +class CryptSerializerTest < ActiveSupport::TestCase + test "should generate marshaled array" do + string = "some string to encrypt" + encrypted = CryptSerializer.dump string + ar = Marshal.load(Base64.urlsafe_decode64(encrypted)) + + assert_instance_of Array, ar + assert_equal 3, ar.count + end + + test "should encrypt and dencrypt" do + string = "test@string.email" + encrypted = CryptSerializer.dump string + decrypted = CryptSerializer.load encrypted + + assert_equal string, decrypted + end + + test "must raise RuntimeError" do + assert_raises RuntimeError do + CryptSerializer.dump nil + end + end +end