[Ruby] Common key cryptographic benchmark test

Introduction

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

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

There are two methods considered: the Ruby standard library OpenSSL (own implementation) and the 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 will result.

Encryption method

Common key cryptography

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

It looks like this when using Ruby's standard library ʻOpenSSL :: Cipher`.

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

  #The decrypted data is ASCII-Since it is 8BIT, forcibly correct the encoding
  decrypted_data.force_encoding("UTF-8")
end

plaintext = "String to encrypt"

key = "Common key"
iv = "Initialization vector"

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

#Data decryption
decrypt(encrypted_data, key, iv)

Public key cryptography

A method in which encryption is performed with a public key and decryption is performed with a private key.

It looks like this when using Ruby's standard library ʻOpenSSL :: Cipher`.

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
  )

  #The decrypted data is ASCII-Since it is 8BIT, forcibly correct the encoding
  decrypted_data.force_encoding("UTF-8")
end

plaintext = "String to encrypt"

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)

#Data decryption
decrypt(encrypted_data, private_key)

Benchmark overview

This time, since the plaintext to be encrypted is a long sentence of several hundred characters, public key cryptography cannot be used (it can be used with a little ingenuity, 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
  #The process to be measured is described here.
end

puts "#{result}s"

Comparison

--Ruby standard library OpenSSL

Benchmark conditions

--Measure the total number of seconds when executed 1000 times --Measure each of encryption only and decryption only

Cryptography 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 have set up 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"

Benchmark script for decryption

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 have set up 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 results

encryption

Method The number of seconds
Ruby standard library OpenSSL 0.006588994991034269
KMS 8.035557514987886
KMS (VPC endpoint) 7.766658762935549

Decryption

Method The number of seconds
Ruby standard library OpenSSL 0.0037274740170687437
KMS 8.964495759923011
KMS (VPC endpoint) 7.9086791928857565

Summary

After all, KMS seems to have a noticeable network cost and slow processing. I think this is the result of network access to AWS every time the encryption / decryption method is called. Setting up a VPC endpoint and allowing it to connect within the VPC would improve it a bit, but it still doesn't seem to beat its own implementation. However, if you lengthen the key used for encryption to improve security, the calculation cost will increase even if you implement it yourself, so it seems that you need to be careful about this point.

Recommended Posts

[Ruby] Common key cryptographic benchmark test