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.
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)
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)
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"
--Ruby standard library OpenSSL
--Measure the total number of seconds when executed 1000 times --Measure each of encryption only and decryption only
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"
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"
Method | The number of seconds |
---|---|
Ruby standard library OpenSSL | 0.006588994991034269 |
KMS | 8.035557514987886 |
KMS (VPC endpoint) | 7.766658762935549 |
Method | The number of seconds |
---|---|
Ruby standard library OpenSSL | 0.0037274740170687437 |
KMS | 8.964495759923011 |
KMS (VPC endpoint) | 7.9086791928857565 |
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