Symmetric Encryption in Ruby
とはいえ低水準なインタフェースであり直接扱うことは推奨されていない
実績のあるライブラリなどの高水準インタフェースを使う
暗号化器 / Cypher
なにはともあれrequire
code:ruby
require 'openssl'
code:ruby
puts OpenSSL::Cipher.ciphers
170個以上でてくる
<algorithm-name>-<key length>-<mode>の形式 e.g. aes-128-cbc
Algorithm
Key Length
鍵長のこと
AESではKey lengthに128ビット・192ビット・256ビットの3つが利用できる
数字が大きいほうがセキュアだがパフォーマンスで劣るというトレードオフがある
数字が大きいほうが変換のラウンド数が異なる
Mode
ブロック暗号とは
固定長のデータ(ブロックと呼ぶ)を単位として処理する暗号の総称である これに対してビット単位やバイト単位で処理を行う暗号はストリーム暗号と呼ばれる つかう
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で試す
暗号化
暗号化オブジェクトの生成
code:ruby
cipher = OpenSSL::Cipher::AES256.new(:CBC)
# もしくは
cipher = OpenSSL::Cipher.new("AES-256-CBC")
暗号化に使うことを宣言
code:ruby
cipher.encrypt
ivの生成
暗号化のたびに毎回暗号鍵を替える作業を省くことができる
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"
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 から生成する 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"
=> "1\x81\x9F\xD8/\xBE9@k\xF3h\xC0\xD5yy/M\fw\xF4.\x01\x13\xDC\f\xE3n\xE3nu\x00\xC3"
=> "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の実装
パスワードやActiveRecord modelのattributesの暗号化に関して高レベルのインタフェースを提供するgem code:ruby
SymmetricEncryption.encrypt "Sensitive data"
SymmetricEncryption.decrypt "JqLJOi6dNjWI9kX9lSL1XQ=="
config YAMLに暗号化のアルゴリズムやiv、KMSのkey file名を指定する ivはデフォルトでは同一の値を利用する
暗号化した値を検索に使う場合には結果が常に同じでないといけないため
検索用途でなければランダムにivを生成させることができる
セキュリティのためにはランダム化を推奨
暗号化するたびに異なる出力が得られる(結果が不定になる)
上述の例のようにOpenSSL::CipherのAPIを利用していることがわかる
Zlib::Deflate.deflateによる圧縮も可能
バイナリ列に暗号化されたあとBase64エンコーディングできる VARCHARやTEXT型には保存できないDBの都合など
文字列だけではなくメタ情報を含むヘッダがデフォルトでつく
ランダムに生成されたivもヘッダに保存されるので復号できる
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アプリケーションからしか見られない
参考