Skip to content

Commit

Permalink
Merge pull request #31 from cyberark/cnjr-479-thread-safe-symmetric-c…
Browse files Browse the repository at this point in the history
…ipher

Fix thread safety issue in Symmetric cipher
  • Loading branch information
micahlee authored Feb 10, 2023
2 parents 6624c22 + c9408a0 commit 56f19ab
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 20 deletions.
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

0 comments on commit 56f19ab

Please sign in to comment.