Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions app/services/pii/cacher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,26 @@ def initialize(user, user_session)

def save(user_password, profile = user.active_profile)
decrypted_pii = profile.decrypt_pii(user_password) if profile
save_decrypted_pii(decrypted_pii) if decrypted_pii
save_decrypted_pii(decrypted_pii, profile.id) if decrypted_pii
rotate_fingerprints(profile) if stale_fingerprints?(profile)
user_session[:decrypted_pii]
end

def save_decrypted_pii(decrypted_pii)
def save_decrypted_pii(decrypted_pii, profile_id = nil)
user_session[:decrypted_pii] = decrypted_pii.to_json

if profile_id.present? && IdentityConfig.store.session_encrypted_profiles_write_enabled
Pii::ProfileCacher.new(user, user_session).save_decrypted_pii(decrypted_pii, profile_id)
end

nil
end

def fetch
def fetch(profile_id = nil)
if profile_id.present? && IdentityConfig.store.session_encrypted_profiles_read_enabled
return Pii::ProfileCacher.new(user, user_session).fetch(profile_id)
end

pii_string = fetch_string
return nil unless pii_string

Expand All @@ -37,6 +46,7 @@ def exists_in_session?
def delete
user_session.delete(:decrypted_pii)
user_session.delete(:encrypted_pii)
Pii::ProfileCacher.new(user, user_session).delete
end

private
Expand Down
69 changes: 69 additions & 0 deletions app/services/pii/profile_cacher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module Pii
class ProfileCacher
attr_reader :user, :user_session

def initialize(user, user_session)
@user = user
@user_session = user_session
end

def save(user_password, profile = user.active_profile)
decrypted_pii = profile.decrypt_pii(user_password) if profile
save_decrypted_pii(decrypted_pii, profile.id) if decrypted_pii
rotate_fingerprints(profile, decrypted_pii) if stale_fingerprints?(profile, decrypted_pii)
decrypted_pii
end

def save_decrypted_pii(decrypted_pii, profile_id)
kms_encrypted_pii = SessionEncryptor.new.kms_encrypt(decrypted_pii.to_json)

user_session[:encrypted_profiles] ||= {}
user_session[:encrypted_profiles][profile_id.to_s] = kms_encrypted_pii
Comment thread
mitchellhenke marked this conversation as resolved.
end

def fetch(profile_id)
return unless user_session[:encrypted_profiles].present?

encrypted_profile_pii = user_session[:encrypted_profiles][profile_id.to_s]
return unless encrypted_profile_pii.present?

decrypted_profile_pii_json = SessionEncryptor.new.kms_decrypt(encrypted_profile_pii)
Pii::Attributes.new_from_json(decrypted_profile_pii_json)
end

def exists_in_session?
user_session[:encrypted_profiles].present?
end

def delete
user_session.delete(:encrypted_profiles)
end

private

def rotate_fingerprints(profile, pii)
KeyRotator::HmacFingerprinter.new.rotate(
user: user,
profile: profile,
pii_attributes: pii,
)
end

def stale_fingerprints?(profile, pii)
stale_ssn_signature?(profile, pii) ||
stale_compound_pii_signature?(profile, pii)
end

def stale_ssn_signature?(profile, pii)
return false unless profile.present? && pii.present?
Pii::Fingerprinter.stale?(pii.ssn, profile.ssn_signature)
end

def stale_compound_pii_signature?(profile, pii)
return false unless profile.present? && pii.present?
compound_pii = Profile.build_compound_pii(pii)
return false unless compound_pii
Pii::Fingerprinter.stale?(compound_pii, profile.name_zip_birth_year_signature)
end
end
end
4 changes: 4 additions & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ seed_agreements_data: true
service_provider_request_ttl_hours: 24
session_check_delay: 30
session_check_frequency: 30
session_encrypted_profiles_read_enabled: true
session_encrypted_profiles_write_enabled: true
session_encryptor_alert_enabled: false
session_timeout_in_minutes: 15
session_timeout_warning_seconds: 150
Expand Down Expand Up @@ -478,6 +480,8 @@ production:
scrypt_cost: 10000$8$1$
secret_key_base:
seed_agreements_data: false
session_encrypted_profiles_read_enabled: false
session_encrypted_profiles_write_enabled: false
session_encryption_key:
skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:dev", "urn:gov:gsa:SAML:2.0.profiles:sp:sso:int"]'
state_tracking_enabled: false
Expand Down
2 changes: 2 additions & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ def self.build_store(config_map)
config.add(:ses_configuration_set_name, type: :string)
config.add(:session_check_delay, type: :integer)
config.add(:session_check_frequency, type: :integer)
config.add(:session_encrypted_profiles_read_enabled, type: :boolean)
config.add(:session_encrypted_profiles_write_enabled, type: :boolean)
config.add(:session_encryption_key, type: :string)
config.add(:session_encryptor_alert_enabled, type: :boolean)
config.add(:session_timeout_in_minutes, type: :integer)
Expand Down
158 changes: 158 additions & 0 deletions spec/services/pii/profile_cacher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
require 'rails_helper'

RSpec.describe Pii::ProfileCacher do
let(:password) { 'salty peanuts are best' }
let(:user) { create(:user, :with_phone, password: password) }
let(:user_session) { {}.with_indifferent_access }

let(:active_pii) do
Pii::Attributes.new(
first_name: 'Test',
last_name: 'Testerson',
dob: '2023-01-01',
zipcode: '10000',
ssn: '123-45-6789',
)
end
let(:active_profile) do
profile = create(:profile, :active, :verified, user: user)
profile.encrypt_pii(active_pii, password)
profile
end

let(:pending_pii) do
Pii::Attributes.new(
first_name: 'Test2',
last_name: 'Testerson2',
dob: '2023-01-01',
zipcode: '10000',
ssn: '999-99-9999',
)
end
let(:pending_profile) do
profile = create(:profile, :verified, :verify_by_mail_pending, user: user)
profile.encrypt_pii(pending_pii, password)
profile
end

subject { described_class.new(user, user_session) }

describe '#save' do
it 'writes decrypted PII to user_session for multiple profiles' do
decrypted_active_pii = subject.save(password, active_profile)
decrypted_pending_pii = subject.save(password, pending_profile)

expect(decrypted_active_pii).to eq(active_pii)
expect(decrypted_pending_pii).to eq(pending_pii)

encrypted_active_session_pii = user_session[:encrypted_profiles][active_profile.id.to_s]
decrypted_active_session_pii = SessionEncryptor.new.kms_decrypt(encrypted_active_session_pii)
expect(decrypted_active_session_pii).to eq(active_pii.to_json)

encrypted_pending_session_pii = user_session[:encrypted_profiles][pending_profile.id.to_s]
decrypted_pending_session_pii = SessionEncryptor.new.kms_decrypt(
encrypted_pending_session_pii,
)
expect(decrypted_pending_session_pii).to eq(pending_pii.to_json)
end

it 'updates PII bundle fingerprints when keys are rotated' do
old_ssn_signature = active_profile.ssn_signature
old_compound_pii_fingerprint = active_profile.name_zip_birth_year_signature

rotate_all_keys

# Create a new user object to drop the memoized encrypted attributes
reloaded_user = User.find(user.id)

described_class.new(reloaded_user, user_session).save(password, active_profile)

active_profile.reload

expect(active_profile.ssn_signature).to_not eq(old_ssn_signature)
expect(active_profile.name_zip_birth_year_signature).to_not eq(old_compound_pii_fingerprint)
end

it 'does not attempt to rotate nil attributes' do
cacher = described_class.new(user, user_session)
rotate_all_keys

expect { cacher.save(password, nil) }.to_not raise_error
end

it 'does not raise an error if pii fingerprint is nil but attributes are present' do
# The name_zip_birth_year_signature column was added after users had
# ecrypted PII. As a result, those users may have a profile with valid PII
# and a nil value here. Caching the PII into the session for those users
# should update the signature column without raising an error
active_profile.update!(name_zip_birth_year_signature: nil)

subject.save(password, active_profile)

expect(active_profile.reload.name_zip_birth_year_signature).to_not be_nil
end

it 'raises an encryption error for an incorrect password' do
expect do
subject.save('incorrect password', active_profile)
end.to raise_error(Encryption::EncryptionError)
end
end

describe '#fetch' do
it 'fetches decrypted PII from user_session' do
user_session[:encrypted_profiles] = {
'123' => SessionEncryptor.new.kms_encrypt(active_pii.to_json),
'456' => SessionEncryptor.new.kms_encrypt(pending_pii.to_json),
}

result = subject.fetch(123)

expect(result).to eq(active_pii)
end

it 'returns nil if the encrypted profiles are not present' do
result = subject.fetch(123)

expect(result).to eq(nil)
end

it 'returns nil a profile has not been decrypted and loaded into the session' do
user_session[:encrypted_profiles] = {
'456' => SessionEncryptor.new.kms_encrypt(pending_pii.to_json),
}

result = subject.fetch(123)

expect(result).to eq(nil)
end
end

describe '#exists_in_session?' do
it 'returns true if the encrypted profiles are in the session' do
user_session[:encrypted_profiles] = {
'123' => SessionEncryptor.new.kms_encrypt(active_pii.to_json),
'456' => SessionEncryptor.new.kms_encrypt(pending_pii.to_json),
}

expect(subject.exists_in_session?).to eq(true)
end

it 'returns false if the encrypted profiles are in the session' do
expect(subject.exists_in_session?).to eq(false)
end
end

describe '#delete' do
it 'deletes the encrypted profiles from the session' do
user_session[:encrypted_profiles] = {
'123' => SessionEncryptor.new.kms_encrypt(active_pii.to_json),
'456' => SessionEncryptor.new.kms_encrypt(pending_pii.to_json),
}

subject.delete

expect(user_session).to eq({})
end
end
end