[Ruby] [Ruby] Benchmark test for common key cryptography

4 minute read

Introduction

When dealing with sensitive information, plaintext is often encrypted and stored in a DB, and decrypted when it is retrieved.

In my work, I examined the methods of encryption and decryption with Ruby, and I had the opportunity to do benchmark tests with each method, so I summarized the contents at that time.

There are two methods considered: Ruby standard library OpenSSL (own implementation) and AWS Key Management Service (KMS).

Your own implementation is likely to be computationally expensive, and KMS is likely to be network costly, so the focus will be on how that result will be.

Encryption method

Common key encryption

A method in which the sender and the receiver secretly share a single key and use a common key for encryption and decryption. If the same data is always replaced with the same ciphertext, plaintext is inferred from the frequency, so the initialization vector (or salt) is set so that the same data can be replaced with different ciphertext. This time we used the initialization vector.

When using OpenSSL::Cipher of the standard library of Ruby, it looks like this.

def encrypt(plaintext, key, iv)
  enc = OpenSSL::Cipher.new('AES-256-CBC')
  enc.encrypt
  enc.key = key
  enc.iv = iv
  enc.update(plaintext) + enc.final
end

def decrypt(encrypted_data, key, iv)
  dec = OpenSSL::Cipher.new('AES-256-CBC')
  dec.decrypt
  dec.key = key
  dec.iv = iv
  decrypted_data = dec.update(encrypted_data) + dec.final

  # Decrypted data is ASCII-8BIT, forcibly correct encoding
  decrypted_data.force_encoding("UTF-8")
end

plaintext = "text to be encrypted"

key = "common key"
iv = "initialization vector"

# Data encryption
encrypted_data = encrypt(plaintext, key, iv)

# Decrypt data
decrypt(encrypted_data, key, iv)

Public key cryptography

A method of encrypting with a public key and decrypting with a secret key.

When using OpenSSL::Cipher of the standard library of Ruby, it looks like this.

def encrypt(plaintext, public_key)
  Base64.encode64(
    public_key.public_encrypt(
      data,
      OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
    )
  )
end

def decrypt(encrypted_data, private_key)
  decrypted_data = private_key.private_decrypt(
    Base64.decode64(encrypted_data),
    OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
  )

  # Decrypted data is ASCII-8BIT, forcibly correct encoding
  decrypted_data.force_encoding("UTF-8")
end

plaintext = "text to be encrypted"

public_key = OpenSSL::PKey::RSA.new(File.read(public_key_file))
private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file))

# Data encryption
encrypted_data = encrypt(plaintext, public_key)

# Decrypt data
decrypt(encrypted_data, private_key)

Benchmark overview

Since the plaintext to be encrypted is a long text of several hundred characters, public key cryptography cannot be used (it can be used with some improvement, but it is not recommended), so we decided to use common key cryptography.

Use the Benchmark library for benchmarking https://docs.ruby-lang.org/ja/latest/class/Benchmark.html

require'benchmark'

result = Benchmark.realtime do
  # Describe the process to be measured here
end

puts "#{result}s"

Comparison

  • Ruby standard library OpenSSL
  • KMS
  • KMS (Set VPC endpoint)

Benchmark conditions

  • Measures the total number of seconds when executed 1000 times
  • Measures only encryption and decryption

Encryption benchmark script

Ruby standard library OpenSSL

require'openssl'
require'base64'
require'benchmark'

def encrypt(plaintext, key, iv)
  enc = OpenSSL::Cipher.new('AES-256-CBC')
  enc.encrypt
  enc.key = key
  enc.iv = iv
  enc.update(comment) + enc.final
end

data = <<-EOS
  Long sentence
EOS

key = "common key"
iv = "initialization vector"

result = Benchmark.realtime do
  1000.times do
    encrypt(plaintext, key, iv)
  end
end

KMS

require'aws-sdk-s3'
require'base64'
require'benchmark'

class KMSClient
  REGION ='ap-northeast-1'
  ALIAS_NAME ='KMS Alias Name'

  def initialize
    @client = Aws::KMS::Client.new(
      region: REGION,
  # If you set a VPC endpoint, specify this instead of region
  # endpoint:'https://vpce-xxxxx.kms.ap-northeast-1.vpce.amazonaws.com',
      access_key_id: ``,
      secret_access_key: ``,
    )
    @alias = @client.list_aliases.aliases.find {|a| a.alias_name == ALIAS_NAME}
  end

  def encrypt(plaintext)
    ciphertext = @client.encrypt(
      key_id: @alias.target_key_id,
      plaintext: plaintext
    )

    Base64.encode64(ciphertext.ciphertext_blob)
  end
end

plaintext = <<-EOS
 Long sentence
EOS

client = KMSClient.new

result = Benchmark.realtime do
  1000.times do
    client.encrypt(plaintext)
  end
end

puts "#{result}s"

Decryption Benchmark Script

Ruby standard library OpenSSL

require'openssl'
require'base64'
require'benchmark'

def decrypt(encrypted_data, key, iv)
  dec = OpenSSL::Cipher.new('AES-256-CBC')
  dec.decrypt
  dec.key = key
  dec.iv = iv
  decrypted_data = dec.update(encrypted_data) + dec.final
  decrypted_data.force_encoding("UTF-8")
end

plaintext = <<-EOS
  Long sentence...
EOS

key = "common key"
iv = "initialization vector"

encrypted_data = encrypt(plaintext, key, iv)

result = Benchmark.realtime do
  1000.times do
    decrypt(encrypted_data, key, iv)
  end
end

puts "#{result}s"

KMS

require'aws-sdk-s3'
require'base64'
require'benchmark'

class KMSClient
  REGION ='ap-northeast-1'
  ALIAS_NAME ='KMS Alias Name'

  def initialize
    @client = Aws::KMS::Client.new(
      region: REGION,
  # If you set a VPC endpoint, specify this instead of region
  # endpoint:'https://vpce-xxxxx.kms.ap-northeast-1.vpce.amazonaws.com',
      access_key_id: ``,
      secret_access_key: ``,
    )
    @alias = @client.list_aliases.aliases.find {|a| a.alias_name == ALIAS_NAME}
    p @alias
  end

  def encrypt(plaintext)
    ciphertext = @client.encrypt(
      key_id: @alias.target_key_id,
      plaintext: plaintext
    )

    Base64.encode64(ciphertext.ciphertext_blob)
  end

  def decrypt(ciphertext_blob)
    @client.decrypt(ciphertext_blob: Base64.decode64(ciphertext_blob)).plaintext
  end
end

plaintext = <<-EOS
  Long sentence
EOS

client = KMSClient.new

encrypted_data = client.encrypt(plaintext)result = Benchmark.realtime do
 1000 times do
    client.decrypt(encrypted_data)
 End
end

puts "#{result}s"

Benchmark result

encryption

Method Seconds
Ruby Standard Library OpenSSL 0.006588994991034269
KMS 8.035557514987886
KMS (VPC Endpoint) 7.766658762935549

Decryption

Method Seconds
Ruby Standard Library OpenSSL 0.0037274740170687437
KMS 8.964495759923011
KMS (VPC Endpoint) 7.9086791928857565

Summary

After all, KMS seems to have a significant network cost and slows down processing. I think this is because the network access to AWS occurs every time the encryption/decryption method is called. Although it will be improved a little if you set the VPC endpoint and connect inside the VPC, it seems that you can not beat your own implementation. However, if you lengthen the key used for encryption to improve security, the calculation cost will increase even with your own implementation, so you need to be careful about this point.