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

Fix thread safety issue in Symmetric cipher #31

Merged
merged 2 commits into from
Feb 10, 2023
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# v3.0.1

* The symmetric cipher class now encrypts and decrypts in a thread-safe manner.
[cyberark/slosilo#31](https://github.com/cyberark/slosilo/pull/31)

# v3.0.0

* Transition to Ruby 3. Consuming projects based on Ruby 2 shall use slosilo V2.X.X.
Expand Down
7 changes: 7 additions & 0 deletions dev/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM ruby

COPY ./ /src/

WORKDIR /src

RUN bundle
8 changes: 8 additions & 0 deletions dev/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: '3'
services:
dev:
build:
context: ..
dockerfile: dev/Dockerfile.dev
volumes:
- ../:/src
43 changes: 26 additions & 17 deletions lib/slosilo/symmetric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Symmetric

def initialize
@cipher = OpenSSL::Cipher.new 'aes-256-gcm' # NB: has to be lower case for whatever reason.
@cipher_mutex = Mutex.new
end

# This lets us do a final sanity check in migrations from older encryption versions
Expand All @@ -13,34 +14,42 @@ def cipher_name
end

def encrypt plaintext, opts = {}
@cipher.reset
@cipher.encrypt
@cipher.key = (opts[:key] or raise("missing :key option"))
@cipher.iv = iv = random_iv
@cipher.auth_data = opts[:aad] || "" # Nothing good happens if you set this to nil, or don't set it at all
ctext = @cipher.update(plaintext) + @cipher.final
tag = @cipher.auth_tag(TAG_LENGTH)
"#{VERSION_MAGIC}#{tag}#{iv}#{ctext}"
# All of these operations in OpenSSL must occur atomically, so we
# synchronize their access to make this step thread-safe.
@cipher_mutex.synchronize do
@cipher.reset
@cipher.encrypt
@cipher.key = (opts[:key] or raise("missing :key option"))
@cipher.iv = iv = random_iv
@cipher.auth_data = opts[:aad] || "" # Nothing good happens if you set this to nil, or don't set it at all
ctext = @cipher.update(plaintext) + @cipher.final
tag = @cipher.auth_tag(TAG_LENGTH)
"#{VERSION_MAGIC}#{tag}#{iv}#{ctext}"
end
end

def decrypt ciphertext, opts = {}
version, tag, iv, ctext = unpack ciphertext

raise "Invalid version magic: expected #{VERSION_MAGIC} but was #{version}" unless version == VERSION_MAGIC

@cipher.reset
@cipher.decrypt
@cipher.key = opts[:key]
@cipher.iv = iv
@cipher.auth_tag = tag
@cipher.auth_data = opts[:aad] || ""
@cipher.update(ctext) + @cipher.final
# All of these operations in OpenSSL must occur atomically, so we
# synchronize their access to make this step thread-safe.
@cipher_mutex.synchronize do
@cipher.reset
@cipher.decrypt
@cipher.key = opts[:key]
@cipher.iv = iv
@cipher.auth_tag = tag
@cipher.auth_data = opts[:aad] || ""
@cipher.update(ctext) + @cipher.final
end
end

def random_iv
@cipher.random_iv
end

def random_key
@cipher.random_key
end
Expand Down
2 changes: 1 addition & 1 deletion lib/slosilo/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Slosilo
VERSION = "3.0.0"
VERSION = "3.0.1"
end
25 changes: 23 additions & 2 deletions spec/symmetric_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,29 @@
expect(subject.encrypt(plaintext, key: key, aad: auth_data)).to eq(ciphertext)
end
end

describe '#decrypt' do

it "doesn't fail when called by multiple threads" do
threads = []

begin
# Verify we can successfuly decrypt using many threads without OpenSSL
# errors.
1000.times do
threads << Thread.new do
100.times do
expect(
subject.decrypt(ciphertext, key: key, aad: auth_data)
).to eq(plaintext)
end
end
end
ensure
threads.each(&:join)
end
end

it "decrypts with AES-256-GCM" do
expect(subject.decrypt(ciphertext, key: key, aad: auth_data)).to eq(plaintext)
end
Expand Down Expand Up @@ -56,7 +77,7 @@
end
end
end

describe '#random_iv' do
it "generates a random iv" do
expect_any_instance_of(OpenSSL::Cipher).to receive(:random_iv).and_return :iv
Expand Down