LoginSignup
1
0

More than 3 years have passed since last update.

【Ruby】共通鍵暗号のベンチマークテスト

Last updated at Posted at 2020-08-01

はじめに

センシティブな情報を扱う際に、平文を暗号化し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内で接続出来るようにすれば少し改善されるものの、やはり自前の実装には勝てないようです。ただ、セキュリティ向上のために暗号化に使用する鍵を長くすると、自前の実装でも計算コストが上昇するので、この点は注意が必要そうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0