New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Encrypt PII at rest #498
Encrypt PII at rest #498
Conversation
f5f73d3
to
9db84d1
Compare
9db84d1
to
6ccc85c
Compare
DELIMITER = '.'.freeze | ||
DIGEST = OpenSSL::Digest::SHA256.new.freeze | ||
|
||
# structure of the encrypted payload: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here's the encryption description
0465b3c
to
e205d20
Compare
e205d20
to
ceb053b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello! Left some comments. Feel free to ignore any that do not make sense, I am new to this proj so trying to pick up the team practices and not sure what is a project-wide style or specific to this PR
@@ -9,12 +10,18 @@ def confirm_idv_steps_complete | |||
redirect_to idv_phone_path unless idv_phone_complete? | |||
end | |||
|
|||
def confirm_current_password | |||
unless current_user.valid_password?(password) | |||
flash[:error] = t('idv.errors.incorrect_password') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yess loving the use of I18n in this proj
@@ -9,12 +10,18 @@ def confirm_idv_steps_complete | |||
redirect_to idv_phone_path unless idv_phone_complete? | |||
end | |||
|
|||
def confirm_current_password | |||
unless current_user.valid_password?(password) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as you know, I prefer if
over unless
in general.
If we could make this if current_user.invalid_password?(password)
I think it would be easier to understand.
def password | ||
params.require(:user)[:password] | ||
rescue ActionController::ParameterMissing | ||
'' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it make sense to have some sort of error msg here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the error message will be generated by the User
model when attempting to validate the password. I.e. a missing password is treated like an empty string because nil
is not allowed as an argument to valid_password?
.
def ssn_is_unique | ||
if Profile.where.not(user_id: @user.id).where(ssn: ssn).any? | ||
if Profile.where.not(user_id: @user.id).where(ssn_signature: ssn_signature).any? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may may sense to throw this line into its own method for understandability. Perhaps something like
def ssn_is_duplicate?
Profile.where.not(user_id: @user.id).where(ssn_signature: ssn_signature).any?
end
@_plain_pii ||= Pii::Attributes.new | ||
end | ||
|
||
def method_missing(method_sym, *arguments, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this in general makes me a tiny bit scared. what is the reason for it in this context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly backwards compatibility. E.g.
profile.ssn = '1234'
is more intuitive and used extensively already, rather than
profile.plain_pii[:ssn] = '1234'
- if current_user.active_profile | ||
- p = current_user.active_profile | ||
- if @active_profile | ||
- p = @active_profile |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting - why are we assigning p
here rather than just using the ivar within this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that was a designer shorthand, just to save keystrokes. Also, as is evident from this proposed change, the origin value of p
might change, but its usage does not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shorthand should be removed - explicit objects please for self-documenting code.
dob: profile.dob.to_s, | ||
phone: profile.phone | ||
} | ||
profile.update!(encrypted_pii: pii.to_json, active: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure -- defining Profile
in here will ensure that we don't run into problems with failing AR validations later on, right? (if, for instance, this migration is run at a time when there are new AR validations but that column hasn't actually been added to the DB yet, so AR data updates will fail)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct.
@@ -4,11 +4,76 @@ | |||
let(:user) { create(:user, :signed_up) } | |||
let(:another_user) { create(:user, :signed_up) } | |||
let(:profile) { create(:profile, user: user) } | |||
|
|||
subject { profile } | |||
let(:pii) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like this is only being used in some tests. What is this project's philosophy on usage of let
and before
statements? I generally avoid them altogether but can see how they are helpful when there is setup that is shared for all specs in a file. Do we still use them if the setup is not shared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let
and before
are used extensively on this project.
Typically, every context
in a rspec has a before
block that creates the described context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are indeed used a lot, but that doesn't mean we have to keep doing that. I've stopped using them, and I don't like before
blocks when there is only one it
block.
|
||
before do | ||
create(:profile, :active, :verified, first_name: 'Jane', user: user) | ||
create(:profile, :verified, first_name: 'Susan', user: user) | ||
profile1.save! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this is only needed because we want to make sure these records are created for the test. If that is the case, why not inline these let
statements and avoid let
and before
in this context altogether? Seems like the use of let
is adding unnecessary complexity for this one spec
describe Pii::Attributes do | ||
describe '#new_from_hash' do | ||
it 'initializes from plain Hash' do | ||
pii = described_class.new_from_hash(first_name: 'Jane') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we using described_class
in this test suite?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been trying to use it to make it easier to rename classes, esp during early feature development.
plaintext_signature = sign(plaintext, private_key) | ||
payload = join_segments(plaintext, plaintext_signature) | ||
cek = cipher.random_key | ||
join_segments(private_key.public_encrypt(cek), encrypt_payload(payload, cek)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public_encrypt
uses PKCS1v1.5 mode padding (per https://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/PKey/RSA.html#method-i-public_encrypt), PKCS1v1.5 should be avoided in new applications, and OAEP should be used instead (I'm not sure the API for this in Ruby offhand)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @alex. Looks like the relevant constant is OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
based on what I see in https://github.com/LaunchKey/launchkey-ruby/blob/master/lib/launchkey/rsa_key.rb
end | ||
|
||
def sign(text, private_key = key_maker.server_key) | ||
encode(private_key.sign(DIGEST, encode(text))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Ruby docs don't say, but I'm pretty sure sign
uses PKCS1v1.5 mode padding, which should be avoided in enw applications, and PSS (Probabilistic Signature Scheme) used isntead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/jondistad/openssl_rsa_pss_verify makes me think that the ruby openssl bindings don't yet support PSS. I'll keep looking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only skimmed the raw crypto, I haven't reviewed the overall design in any real detail. |
Sounds plausible! On Mon, Oct 3, 2016 at 3:55 PM, Peter Karman notifications@github.com
"I disapprove of what you say, but I will defend to the death your right to |
(For your awareness, I intend to do a more complete review of this tomorrow) |
idv_session.resolution = resolution | ||
idv_session.question_number = 0 | ||
idv_session.profile_from_applicant(idv_session.applicant, password) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this related to this PR? If not, move to a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's related in the sense that reek complained of too many methods in the controller, so a slight refactor was necessary to get codeclimate passing.
idv_session.question_number = 0 | ||
idv_session.profile_from_applicant(idv_session.applicant) | ||
def invalid_password? | ||
!current_user.valid_password?(password) | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes here don't seem to be related to encrypting PII. Do you mind removing the changes that are not specific to encryption to make this PR smaller and easier to review?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see comment above about reek.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, by adding the password confirmation feature to this controller, it made the controller too big for reek, so you had to move some methods into concerns/idv_session.rb
and services/idv/session.rb
right? My question is about this confirm_current_password
feature. It doesn't seem related to PII encryption, but I could be wrong. If it's not related, then several changes related/caused by this feature could be moved to a different PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the user's password is used in order to encrypt their PII, we must collect that password at the moment we intend to encrypt it. Likewise, we use their password at log-in time to decrypt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that makes sense.
**Why**: Requires less space and we are already one-way hashing.
I am splitting this PR into 3 different PRs to make reviewing easier. #598 is a small refactor to satisfy codeclimate This PR will, once the other 2 are merged, reflect only the encryption code itself, as most of the comment threads here are about the encryption model and implementation. |
I am aware of test failures and merge conflict. I will fix these once #599 is merged. |
Will create a new PR once encryption model is finalized. |
Why: All PII must be encrypted at rest.
Storing attributes in a single JSON blob makes this much easier.
How:
The encryption architecture is similar to a hotel. Each user gets their
own room. The hotel management has a master key to every room.
Inside each room is a safe, where the PII itself is stored,
and only the user has the combination to the safe. If the user loses
their room key, the management can replace it. If the user loses
the safe combination, the safe can never be opened.
All PII is encrypted at rest in 2 layers: once with a unique
private key encrypted with the user's own password, and once
with a server-wide private key. The user's private key
is enclosed within the server-wide-key encrypted payload.
Encryption happens at the time a Profile is saved. Each time the
user logs in, the PII is decrypted using the password the user
provides. The PII is then re-encrypted within the session, using
the server-wide private key (just one layer of encryption).
The PII can be decrypted as-needed during the session (as for
SAML responses and displaying profile information to the user)
using the server-wide key.
NOTE: This PR does not include the following features, which
will be implemented in a follow-on PR: