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

# はじめに
センシティブな情報を扱う際に、平文を暗号化しDBに保存し、取り出す時に復号化することがよくあります。

業務の中で、Rubyで暗号・復号化する方法を検討し、それぞれの方法でベンチマークテストを行う機会があったので、その時の内容をまとめました。

検討した方法は、Ruby標準ライブラリOpenSSL（自前の実装）とAWS Key Management Service（KMS）の2つになります。

自前の実装は計算コストが掛かりそうで、KMSはネットワークコストが掛かりそうなので、その点がどう結果に出るかが焦点になります。

# 暗号化方式

## 共通鍵暗号

送信者と受信者は１つの鍵を秘密に共有し、暗号化と復号化に共通の鍵を使う方式。同じデータを常に同じ暗号文に置き換えると、その頻度から平文が推測されてしまうため、同じデータでも違う暗号文に置き換えられるように初期化ベクトル（または、ソルト）を設定する。今回は初期化ベクトルを使用しました。

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