-
Notifications
You must be signed in to change notification settings - Fork 166
Reduce session size with encoding and compression improvements #7703
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
72c98d7
Reduce session size with encoding and compression changes
154b458
remove metric logging
bfe1c28
add msgpack to Gemfile
bc438c2
Update lib/legacy_session_encryptor.rb
728da51
rename Small -> V2
40aab9e
rename Legacy back to regular
09028ae
add specs
035d887
add "deprecation" warnings
b6e8733
add frozen_string_literal
15ed9b1
constantize session payload keys
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Encryption | ||
| class AesCipherV2 | ||
| def encrypt(plaintext, cek) | ||
| self.cipher = self.class.encryption_cipher | ||
| # The key length for the AES-256-GCM cipher is fixed at 128 bits, or 32 | ||
| # characters. Starting with Ruby 2.4, an expection is thrown if you try to | ||
| # set a key longer than 32 characters, which is what we have been doing | ||
| # all along. In prior versions of Ruby, the key was silently truncated. | ||
| cipher.key = cek[0..31] | ||
| encipher(plaintext) | ||
| end | ||
|
|
||
| def decrypt(payload, cek) | ||
| self.cipher = OpenSSL::Cipher.new 'aes-256-gcm' | ||
| cipher.decrypt | ||
| cipher.key = cek[0..31] | ||
| decipher(payload) | ||
| end | ||
|
|
||
| def self.encryption_cipher | ||
| OpenSSL::Cipher.new('aes-256-gcm').encrypt | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_accessor :cipher | ||
|
|
||
| def encipher(plaintext) | ||
| iv = cipher.random_iv | ||
| cipher.auth_data = 'PII' | ||
| ciphertext = cipher.update(plaintext) << cipher.final | ||
| tag = cipher.auth_tag | ||
|
|
||
| { iv: iv, ciphertext: ciphertext, tag: tag }.to_msgpack | ||
| end | ||
|
|
||
| def decipher(payload) | ||
| unpacked_payload = unpack_payload(payload) | ||
| cipher.iv = iv(unpacked_payload) | ||
| cipher.auth_tag = tag(unpacked_payload) | ||
| cipher.auth_data = 'PII' | ||
| try_decipher(unpacked_payload) | ||
| end | ||
|
|
||
| def try_decipher(unpacked_payload) | ||
| cipher.update(ciphertext(unpacked_payload)) << cipher.final | ||
| rescue OpenSSL::Cipher::CipherError => err | ||
| raise EncryptionError, "failed to decipher payload: #{err}" | ||
| end | ||
|
|
||
| def unpack_payload(payload) | ||
| MessagePack.unpack(payload) | ||
| rescue StandardError | ||
| raise EncryptionError, 'Unable to parse encrypted payload' | ||
| end | ||
|
|
||
| def iv(unpacked_payload) | ||
| unpacked_payload['iv'] | ||
| end | ||
|
|
||
| def tag(unpacked_payload) | ||
| unpacked_payload['tag'] | ||
| end | ||
|
|
||
| def ciphertext(unpacked_payload) | ||
| unpacked_payload['ciphertext'] | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| module Encryption | ||
| module Encryptors | ||
| class AesEncryptorV2 | ||
| def initialize | ||
| self.cipher = AesCipherV2.new | ||
| end | ||
|
|
||
| def encrypt(plaintext, cek) | ||
| payload = fingerprint_and_concat(plaintext) | ||
| cipher.encrypt(payload, cek) | ||
| end | ||
|
|
||
| def decrypt(ciphertext, cek) | ||
| decrypt_and_test_payload(ciphertext, cek) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_accessor :cipher | ||
|
|
||
| def fingerprint_and_concat(plaintext) | ||
| fingerprint = Pii::Fingerprinter.fingerprint(plaintext) | ||
| [plaintext, fingerprint].to_msgpack | ||
| end | ||
|
|
||
| def decrypt_and_test_payload(payload, cek) | ||
| begin | ||
| plaintext, fingerprint = MessagePack.unpack(cipher.decrypt(payload, cek)) | ||
| rescue OpenSSL::Cipher::CipherError, MessagePack::MalformedFormatError => err | ||
| raise EncryptionError, err.inspect | ||
| end | ||
| return plaintext if Pii::Fingerprinter.verify(plaintext, fingerprint) | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,150 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class LegacySessionEncryptor | ||
| CIPHERTEXT_HEADER = 'v2' | ||
|
mitchellhenke marked this conversation as resolved.
|
||
| def load(value) | ||
| decrypted = encryptor.decrypt(value) | ||
| _v2, ciphertext = value.split(':') | ||
| decrypted = outer_decrypt(ciphertext) | ||
|
|
||
| session = JSON.parse(decrypted, quirks_mode: true).with_indifferent_access | ||
| kms_decrypt_sensitive_paths!(session) | ||
|
|
||
| JSON.parse(decrypted, quirks_mode: true).with_indifferent_access | ||
| session | ||
| end | ||
|
|
||
| def dump(value) | ||
| value.deep_stringify_keys! | ||
|
|
||
| kms_encrypt_pii!(value) | ||
| kms_encrypt_sensitive_paths!(value, SessionEncryptor::SENSITIVE_PATHS) | ||
| alert_or_raise_if_contains_sensitive_keys!(value) | ||
| plain = JSON.generate(value, quirks_mode: true) | ||
| encryptor.encrypt(plain) | ||
| alert_or_raise_if_contains_sensitive_value!(plain, value) | ||
| CIPHERTEXT_HEADER + ':' + outer_encrypt(plain) | ||
| end | ||
|
|
||
| def kms_encrypt(text) | ||
| Base64.encode64(Encryption::KmsClient.new.encrypt(text, 'context' => 'session-encryption')) | ||
| end | ||
|
|
||
| def kms_decrypt(text) | ||
| Encryption::KmsClient.new.decrypt( | ||
| Base64.decode64(text), 'context' => 'session-encryption' | ||
| ) | ||
| end | ||
|
|
||
| def outer_encrypt(plaintext) | ||
| Encryption::Encryptors::AesEncryptor.new.encrypt(plaintext, session_encryption_key) | ||
| end | ||
|
|
||
| def outer_decrypt(ciphertext) | ||
| Encryption::Encryptors::AesEncryptor.new.decrypt(ciphertext, session_encryption_key) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def encryptor | ||
| Encryption::Encryptors::SessionEncryptor.new | ||
| # The PII bundle is stored in the user session in the 'decrypted_pii' key. | ||
| # The PII is decrypted with the user's password when they successfully submit it and then | ||
| # stored in the session. Before saving the session, this method encrypts the PII with KMS and | ||
| # stores it in the 'encrypted_pii' key. | ||
| # | ||
| # The PII is not frequently needed in its KMS-decrypted state. To reduce the | ||
| # risks around holding plaintext PII in memory during requests, this PII is KMS-decrypted | ||
| # on-demand by the Pii::Cacher. | ||
| def kms_encrypt_pii!(session) | ||
| return unless session.dig('warden.user.user.session', 'decrypted_pii') | ||
| decrypted_pii = session['warden.user.user.session'].delete('decrypted_pii') | ||
| session['warden.user.user.session']['encrypted_pii'] = | ||
| kms_encrypt(decrypted_pii) | ||
| nil | ||
| end | ||
|
|
||
| # This method extracts all of the sensitive paths that exist into a | ||
| # separate hash. This separate hash is then encrypted and placed in the session. | ||
| # We use #reduce to build the nested empty hash if needed. If Hash#bury | ||
| # (https://bugs.ruby-lang.org/issues/11747) existed, we could use that instead. | ||
| def kms_encrypt_sensitive_paths!(session, sensitive_paths) | ||
| sensitive_data = {} | ||
|
|
||
| sensitive_paths.each do |path| | ||
| *all_but_last_key, last_key = path | ||
|
|
||
| if all_but_last_key.blank? | ||
| value = session.delete(last_key) | ||
| else | ||
| value = session.dig(*all_but_last_key)&.delete(last_key) | ||
| end | ||
|
|
||
| if value | ||
| all_but_last_key.reduce(sensitive_data) do |hash, key| | ||
| hash[key] ||= {} | ||
|
|
||
| hash[key] | ||
| end | ||
|
|
||
| if all_but_last_key.blank? | ||
| sensitive_data[last_key] = value | ||
| else | ||
| sensitive_data.dig(*all_but_last_key).store(last_key, value) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| raise "invalid session, 'sensitive_data' is reserved key" if session['sensitive_data'].present? | ||
| return if sensitive_data.blank? | ||
| session['sensitive_data'] = kms_encrypt(JSON.generate(sensitive_data)) | ||
| end | ||
|
|
||
| # This method reverses the steps taken in #kms_encrypt_sensitive_paths! | ||
| # The encrypted hash is decrypted and then deep merged into the session hash. | ||
| # The merge must be a deep merge to avoid collisions with existing hashes in the | ||
| # session. | ||
| def kms_decrypt_sensitive_paths!(session) | ||
| sensitive_data = session.delete('sensitive_data') | ||
| return if sensitive_data.blank? | ||
|
|
||
| sensitive_data = JSON.parse( | ||
| kms_decrypt(sensitive_data), quirks_mode: true | ||
| ) | ||
|
|
||
| session.deep_merge!(sensitive_data) | ||
| end | ||
|
|
||
| def alert_or_raise_if_contains_sensitive_value!(string, hash) | ||
| if SessionEncryptor::SENSITIVE_REGEX.match?(string) | ||
| exception = SessionEncryptor::SensitiveValueError.new | ||
| if IdentityConfig.store.session_encryptor_alert_enabled | ||
| NewRelic::Agent.notice_error( | ||
| exception, custom_params: { | ||
| session_structure: hash.deep_transform_values { |v| nil }, | ||
| } | ||
| ) | ||
| else | ||
| raise exception | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def alert_or_raise_if_contains_sensitive_keys!(hash) | ||
| hash.deep_transform_keys do |key| | ||
| if SessionEncryptor::SENSITIVE_KEYS.include?(key.to_s) | ||
| exception = SessionEncryptor::SensitiveKeyError.new( | ||
| "#{key} unexpectedly appeared in session", | ||
| ) | ||
| if IdentityConfig.store.session_encryptor_alert_enabled | ||
| NewRelic::Agent.notice_error( | ||
| exception, custom_params: { | ||
| session_structure: hash.deep_transform_values { |_v| '' }, | ||
| } | ||
| ) | ||
| else | ||
| raise exception | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def session_encryption_key | ||
| IdentityConfig.store.session_encryption_key | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.