Symmetric Encryption in Ruby
Symmetric EncryptionをRubyで扱うにはOpenSSL::Cipher classを用いる
とはいえ低水準なインタフェースであり直接扱うことは推奨されていない
実績のあるライブラリなどの高水準インタフェースを使う
暗号化器 / Cypher
なにはともあれrequire
code:ruby
require 'openssl'
使える暗号化器 / cypherの一覧をみる
code:ruby
puts OpenSSL::Cipher.ciphers
170個以上でてくる
<algorithm-name>-<key length>-<mode>の形式 e.g. aes-128-cbc
Algorithm
Data Encryption Standard(DES)は2000年頃には相対的に強度が低いので2021年のスタンダードではない
Rivest Cipher(RC)はもはや安全ではないのでつかわない
Prohibiting RC4 Cipher Suites
Advanced Encryption Standard(AES)は広く使われており、十分にセキュアにできる
Key Length
鍵長のこと
AESではKey lengthに128ビット・192ビット・256ビットの3つが利用できる
数字が大きいほうがセキュアだがパフォーマンスで劣るというトレードオフがある
数字が大きいほうが変換のラウンド数が異なる
https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard
Mode
AESのようなブロック暗号ではモードを指定する
https://ja.wikipedia.org/wiki/暗号利用モード
ブロック暗号とは
固定長のデータ(ブロックと呼ぶ)を単位として処理する暗号の総称である
これに対してビット単位やバイト単位で処理を行う暗号はストリーム暗号と呼ばれる
通常はCBCを選択すればよい
つかう
基本的な順序は以下の通り(https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html より)
OpenSSL::Cipher.new や OpenSSL::Cipher::AES256.new などで暗号オブジェクトを生成する
OpenSSL::Cipher#encrypt, OpenSSL::Cipher#decrypt で 暗号、復号のいずれをするかを設定する
OpenSSL::Cipher#key=, OpenSSL::Cipher#iv=, OpenSSL::Cipher#random_key, OpenSSL::Cipher#random_iv などで 鍵と IV(initialization vector) を設定する
OpenSSL::Cipher#update, OpenSSL::Cipher#final で 暗号化/復号をする
AES-256-CBCで試す
暗号化したい平文、鍵、初期化ベクトル (IV) さえ用意すれば以下の手順で試せる
Ruby gemのsymmetric-encryptionもデフォルトでは"aes-256-cbs"を使っている
暗号化
暗号化オブジェクトの生成
code:ruby
cipher = OpenSSL::Cipher::AES256.new(:CBC)
# もしくは
cipher = OpenSSL::Cipher.new("AES-256-CBC")
暗号化に使うことを宣言
code:ruby
cipher.encrypt
ivの生成
information vectorは同じ暗号鍵で生成しても毎回異なる結果を生成するために用いるビット列
暗号化のたびに毎回暗号鍵を替える作業を省くことができる
code:ruby
iv = cipher.random_iv
=> "\x91\x80O5G\xA9\xEEI\xBC\x9B\xB8\xB3\x19P*;"
iv = cipher.random_iv
=> "\xCA\xF0-\xECA\xF6c8\xE82\x98\xC8[f\xE8\x00" # 異なるビット列
keyの設定
文字列ならなんでもいいわけではない
code:ruby
cipher.key = "SecretPassword"
ArgumentError (key must be 32 bytes)
256bit、つまり32byteの長さでなければならない
エンコーディングがUTF-8であればアルファベット1文字は1byteなので、32文字のアルファベットであればよい(現実的にはよくないが試すうえでは)
code:ruby
("password" * 4).bytesize
=> 32
key = ("password" * 4)
cipher.key = key
=> "passwordpasswordpasswordpassword"
SHA-256でハッシュ化して得られる256bitを使う手もある
code:ruby
key = Digest::SHA256.digest 'password'
=> "^\x88H\x98\xDA(\x04qQ\xD0\xE5o\x8D\xC6)'s`=\rj\xAB\xBD\xD6*\x11\xEFr\x1D\x15B\xD8"
るりまに載っていた、鍵とivをまとめて生成する例
鍵とIV(Initialize Vector)を PKCS#5 に従ってパスワードと salt から生成する
https://ja.wikipedia.org/wiki/PKCS
code:ruby
pass = "**secret password**" # パスワード
salt = OpenSSL::Random.random_bytes(8) # salt
=> "\v\x86\e{y\x97\xA2\xD2"
key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, 2000, cipher.key_len + cipher.iv_len)
=> "1\x81\x9F\xD8/\xBE9@k\xF3h\xC0\xD5yy/M\fw\xF4.\x01\x13\xDC\f\xE3n\xE3nu\x00\xC3b\x87j\xAD47\xD8}\x9C*:o\x82\xC9\xAF\x10"
key = key_iv0, cipher.key_len
=> "1\x81\x9F\xD8/\xBE9@k\xF3h\xC0\xD5yy/M\fw\xF4.\x01\x13\xDC\f\xE3n\xE3nu\x00\xC3"
iv = key_ivcipher.key_len, cipher.iv_len
=> "b\x87j\xAD47\xD8}\x9C*:o\x82\xC9\xAF\x10"
暗号化する
code:ruby
plain_text = 'This is a secret message'
cipher_text = cipher.update(plain_text) + cipher.final
=> "\\8X\xDD\xDB\x9Eyc\xDE\xEAe\xDD*\x16uL\xD5\xEEH\xC4'O\xCBS\x17>\ea!\xBF\xBCh"
復号
暗号化オブジェクトの生成
code:ruby
decipher = OpenSSL::Cipher::AES256.new(:CBC)
# もしくは
decipher = OpenSSL::Cipher.new("AES-256-CBC")
暗号化に使うことを宣言
code:ruby
decipher.decrypt
keyとivをセットして復号
code:ruby
decipher.iv = iv
=> "\xCA\xF0-\xECA\xF6c8\xE82\x98\xC8[f\xE8\x00"
decipher.key = key
=> "passwordpasswordpasswordpassword"
plain_text = decipher.update(cipher_text) + decipher.final
=> "This is a secret message"
Symmetric-encryption gemの実装
https://github.com/rocketjob/symmetric-encryption
パスワードやActiveRecord modelのattributesの暗号化に関して高レベルのインタフェースを提供するgem
Mongoidサポートもある
code:ruby
SymmetricEncryption.encrypt "Sensitive data"
SymmetricEncryption.decrypt "JqLJOi6dNjWI9kX9lSL1XQ=="
config YAMLに暗号化のアルゴリズムやiv、KMSのkey file名を指定する
ivはデフォルトでは同一の値を利用する
暗号化した値を検索に使う場合には結果が常に同じでないといけないため
検索用途でなければランダムにivを生成させることができる
セキュリティのためにはランダム化を推奨
暗号化するたびに異なる出力が得られる(結果が不定になる)
key storeとしてAWS KMSやGCP KMSが利用できる
http://encryption.rocketjob.io/configuration.html
上述の例のようにOpenSSL::CipherのAPIを利用していることがわかる
https://github.com/rocketjob/symmetric- encryption/blob/64c3d29d12dd8beb3075c59972a47b4e0c7ec453/lib/symmetric_encryption/cipher.rb#L267-L279
Zlib::Deflate.deflateによる圧縮も可能
バイナリ列に暗号化されたあとBase64エンコーディングできる
VARCHARやTEXT型には保存できないDBの都合など
文字列だけではなくメタ情報を含むヘッダがデフォルトでつく
ランダムに生成されたivもヘッダに保存されるので復号できる
https://github.com/rocketjob/symmetric-encryption/blob/526e49c326fff538d2a3fb01729e6a57fab95590/lib/symmetric_encryption/header.rb#L199-L231
RailsのAttributes APIを使い、特定のattributeをDBに保存する際に暗号化し、読み出すときに復号できる
code:ruby
class Person < ActiveRecord::Base
attribute :name, :encrypted
end
検索でもPerson.where(name: "Jack").countのように平文でクエリをかけばよい
attribute :name, :encrypted, random_iv: trueとすることでivがランダムになるので暗号化のたびに生成される文字列が変わる
より強固になる
ただし検索はいっさいできなくなる
暗号化されたデータは基本的には鍵を知っているRailsアプリケーションからしか見られない
鍵がわかってしまえばMySQLで暗号化と復号できてしまう
参考
https://ja.wikipedia.org/wiki/共通鍵暗号
https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard
https://ja.wikipedia.org/wiki/ブロック暗号
https://ja.wikipedia.org/wiki/暗号利用モード
https://ja.wikipedia.org/wiki/初期化ベクトル
https://ja.wikipedia.org/wiki/PKCS
https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html
https://medium.com/@Bakku1505/playing-with-symmetric-encryption-algorithms-in-ruby-8652f105341e