Skip to content
16 changes: 15 additions & 1 deletion app/controllers/idv/personal_key_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,21 @@ def profile

def generate_personal_key
cacher = Pii::Cacher.new(current_user, user_session)
profile.encrypt_recovery_pii(cacher.fetch)

new_personal_key = nil

Profile.transaction do
current_user.profiles.each do |profile|
pii = cacher.fetch(profile.id)
next if pii.nil?

new_personal_key = profile.encrypt_recovery_pii(pii, personal_key: new_personal_key)

profile.save!
end
end

new_personal_key
end

def in_person_enrollment?
Expand Down
4 changes: 2 additions & 2 deletions app/models/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ def encrypt_pii(pii, password)
end

# @param [Pii::Attributes] pii
def encrypt_recovery_pii(pii)
personal_key = personal_key_generator.create
def encrypt_recovery_pii(pii, personal_key: nil)
personal_key ||= personal_key_generator.create
encryptor = Encryption::Encryptors::PiiEncryptor.new(
personal_key_generator.normalize(personal_key),
)
Expand Down
118 changes: 118 additions & 0 deletions spec/controllers/idv/personal_key_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,36 @@
include SamlAuthHelper
include PersonalKeyValidator

def assert_personal_key_generated_for_profiles(*profile_pii_pairs)
expect(idv_session.personal_key).to be_present

normalized_personal_key = normalize_personal_key(idv_session.personal_key)

# These keys are present in our applicant fixture but
# are not actually supported in Pii::Attributes
keys_to_ignore = %i[
state_id_expiration
state_id_issued
state_id_number
state_id_type
]

profile_pii_pairs.each do |profile, pii|
expected = Pii::Attributes.new(pii.except(*keys_to_ignore))
actual = profile.reload.recover_pii(normalized_personal_key)
expect(actual).to eql(expected)
end
end

let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE }
let(:password) { 'sekrit phrase' }
let(:user) { create(:user, :fully_registered, password: password) }

# Most (but not all) of these tests assume that a profile has been minted
# from the data in idv_session. Set this to false to prevent this behavior
# and test the other way.
# (idv_session.profile will be nil if the user is coming back to complete
# the IdV flow out-of-band, like with GPO.)
let(:mint_profile_from_idv_session) { true }

let(:address_verification_mechanism) { 'phone' }
Expand Down Expand Up @@ -148,6 +171,101 @@ def index
expect(response).to redirect_to idv_enter_password_url
end
end

context 'no personal key generated yet' do
before do
idv_session.personal_key = nil
end

it 'generates a personal key that encrypts the idv_session profile data' do
get :show
assert_personal_key_generated_for_profiles([idv_session.profile, applicant])
end

context 'user has an existing profile in addition to the one attached to idv_session' do
let(:existing_profile_pii) { idv_session.applicant.merge(first_name: 'Existing') }
let!(:existing_profile) do
create(
:profile,
:verify_by_mail_pending,
user: user,
pii: existing_profile_pii,
)
end

before do
Pii::ProfileCacher.new(user, subject.user_session).save_decrypted_pii(
existing_profile_pii,
existing_profile.id,
)
end

it 'generates a personal key that encrypts the idv_session and existing profile data' do
expect(user.profiles).to include(existing_profile)
expect(user.profiles).to include(idv_session.profile)
get :show
assert_personal_key_generated_for_profiles(
[idv_session.profile, idv_session.applicant],
[existing_profile, existing_profile_pii],
)
end
end

context 'no profile attached to idv_session' do
let(:mint_profile_from_idv_session) { false }

context 'user has a pending profile' do
let!(:pending_profile_pii) { applicant.merge(first_name: 'Pending') }
let!(:pending_profile) do
create(
:profile,
:verify_by_mail_pending,
user: user,
pii: pending_profile_pii,
)
end

before do
Pii::ProfileCacher.new(user, subject.user_session).save_decrypted_pii(
pending_profile_pii,
pending_profile.id,
)
end

it 'generates a personal key that encrypts the pending profile data' do
get :show
assert_personal_key_generated_for_profiles([pending_profile, pending_profile_pii])
end

context 'and user has an active profile' do
let(:active_profile_pii) { applicant.merge(first_name: 'Active') }
let!(:active_profile) do
create(
:profile,
:active,
user: user,
pii: active_profile_pii,
)
end

before do
Pii::ProfileCacher.new(user, subject.user_session).save_decrypted_pii(
active_profile_pii,
active_profile.id,
)
end

it 'generates a personal key that encrypts both profiles' do
get :show
assert_personal_key_generated_for_profiles(
[active_profile, active_profile_pii],
[pending_profile, pending_profile_pii],
)
end
end
end
end
end
end

describe '#update' do
Expand Down
12 changes: 12 additions & 0 deletions spec/models/profile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@
expect(user.reload.encrypted_recovery_code_digest).to_not eq initial_personal_key
expect(profile.personal_key).to_not eq user.encrypted_recovery_code_digest
end

it 'can be passed a personal key' do
expect(profile.encrypted_pii_recovery).to be_nil

personal_key = 'ABCD-1234'
returned_personal_key = profile.encrypt_recovery_pii(pii, personal_key: personal_key)

expect(returned_personal_key).to eql(personal_key)

expect(profile.encrypted_pii_recovery).to be_present
expect(profile.personal_key).to eq personal_key
end
end

describe '#decrypt_pii' do
Expand Down