Skip to content
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

Closed
wants to merge 19 commits into from
Closed

Encrypt PII at rest #498

wants to merge 19 commits into from

Conversation

pkarman
Copy link
Contributor

@pkarman pkarman commented Sep 23, 2016

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:

  • re-encrypt PII when password changed
  • mark profile.active=false when password reset
  • encrypt IdV session at rest

DELIMITER = '.'.freeze
DIGEST = OpenSSL::Digest::SHA256.new.freeze

# structure of the encrypted payload:
Copy link
Contributor Author

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

@pkarman
Copy link
Contributor Author

pkarman commented Sep 30, 2016

@paultag @alex @bandrzej would love your 👀 on this one as well, especially app/services/pii/encryptor.rb

Copy link
Contributor

@jessieay jessieay left a 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')
Copy link
Contributor

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)
Copy link
Contributor

@jessieay jessieay Oct 3, 2016

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
''
Copy link
Contributor

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?

Copy link
Contributor Author

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?
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

@pkarman pkarman Oct 3, 2016

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.

Copy link

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)
Copy link
Contributor

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)

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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!
Copy link
Contributor

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')
Copy link
Contributor

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?

Copy link
Contributor Author

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))
Copy link

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)

Copy link
Contributor Author

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)))
Copy link

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex
Copy link

alex commented Oct 3, 2016

I only skimmed the raw crypto, I haven't reviewed the overall design in any real detail.

@alex
Copy link

alex commented Oct 3, 2016

Sounds plausible!

On Mon, Oct 3, 2016 at 3:55 PM, Peter Karman notifications@github.com
wrote:

@pkarman commented on this pull request.

In app/services/pii/encryptor.rb
#498:

  • end
  • def decrypt(ciphertext, password)
  •  user_payload = decrypt_with_key(ciphertext, key_maker.server_key)
    
  •  return unless user_payload
    
  •  user_private_key_pem, user_ciphertext = split_into_segments(user_payload)
    
  •  user_private_key = rsa_key(user_private_key_pem, password)
    
  •  decrypt_with_key(user_ciphertext, user_private_key)
    
  • end
  • DHS/NIST algorithm

  • def encrypt_with_key(plaintext, private_key)
  •  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))
    

thanks @alex https://github.com/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


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#498, or mute the thread
https://github.com/notifications/unsubscribe-auth/AAADBDOZjmscnWRYAZyQyuUAK_kiTfdoks5qwV3HgaJpZM4KFGRq
.

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: D1B3 ADC0 E023 8CA6

@alex
Copy link

alex commented Oct 4, 2016

(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
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense.

@pkarman
Copy link
Contributor Author

pkarman commented Oct 22, 2016

I am splitting this PR into 3 different PRs to make reviewing easier.

#598 is a small refactor to satisfy codeclimate
#599 is the model change and UX change to collect passphrase during IdV

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.

@pkarman
Copy link
Contributor Author

pkarman commented Oct 24, 2016

@alex OpenSSL verify() implemented in fe3e3e2

@alex @monfresh aes-256-gcm implemented in bfa7704

@pkarman
Copy link
Contributor Author

pkarman commented Oct 24, 2016

I am aware of test failures and merge conflict. I will fix these once #599 is merged.

@pkarman
Copy link
Contributor Author

pkarman commented Oct 26, 2016

Will create a new PR once encryption model is finalized.

@pkarman pkarman closed this Oct 26, 2016
@pkarman pkarman deleted the encrypt-pii branch November 14, 2016 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants