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

Don't reuse IVs and add future placeholder for key rotation. #422

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions lib/aca_entities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'aca_entities/error'

require 'aca_entities/configuration/encryption'
require 'aca_entities/encryption'
require 'aca_entities/operations/mongoid/model_adapter'

require 'aca_entities/libraries/aca_individual_market_library'
Expand Down
10 changes: 10 additions & 0 deletions lib/aca_entities/encryption.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

require_relative "encryption/symmetric"

module AcaEntities
# Manages encryption and encryption primatives for ACA Entities
# and associated payloads.
module Encryption
end
end
45 changes: 45 additions & 0 deletions lib/aca_entities/encryption/symmetric.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require "base64"
require_relative "symmetric/legacy_keyset"
require_relative "symmetric/key_manager"
require_relative "symmetric/encrypted_payload"
require_relative "symmetric/legacy_encrypted_payload"
require_relative "symmetric/parse_encrypted_payload"
require_relative "symmetric/decrypt_payload"
require_relative "symmetric/encrypt_payload"

module AcaEntities
module Encryption
# Management and algorithms for symmetric algorithms, supported by
# libsodium.
module Symmetric
# Algorithm implementation versions.
#
# Right now we have only Version 1.
ALGO_VERSIONS = ["S1"].freeze
CURRENT_ALGO_VERSION = "S1"

class InvalidPayloadHeaderError < StandardError; end

class KeyNotFoundError < StandardError; end

# Manages nonces for symmetric encryption
class Nonce
def self.generate(byte_size)
RbNaCl::Random.random_bytes(byte_size)
end
end

def decrypt(payload)
DecryptPayload.new.call(payload)
end

def encrypt(payload)
EncryptPayload.new.call(payload)
end

module_function :decrypt, :encrypt
end
end
end
47 changes: 47 additions & 0 deletions lib/aca_entities/encryption/symmetric/decrypt_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require 'dry/monads'
require 'dry/monads/do'

module AcaEntities
module Encryption
module Symmetric
# Decrypt a payload.
class DecryptPayload
send(:include, Dry::Monads[:result, :do])
send(:include, Dry::Monads[:try])

def call(payload)
encrypted_payload = yield parse_header_and_payload(payload)
decrypt_payload(encrypted_payload)
end

def parse_header_and_payload(payload)
ParseEncryptedPayload.new.call(payload)
end

def decrypt_payload(encrypted_payload)
if encrypted_payload.header?
found_key = yield lookup_key(encrypted_payload)
decryption_result = Try do
decryption_box = RbNaCl::SecretBox.new(found_key)
decryption_box.decrypt(encrypted_payload.nonce, encrypted_payload.content)
end
decryption_result.to_result
else
secret_box = RbNaCl::SecretBox.new(LegacyKeyset.secret_key)
Success(secret_box.decrypt(LegacyKeyset.iv, Base64.decode64(encrypted_payload.content)))
end
end

def lookup_key(encrypted_payload)
key_result = Try do
KeyManager.resolve_key(encrypted_payload.algo_version, encrypted_payload.key_version)
end

key_result.to_result
end
end
end
end
end
51 changes: 51 additions & 0 deletions lib/aca_entities/encryption/symmetric/encrypt_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

require 'dry/monads'
require 'dry/monads/do'

module AcaEntities
module Encryption
module Symmetric
# Encrypte a payload.
class EncryptPayload
send(:include, Dry::Monads[:result, :do])
send(:include, Dry::Monads[:try])

def call(payload)
encrypted = yield construct_encrypted_payload(payload)
encode_payload(encrypted)
end

def construct_encrypted_payload(payload)
encryption_attempt = Try do
current_algo_v = KeyManager.current_algo_version
current_key_v = KeyManager.current_key_version
key = KeyManager.resolve_key(current_algo_v, current_key_v)
encrypt_box = RbNaCl::SecretBox.new(key)
nonce = Nonce.generate(encrypt_box.nonce_bytes)
content = encrypt_box.encrypt(nonce, payload)
EncryptedPayload.new(
current_algo_v,
current_key_v,
nonce,
content
)
end
encryption_attempt.to_result
end

def encode_payload(encrypted)
encoded_result = Try do
# rubocop:disable Style/StringConcatenation
encrypted.algo_version + "." + encrypted.key_version + "." +
Base64.strict_encode64(encrypted.nonce) + "." +
Base64.strict_encode64(encrypted.content)
# rubocop:enable Style/StringConcatenation
end

encoded_result.to_result
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/aca_entities/encryption/symmetric/encrypted_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module AcaEntities
module Encryption
module Symmetric
# A standard payload with a header for encryption.
class EncryptedPayload
attr_reader :algo_version, :key_version, :nonce, :content

def initialize(a_version, k_version, nonce_value, content_value)
@algo_version = a_version
@key_version = k_version
@nonce = nonce_value
@content = content_value
end

def header?
true
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/aca_entities/encryption/symmetric/key_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module AcaEntities
module Encryption
module Symmetric
# Manages the selection of a key and algorithm using version headers
class KeyManager
def self.current_algo_version
CURRENT_ALGO_VERSION
end

# Right now, since we're using the legacy configuration as our source,
# we'll tag our key version as 'L'.
def self.current_key_version
"L"
end

# Eventually we will replace this with a dynamic lookup to allow key
# rotation.
def self.resolve_key(_algo_version, _key_version)
LegacyKeyset.secret_key
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/aca_entities/encryption/symmetric/legacy_encrypted_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module AcaEntities
module Encryption
module Symmetric
# A payload from before we versioned our algorithms or keys.
class LegacyEncryptedPayload
attr_reader :content

def initialize(content_value)
@content = content_value
end

def header?
false
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/aca_entities/encryption/symmetric/legacy_keyset.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module AcaEntities
module Encryption
module Symmetric
# Manages key settings for the 'original' implementation.
class LegacyKeyset
def self.secret_key
key = AcaEntities::Configuration::Encryption.config.secret_key
[key].pack("H*")
end

def self.iv
AcaEntities::Configuration::Encryption.config.iv
end
end
end
end
end
50 changes: 50 additions & 0 deletions lib/aca_entities/encryption/symmetric/parse_encrypted_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require 'dry/monads'
require 'dry/monads/do'

module AcaEntities
module Encryption
module Symmetric
# Parse the header and body for an encrypted payload.
#
# We might return an error in the case of an invalid payload, or we
# we might return an empty payload, as the current data was provided
# in the 'legacy' encryption format.
class ParseEncryptedPayload
send(:include, Dry::Monads[:result, :do])
send(:include, Dry::Monads[:try])

def call(payload)
return Success(LegacyEncryptedPayload.new(payload)) if payload.blank?
return Success(LegacyEncryptedPayload.new(payload)) unless payload.include?(".")

payload_parts = yield split_parts(payload)
parse_header_values(payload_parts)
end

def split_parts(payload)
split_attempt = Try do
payload_parts = payload.split(".").map(&:strip)
raise InvalidPayloadHeaderError, "Payload only had #{payload_parts.length} parts, expected 4." if payload_parts.length < 4
payload_parts
end

split_attempt.to_result
end

def parse_header_values(payload_parts)
parse_attempt = Try do
algo_version = payload_parts.first
key_version = payload_parts[1]
nonce = Base64.decode64(payload_parts[2])
content = Base64.decode64(payload_parts[3])
EncryptedPayload.new(algo_version, key_version, nonce, content)
end

parse_attempt.to_result
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ def applicant_entity(applicant_hash)
def find_response_for_applicant(applicant, esi_response)
esi_response[:esiMECResponse][:applicantResponseArray].detect do |applicant_response|
ssn = applicant_response[:personSocialSecurityNumber]
encrypted_ssn = AcaEntities::Operations::Encryption::Encrypt.new.call({ value: ssn }).value!
applicant[:identifying_information][:encrypted_ssn] == encrypted_ssn
encrypted_ssn = applicant[:identifying_information][:encrypted_ssn]
decrypted_ssn = AcaEntities::Operations::Encryption::Decrypt.new.call({ value: encrypted_ssn }).value!
ssn == decrypted_ssn
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ def applicant_entity(applicant_hash)
def find_response_for_applicant(applicant, non_esi_response)
non_esi_response[:verifyNonESIMECResponse][:individualResponseArray].detect do |individual_response|
ssn = individual_response[:personSocialSecurityNumber]
encrypted_ssn = AcaEntities::Operations::Encryption::Encrypt.new.call({ value: ssn }).value!
applicant[:identifying_information][:encrypted_ssn] == encrypted_ssn
encrypted_ssn = applicant[:identifying_information][:encrypted_ssn]
decrypted_ssn = AcaEntities::Operations::Encryption::Decrypt.new.call({ value: encrypted_ssn }).value!
ssn == decrypted_ssn
end
end
end
Expand Down
13 changes: 1 addition & 12 deletions lib/aca_entities/operations/encryption/decrypt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,7 @@ class Decrypt

# @param [hash] pass in value to be encrypted
def call(params)
decrypted_value = yield decrypt(params[:value])

Success(decrypted_value)
end

private

def decrypt(value)
key = AcaEntities::Configuration::Encryption.config.secret_key
iv = AcaEntities::Configuration::Encryption.config.iv
secret_box = RbNaCl::SecretBox.new([key].pack("H*"))
Success(secret_box.decrypt(iv, Base64.decode64(value)))
AcaEntities::Encryption::Symmetric.decrypt(params[:value])
end
end
end
Expand Down
13 changes: 1 addition & 12 deletions lib/aca_entities/operations/encryption/encrypt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,7 @@ class Encrypt

# @param [hash] pass in value to be encrypted
def call(params)
encrypted_value = yield encrypt(params[:value])

Success(encrypted_value)
end

private

def encrypt(value)
key = AcaEntities::Configuration::Encryption.config.secret_key
iv = AcaEntities::Configuration::Encryption.config.iv
secret_box = RbNaCl::SecretBox.new([key].pack("H*"))
Success(Base64.encode64(secret_box.encrypt(iv, value)))
AcaEntities::Encryption::Symmetric.encrypt(params[:value])
end
end
end
Expand Down
Loading
Loading