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
7 changes: 6 additions & 1 deletion app/forms/idv/api_image_upload_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,16 @@ def update_analytics(client_response)
).merge(native_camera_ab_test_data),
)
pii_from_doc = client_response.pii_from_doc || {}
store_encrypted_images_if_required
stored_image_result = store_encrypted_images_if_required
irs_attempts_api_tracker.idv_document_upload_submitted(
success: client_response.success?,
document_state: pii_from_doc[:state],
document_number: pii_from_doc[:state_id_number],
document_issued: pii_from_doc[:state_id_issued],
document_expiration: pii_from_doc[:state_id_expiration],
document_front_image_filename: stored_image_result&.front_filename,
document_back_image_filename: stored_image_result&.back_filename,
document_image_encryption_key: stored_image_result&.encryption_key,
first_name: pii_from_doc[:first_name],
last_name: pii_from_doc[:last_name],
date_of_birth: pii_from_doc[:dob],
Expand All @@ -239,7 +242,9 @@ def store_encrypted_images_if_required

encrypted_document_storage_writer.encrypt_and_write_document(
front_image: front_image_bytes,
front_image_content_type: front.content_type,
back_image: back_image_bytes,
back_image_content_type: back.content_type,
)
end

Expand Down
28 changes: 19 additions & 9 deletions app/services/encrypted_document_storage/document_writer.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
module EncryptedDocumentStorage
class DocumentWriter
def encrypt_and_write_document(front_image:, back_image:)
def encrypt_and_write_document(
front_image:,
front_image_content_type:,
back_image:,
back_image_content_type:
)
key = SecureRandom.bytes(32)
encrypted_front_image = aes_cipher.encrypt(front_image, key)
encrypted_back_image = aes_cipher.encrypt(back_image, key)

front_image_uuid = SecureRandom.uuid
back_image_uiid = SecureRandom.uuid
front_filename = build_filename_for_content_type(front_image_content_type)
back_filename = build_filename_for_content_type(back_image_content_type)

storage.write_image(encrypted_image: encrypted_front_image, name: front_image_uuid)
storage.write_image(encrypted_image: encrypted_back_image, name: back_image_uiid)
storage.write_image(encrypted_image: encrypted_front_image, name: front_filename)
storage.write_image(encrypted_image: encrypted_back_image, name: back_filename)

WriteDocumentResult.new(
front_uuid: front_image_uuid,
back_uuid: back_image_uiid,
front_encryption_key: Base64.strict_encode64(key),
back_encryption_key: Base64.strict_encode64(key),
front_filename: front_filename,
back_filename: back_filename,
encryption_key: Base64.strict_encode64(key),
)
end

Expand All @@ -32,5 +36,11 @@ def storage
def aes_cipher
@aes_cipher ||= Encryption::AesCipher.new
end

# @return [String] A new, unique S3 key for an image of the given content type.
def build_filename_for_content_type(content_type)
ext = Rack::Mime::MIME_TYPES.rassoc(content_type)&.first
"#{SecureRandom.uuid}#{ext}"
end
end
end
6 changes: 6 additions & 0 deletions app/services/encrypted_document_storage/local_storage.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
module EncryptedDocumentStorage
class LocalStorage
# Used in tests to verify results
def read_image(name:)
filepath = tmp_document_storage_dir.join(name)
File.read(filepath)
end

def write_image(encrypted_image:, name:)
FileUtils.mkdir_p(tmp_document_storage_dir)
filepath = tmp_document_storage_dir.join(name)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
module EncryptedDocumentStorage
WriteDocumentResult = Struct.new(
:front_uuid,
:back_uuid,
:front_encryption_key,
:back_encryption_key,
:front_filename,
:back_filename,
:encryption_key,
keyword_init: true,
)
end
5 changes: 5 additions & 0 deletions app/services/idv/data_url_image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ def initialize(data_url)
@data = data
end

# @return [String]
def content_type
@header.split(';', 2).first
end

# @return [String]
def read
if base64_encoded?
Expand Down
9 changes: 9 additions & 0 deletions app/services/irs_attempts_api/tracker_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ def idv_document_upload_rate_limited
# @param [String] document_number
# @param [String] document_issued
# @param [String] document_expiration
# @param [String] document_front_image_filename Filename in S3 w/ encrypted data for the front.
# @param [String] document_back_image_filename Filename in S3 w/ encrypted data for the back.
# @param [String] document_image_encryption_key Base64-encoded AES key used for images.
# @param [String] first_name
# @param [String] last_name
# @param [String] date_of_birth
Expand All @@ -101,6 +104,9 @@ def idv_document_upload_submitted(
document_number: nil,
document_issued: nil,
document_expiration: nil,
document_front_image_filename: nil,
document_back_image_filename: nil,
document_image_encryption_key: nil,
first_name: nil,
last_name: nil,
date_of_birth: nil,
Expand All @@ -114,6 +120,9 @@ def idv_document_upload_submitted(
document_number: document_number,
document_issued: document_issued,
document_expiration: document_expiration,
document_front_image_filename: document_front_image_filename,
document_back_image_filename: document_back_image_filename,
document_image_encryption_key: document_image_encryption_key,
first_name: first_name,
last_name: last_name,
date_of_birth: date_of_birth,
Expand Down
80 changes: 79 additions & 1 deletion spec/controllers/idv/image_uploads_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
require 'rails_helper'

describe Idv::ImageUploadsController do
let(:document_filename_regex) { /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}\.[a-z]+$/ }
let(:base64_regex) { /^[a-z0-9+\/]+=*$/i }

describe '#create' do
subject(:action) { post :create, params: params }
subject(:action) do
post :create, params: params
end

let(:user) { create(:user) }
let!(:document_capture_session) { user.document_capture_sessions.create!(user: user) }
Expand All @@ -18,6 +23,12 @@
end
let(:json) { JSON.parse(response.body, symbolize_names: true) }

let(:store_encrypted_images) { false }

before do
allow(controller).to receive(:store_encrypted_images?).and_return(store_encrypted_images)
end

before do
Funnel::DocAuth::RegisterStep.new(user.id, '').call('welcome', :view, true)
end
Expand Down Expand Up @@ -294,6 +305,9 @@
:idv_document_upload_submitted,
success: true,
failure_reason: nil,
document_back_image_filename: nil,
document_front_image_filename: nil,
document_image_encryption_key: nil,
document_state: 'MT',
document_number: '1111111111111',
document_issued: '2019-12-31',
Expand All @@ -309,6 +323,27 @@
expect_funnel_update_counts(user, 1)
end

context 'encrypted document storage is enabled' do
let(:store_encrypted_images) { true }

it 'includes image fields in attempts api event' do
stub_attempts_tracker

expect(@irs_attempts_api_tracker).to receive(:track_event).with(
:idv_document_upload_submitted,
hash_including(
success: true,
failure_reason: nil,
document_back_image_filename: match(document_filename_regex),
document_front_image_filename: match(document_filename_regex),
document_image_encryption_key: match(base64_regex),
),
)

action
end
end

context 'but doc_pii validation fails' do
let(:first_name) { 'FAKEY' }
let(:last_name) { 'MCFAKERSON' }
Expand All @@ -334,6 +369,34 @@
)
end

context 'encrypted document storage is enabled' do
let(:store_encrypted_images) { true }
let(:first_name) { nil }

it 'includes image references in attempts api' do
stub_attempts_tracker

expect(@irs_attempts_api_tracker).to receive(:track_event).with(
:idv_document_upload_submitted,
success: true,
failure_reason: nil,
document_state: 'ND',
document_number: nil,
document_issued: nil,
document_expiration: nil,
first_name: nil,
last_name: 'MCFAKERSON',
date_of_birth: '10/06/1938',
address: nil,
document_back_image_filename: match(document_filename_regex),
document_front_image_filename: match(document_filename_regex),
document_image_encryption_key: match(base64_regex),
)

action
end
end

context 'due to invalid Name' do
let(:first_name) { nil }

Expand Down Expand Up @@ -403,6 +466,9 @@
last_name: 'MCFAKERSON',
date_of_birth: '10/06/1938',
address: nil,
document_back_image_filename: nil,
document_front_image_filename: nil,
document_image_encryption_key: nil,
)

action
Expand Down Expand Up @@ -478,6 +544,9 @@
last_name: 'MCFAKERSON',
date_of_birth: '10/06/1938',
address: nil,
document_back_image_filename: nil,
document_front_image_filename: nil,
document_image_encryption_key: nil,
)

action
Expand Down Expand Up @@ -545,6 +614,9 @@
:idv_document_upload_submitted,
success: true,
failure_reason: nil,
document_back_image_filename: nil,
document_front_image_filename: nil,
document_image_encryption_key: nil,
document_state: 'ND',
document_number: nil,
document_issued: nil,
Expand Down Expand Up @@ -631,6 +703,9 @@
failure_reason: {
front: [I18n.t('doc_auth.errors.general.multiple_front_id_failures')],
},
document_back_image_filename: nil,
document_front_image_filename: nil,
document_image_encryption_key: nil,
document_state: nil,
document_number: nil,
document_issued: nil,
Expand Down Expand Up @@ -713,6 +788,9 @@
general: [I18n.t('doc_auth.errors.alerts.barcode_content_check')],
back: [I18n.t('doc_auth.errors.general.fallback_field_level')],
},
document_back_image_filename: nil,
document_front_image_filename: nil,
document_image_encryption_key: nil,
document_state: nil,
document_number: nil,
document_issued: nil,
Expand Down
44 changes: 34 additions & 10 deletions spec/forms/idv/api_image_upload_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,31 +252,55 @@
let(:store_encrypted_images) { true }

it 'writes encrypted documents' do
# This is not a _great_ way to test this. Once we start writing these events to the
# attempts API we should use the fake attempts API to grab the 'reference` value for the
# front and back image and check that those files are written.
form.submit

upload_events = irs_attempts_api_tracker.events[:idv_document_upload_submitted]
expect(upload_events).to have_attributes(length: 1)
upload_event = upload_events.first

document_writer = form.send(:encrypted_document_storage_writer)

expect(document_writer).to receive(:encrypt_and_write_document).with(
front_image: DocAuthImageFixtures.document_front_image_multipart.read,
back_image: DocAuthImageFixtures.document_back_image_multipart.read,
).and_call_original
front_image.rewind
back_image.rewind

form.submit
cipher = Encryption::AesCipher.new

front_image_ciphertext =
document_writer.storage.read_image(name: upload_event[:document_front_image_filename])

back_image_ciphertext =
document_writer.storage.read_image(name: upload_event[:document_back_image_filename])

key = Base64.decode64(upload_event[:document_image_encryption_key])

expect(cipher.decrypt(front_image_ciphertext, key)).to eq(front_image.read)
expect(cipher.decrypt(back_image_ciphertext, key)).to eq(back_image.read)
end
end

context 'when the attempts API is not enabled' do
context 'when encrypted image storage is disabled' do
let(:store_encrypted_images) { false }

it 'when encrypted image storage is disabled' do
it 'does not write images' do
document_writer = instance_double(EncryptedDocumentStorage::DocumentWriter)
allow(form).to receive(:encrypted_document_storage_writer).and_return(document_writer)

expect(document_writer).to_not receive(:encrypt_and_write_document)

form.submit
end

it 'does not send image info to attempts api' do
expect(irs_attempts_api_tracker).to receive(:idv_document_upload_submitted).with(
hash_including(
document_front_image_filename: nil,
document_back_image_filename: nil,
document_image_encryption_key: nil,
),
)

form.submit
end
end
end

Expand Down
Loading