Ruby スクリプトでデータを暗号化する方法
本日は、 Ruby の OpenSSL バインドを利用してデータを暗号化する方法をご紹介します。というのも最近、自宅サーバーにある各種データを Web 上のサービスに移動しようと画策していまして、その際にプライベートなデータは暗号化して保存したいのです。ほとんどの Web API は暗号化なしの HTTP で通信しますし、いくらパスワードで保護されているとはいえ、他所の HDD にプレーンな状態で保存するのは不安ですからね。
それ以外でもスクリプトで暗号化の処理をしたい場面はいろいろあると思います。そんなときは、ぜひ参考にしてください。
暗号化する
それでは、まずは暗号化の処理から。 OpenSSL はさまざまな暗号化アルゴリズムをサポートしていますが、ここではリファレンスでも推奨されている AES-256-CBC を使うことにします。ひとつの文字列(バイト列)を暗号化する関数は以下のようになります。
def encrypt_data(data, password, salt) cipher = OpenSSL::Cipher::Cipher.new("AES-256-CBC") cipher.encrypt cipher.pkcs5_keyivgen(password, salt) cipher.update(data) + cipher.final end
引数 data は暗号化するデータ、 password は名前のとおり暗号化の解除に使うパスワードです。 salt にはランダムな 8byte の文字列(バイト列)を与えてください(詳細は後述)。
処理内容を簡単に説明すると、以下のようになります。
- 暗号化アルゴリズム (AES-256-CBC) を指定して、 Cipher クラスのインスタンスを作成する。
- encrypt メソッドを呼び出し、暗号化モードに切り替える。これは他のメソッドを呼ぶ前に必ず実行する必要がある。
- pkcs5_keyivgen メソッドにパスワードと salt を渡し、暗号化のキーと初期ベクトルを生成・設定する。
- update メソッドでデータを暗号化する。
- final メソッドで暗号化を完了させる。
update メソッドは渡されたデータを暗号化して返しますが、暗号化は固定長(AES-256-CBC は 16byte)のブロック単位で行われることに注意してください。このため、例えば 70byte のデータを update に渡すと、最初の 64byte 分が暗号化され、残りの 6byte は cipher オブジェクト内部にバッファリングされます。 update は複数回呼べますので、もし再度 update が呼ばれれば、バッファにある 6byte と渡されたデータを結合し、(16byte 以上になれば)やはりブロックを満たす分を暗号化して返します。そして最後に final メソッドを呼ぶことで、バッファにある残りのデータが暗号化される、という仕組みになっています。したがって、 update と final の返り値をすべて結合したものが、最終的な暗号化データになります。
復号する
データの復号処理は暗号化とほぼ同じです。
def decrypt_data(data, password, salt) cipher = OpenSSL::Cipher::Cipher.new("AES-256-CBC") cipher.decrypt cipher.pkcs5_keyivgen(password, salt) cipher.update(data) + cipher.final end
encrypt メソッドが decrypt に置き換わった以外はまったく同じですね。 data には先ほどの encrypt_data 関数で暗号化したデータを、 password, salt には暗号化の際に指定したのと同じ文字(バイナリ)列を指定してください。
実際にファイルを暗号化してみる
それでは、上記の関数を使って実際にファイルを暗号化するスクリプトを書いてみましょう。
#! /usr/bin/ruby # -*- coding: utf-8 -*- require 'openssl' def encrypt_data(data, password, salt) # ...略... end def decrypt_data(data, password, salt) # ...略... end def print_help print "encrypt.rb コマンド 入力ファイル名\n" print " コマンドは e で暗号化、 d で復号化\n" end command = { 'e' => 0, 'd' => 1}[(ARGV[0]||'').strip.downcase] data = ARGV[1] && IO.read(ARGV[1]) if command && data $stderr << "Password: " password = $stdin.gets.strip if command == 0 salt = OpenSSL::Random.random_bytes(8) $stdout << "Salted__" + salt + encrypt_data(data, password, salt) else command == 1 $stdout << decrypt_data(data[16, data.size], password, data[8, 8]) end else print_help() exit(1) end
以下の場所に完全なソースファイルがあるので、ご利用ください。
このスクリプトを使って "source" というファイルを暗号化するには、以下のようにします。
encrypt.rb e source > encrypted
その後、暗号化のパスワードを入力すれば、暗号化されたデータが標準出力に出力されます(上記の例では "encrypted" にリダイレクトしています)。出力されるデータは以下の形式になっています。
オフセット | サイズ | 内容 |
---|---|---|
0x00 | 8byte | 固定文字列 "Salted__" |
0x08 | 8byte | salt |
0x10 | 任意 | 暗号化されたデータ |
先頭に "Salted__" という固定文字列を入れているのは、後述の openssl コマンドと互換性をとるためです。それが必要なければ、単純に salt と暗号化データを保存するだけにしても問題ありません。
"encrypted" を復号するには、以下のようにします。
encrypt.rb d encrypted
暗号化時と同様にパスワードを入力すれば、 "source" と同じ内容が標準出力に表示されます。
salt について
さて、これまで何気なく使ってきた「salt」という言葉ですが、耳慣れない方も多いかと思います。私も暗号化に関しては素人なので正確ではないかもしれませんが、理解している範囲で説明してみます。
salt というのは、いわば 2 つめの補助的なパスワードです。ただし、通常のパスワードとは以下の点が異なります。
- 人間が入力するのではなく、プログラム内でランダムに生成する。
- 人間が覚える代わりに、暗号化されたデータと一緒に保存する。
「暗号化データと一緒に保存してしまっては、パスワードの意味がない」と思われるかもしれませんが、通常のパスワードとは少々目的が異なります。 salt を使う理由は、たとえまったく同じデータを暗号化した場合でも、暗号化後のデータを毎回変化させるためです。
例えば、ユーザーのログインパスワードを共通の暗号化キー(salt なし)で暗号化して DB に保存している Web アプリケーションがあり、攻撃者がその DB の内容を盗む方法を見つけたとします。すると、攻撃者は以下の方法で一部のユーザーアカウントを乗っ取れる可能性があります。
- いかにもありそうなパスワードを使って、その Web アプリケーションにユーザー登録をする。
- ユーザー登録が完了したら、 DB の内容を盗む。
- DB の内容から暗号化された自分のパスワードを取得する。
- 自分以外に、暗号化後のパスワードが自分と同じになっているユーザーを見つける。
- ユーザーが見つかれば(暗号化前のパスワードも自分と同じはずなので)そのアカウントを乗っ取ることができる。
もちろん (4) で同じパスワードを使っているユーザーがいなければ失敗ですが、ユーザー数が十分に多ければ見つかる可能性も高まります。
このような攻撃が可能なのは、「同じ暗号化キーで同じデータを暗号化した結果は常に同じになる」という法則が成り立つためです。そこで、 salt を導入して毎回暗号化後の結果が変わるようにすれば、攻撃を防ぐ(困難にする)ことができます。
また、私はきちんと理解できていないのですが、「レインボーテーブル」と呼ばれる巨大なテーブルを使った攻撃方法があり、これを防ぐのにも salt が効果的とのことです。
このような目的で使われるため、 salt の生成にはなるべく質の良い乱数を使うのが賢明です。そこで、上記のサンプルでは OpenSSL の乱数生成関数である OpenSSL::Random.random_bytes を使っています。この関数についてはこちらの記事で簡単に紹介していますので、参考にしてください。
openssl コマンドとの互換性
実は OpenSSL ライブラリには openssl というコマンドライン・ツールも付属しており、これを使ってファイルを暗号化することもできます。例えば "source" を AES-256-CBC で暗号化するには以下のようにします。
openssl enc -e -aes-256-cbc -in source -out encrypted
このようにして生成されるファイルと互換性のある暗号化を行うこともできます。それには、先ほどの encrypt.rb の encrypt_data, decrypt_data の以下の行を
cipher.pkcs5_keyivgen(password, salt)
こう書き換えます(第三引数として 1 を渡しています)。
cipher.pkcs5_keyivgen(password, salt, 1)
これで、 openssl コマンドで暗号化したデータを encrypt.rb で復元できますし、その逆も可能になります。
追加した第三引数は、パスワードと salt から暗号化キーを生成する際の繰り返し回数です。 openssl コマンドはこの値が 1 でハードコーディングされているのですが、実はこの数値を小さくするのはあまり好ましくありません(Ruby のデフォルトは 2048)。 openssl コマンドとの互換性がほんとうに必要なときだけ、この指定を行うのが良いでしょう。
以上、本日は Ruby の OpenSSL バインドでデータを暗号化する方法をご紹介しました。暗号化の処理が必要になった際には、ぜひご活用ください!
詳しくはこちらの記事をどうぞ!
この記事にコメントする