Skip to content

Commit

Permalink
Read 2L-KMS encrypted sessions (#2367)
Browse files Browse the repository at this point in the history
**Why**: To start the migration to 2L-KMS. This commit wraps an AES
encrypted ciphertext with KMS. This commit allows new and old instances
to exist in parallel when we start writing 2L-KMS encrypted cihpertexts
to the session.
  • Loading branch information
jmhooper authored Jul 23, 2018
1 parent b182b75 commit a859b03
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 77 deletions.
28 changes: 28 additions & 0 deletions app/services/encryption/encryptors/deprecated_session_encryptor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Encryption
module Encryptors
class DeprecatedSessionEncryptor
def encrypt(plaintext)
user_access_key = self.class.load_or_init_user_access_key
UserAccessKeyEncryptor.new(user_access_key).encrypt(plaintext)
end

def decrypt(ciphertext)
user_access_key = self.class.load_or_init_user_access_key
UserAccessKeyEncryptor.new(user_access_key).decrypt(ciphertext)
end

def self.load_or_init_user_access_key
if @user_access_key_scrypt_hash.present?
return UserAccessKey.new(scrypt_hash: @user_access_key_scrypt_hash)
end

key = Figaro.env.session_encryption_key
user_access_key = UserAccessKey.new(
password: key, salt: OpenSSL::Digest::SHA256.hexdigest(key)
)
@user_access_key_scrypt_hash = user_access_key.as_scrypt_hash
user_access_key
end
end
end
end
40 changes: 23 additions & 17 deletions app/services/encryption/encryptors/session_encryptor.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
module Encryption
module Encryptors
class SessionEncryptor
def encrypt(plaintext)
user_access_key = self.class.load_or_init_user_access_key
UserAccessKeyEncryptor.new(user_access_key).encrypt(plaintext)
end
include Encodable

delegate :encrypt, to: :deprecated_encryptor

def decrypt(ciphertext)
user_access_key = self.class.load_or_init_user_access_key
UserAccessKeyEncryptor.new(user_access_key).decrypt(ciphertext)
return deprecated_encryptor.decrypt(ciphertext) if legacy?(ciphertext)

aes_ciphertext = KmsClient.new.decrypt(decode(ciphertext))
aes_encryptor.decrypt(aes_ciphertext, aes_encryption_key)
end

private

def legacy?(ciphertext)
ciphertext.index('.')
end

def aes_encryptor
AesEncryptor.new
end

def aes_encryption_key
Figaro.env.session_encryption_key[0...32]
end

def self.load_or_init_user_access_key
if @user_access_key_scrypt_hash.present?
return UserAccessKey.new(scrypt_hash: @user_access_key_scrypt_hash)
end

key = Figaro.env.session_encryption_key
user_access_key = UserAccessKey.new(
password: key, salt: OpenSSL::Digest::SHA256.hexdigest(key)
)
@user_access_key_scrypt_hash = user_access_key.as_scrypt_hash
user_access_key
def deprecated_encryptor
DeprecatedSessionEncryptor.new
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'rails_helper'

describe Encryption::Encryptors::DeprecatedSessionEncryptor do
let(:plaintext) { '{ "foo": "bar" }' }

before do
described_class.instance_variable_set(:@user_access_key_scrypt_hash, nil)
end

describe '#encrypt' do
it 'returns encrypted text' do
ciphertext = subject.encrypt(plaintext)

expect(ciphertext).to_not eq(plaintext)
end

it 'only computes an scrypt hash on the first encryption' do
expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original

subject.encrypt(plaintext)

expect(SCrypt::Engine).to_not receive(:hash_secret)

subject.encrypt(plaintext)
end
end

describe '#decrypt' do
let(:ciphertext) do
result = subject.encrypt(plaintext)
described_class.instance_variable_set(:@user_access_key_scrypt_hash, nil)
result
end

before do
# Memoize the ciphertext and purge memoized key so that encryption does not
# affect expected call counts
ciphertext
end

it 'returns a decrypted ciphertext' do
expect(subject.decrypt(ciphertext)).to eq(plaintext)
end

it 'only computes and scrypt hash on the first decryption' do
expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original

subject.decrypt(ciphertext)

expect(SCrypt::Engine).to_not receive(:hash_secret)

subject.decrypt(ciphertext)
end
end

describe '.load_or_init_user_access_key' do
it 'does not return the same key object for the same salt and cost' do
expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original

key1 = described_class.load_or_init_user_access_key
key2 = described_class.load_or_init_user_access_key

expect(key1.as_scrypt_hash).to eq(key2.as_scrypt_hash)
expect(key1).to_not eq(key2)
end
end

it 'makes a roundtrip across multiple encryptors' do
encryptor1 = described_class.new
encryptor2 = described_class.new

# Memoize user access key scrypt hash
encryptor1.decrypt(encryptor1.encrypt('asdf'))
encryptor2.decrypt(encryptor2.encrypt('1234'))

encrypted_text = encryptor1.encrypt(plaintext)
expect(encryptor2.decrypt(encrypted_text)).to eq(plaintext)
end
end
86 changes: 26 additions & 60 deletions spec/services/encryption/encryptors/session_encryptor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,43 @@
describe Encryption::Encryptors::SessionEncryptor do
let(:plaintext) { '{ "foo": "bar" }' }

before do
described_class.instance_variable_set(:@user_access_key_scrypt_hash, nil)
end

describe '#encrypt' do
it 'returns encrypted text' do
ciphertext = subject.encrypt(plaintext)

expect(ciphertext).to_not eq(plaintext)
end

it 'only computes an scrypt hash on the first encryption' do
expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original
it 'returns ciphertext created by the deprecated session encryptor' do
expected_ciphertext = '123abc'

subject.encrypt(plaintext)
deprecated_encryptor = Encryption::Encryptors::DeprecatedSessionEncryptor.new
expect(deprecated_encryptor).to receive(:encrypt).
with(plaintext).
and_return(expected_ciphertext)
expect(Encryption::Encryptors::DeprecatedSessionEncryptor).to receive(:new).
and_return(deprecated_encryptor)

expect(SCrypt::Engine).to_not receive(:hash_secret)
ciphertext = subject.encrypt(plaintext)

subject.encrypt(plaintext)
expect(ciphertext).to eq(expected_ciphertext)
end
end

describe '#decrypt' do
let(:ciphertext) do
result = subject.encrypt(plaintext)
described_class.instance_variable_set(:@user_access_key_scrypt_hash, nil)
result
end

before do
# Memoize the ciphertext and purge memoized key so that encryption does not
# affect expected call counts
ciphertext
end

it 'returns a decrypted ciphertext' do
expect(subject.decrypt(ciphertext)).to eq(plaintext)
end

it 'only computes and scrypt hash on the first decryption' do
expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original

subject.decrypt(ciphertext)
context 'with a legacy ciphertext' do
let(:ciphertext) { Encryption::Encryptors::DeprecatedSessionEncryptor.new.encrypt(plaintext) }

expect(SCrypt::Engine).to_not receive(:hash_secret)

subject.decrypt(ciphertext)
it 'decrypts the ciphertext' do
expect(subject.decrypt(ciphertext)).to eq(plaintext)
end
end
end

describe '.load_or_init_user_access_key' do
it 'does not return the same key object for the same salt and cost' do
expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original

key1 = described_class.load_or_init_user_access_key
key2 = described_class.load_or_init_user_access_key

expect(key1.as_scrypt_hash).to eq(key2.as_scrypt_hash)
expect(key1).to_not eq(key2)
context 'with a 2L-KMS ciphertext' do
let(:ciphertext) do
key = Figaro.env.session_encryption_key[0...32]
aes_ciphertext = Encryption::Encryptors::AesEncryptor.new.encrypt(plaintext, key)
kms_ciphertext = Encryption::KmsClient.new.encrypt(aes_ciphertext)
Base64.strict_encode64(kms_ciphertext)
end

it 'decrypts the ciphertext' do
expect(subject.decrypt(ciphertext)).to eq(plaintext)
end
end
end

it 'makes a roundtrip across multiple encryptors' do
encryptor1 = described_class.new
encryptor2 = described_class.new

# Memoize user access key scrypt hash
encryptor1.decrypt(encryptor1.encrypt('asdf'))
encryptor2.decrypt(encryptor2.encrypt('1234'))

encrypted_text = encryptor1.encrypt(plaintext)
expect(encryptor2.decrypt(encrypted_text)).to eq(plaintext)
end
end

0 comments on commit a859b03

Please sign in to comment.