はじめに
センシティブな情報を扱う際に、平文を暗号化しDBに保存し、取り出す時に復号化することがよくあります。
業務の中で、Rubyで暗号・復号化する方法を検討し、それぞれの方法でベンチマークテストを行う機会があったので、その時の内容をまとめました。
検討した方法は、Ruby標準ライブラリOpenSSL(自前の実装)とAWS Key Management Service(KMS)の2つになります。
自前の実装は計算コストが掛かりそうで、KMSはネットワークコストが掛かりそうなので、その点がどう結果に出るかが焦点になります。
暗号化方式
共通鍵暗号
送信者と受信者は1つの鍵を秘密に共有し、暗号化と復号化に共通の鍵を使う方式。同じデータを常に同じ暗号文に置き換えると、その頻度から平文が推測されてしまうため、同じデータでも違う暗号文に置き換えられるように初期化ベクトル(または、ソルト)を設定する。今回は初期化ベクトルを使用しました。
Rubyの標準ライブラリの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
# 復号化したデータはASCII-8BITであるため、強制的にエンコーディングを修正する
decrypted_data.force_encoding("UTF-8")
end
plaintext = "暗号化する文字列"
key = "共通鍵"
iv = "初期化ベクトル"
# データの暗号化
encrypted_data = encrypt(plaintext, key, iv)
# データの復号化
decrypt(encrypted_data, key, iv)
公開鍵暗号
公開鍵で暗号化を行い、秘密鍵で復号化を行う方式。
Rubyの標準ライブラリの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
)
# 復号化したデータはASCII-8BITであるため、強制的にエンコーディングを修正する
decrypted_data.force_encoding("UTF-8")
end
plaintext = "暗号化する文字列"
public_key = OpenSSL::PKey::RSA.new(File.read(public_key_file))
private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file))
# データの暗号化
encrypted_data = encrypt(plaintext, public_key)
# データの復号化
decrypt(encrypted_data, private_key)
ベンチマークの概要
今回、暗号化する平文は数百文字の長文のため、公開鍵暗号が使用出来ない(少し工夫すれば使用は可能だが、推奨されていない)ため、共通鍵暗号を使用することにした。
ベンチマークには、Benchmark
ライブラリを使用
https://docs.ruby-lang.org/ja/latest/class/Benchmark.html
require 'benchmark'
result = Benchmark.realtime do
# ここに計測する処理を記載する
end
puts "#{result}s"
比較対象
- Ruby標準ライブラリOpenSSL
- KMS
- KMS(VPCエンドポイントを設定)
ベンチマークの条件
- 1000回実行した場合の合計秒数を計測
- 暗号化だけ、復号化だけのそれぞれを計測
暗号化のベンチマークスクリプト
Ruby標準ライブラリ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
長文・・・・・
EOS
key = "共通鍵"
iv = "初期化ベクトル"
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,
# VPCエンドポイントを設定した場合は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
長文・・・・・
EOS
client = KMSClient.new
result = Benchmark.realtime do
1000.times do
client.encrypt(plaintext)
end
end
puts "#{result}s"
復号化のベンチマークスクリプト
Ruby標準ライブラリ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
長文・・・・・・
EOS
key = "共通鍵"
iv = "初期化ベクトル"
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,
# VPCエンドポイントを設定した場合は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
長文・・・・・
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"
ベンチマーク結果
暗号化
|方法 |秒数 |
|---|---|---|
|Ruby標準ライブラリOpenSSL |0.006588994991034269|
|KMS |8.035557514987886|
|KMS(VPCエンドポイント)|7.766658762935549 |
復号化
|方法 |秒数 |
|---|---|---|
|Ruby標準ライブラリOpenSSL |0.0037274740170687437|
|KMS |8.964495759923011|
|KMS(VPCエンドポイント)|7.9086791928857565|
まとめ
やはり、KMSはネットワークコストが顕著に出て処理が遅くなるようです。これは、暗号・復号化のメソッド呼び出しの度にAWSへのネットワークアクセスが発生するため、このような結果になったのだと思います。VPCエンドポイントを設定し、VPC内で接続出来るようにすれば少し改善されるものの、やはり自前の実装には勝てないようです。ただ、セキュリティ向上のために暗号化に使用する鍵を長くすると、自前の実装でも計算コストが上昇するので、この点は注意が必要そうです。