開発部のにゃんです。主にバックエンドを弄っております。
Webアプリケーションではセキュリティ対策のためにランダムな文字列を使用する場面が多々あります。例えば
- CSRF対策のトークン
- OAuthやOpenID Connectで使用するnonce, state, code_verifier
- メールの到達確認用URLのトークン
- パスワードをhashする際に使用するsalt
- セッションID
これらの値は単に衝突しなければOKというものではありません。十分なセキュリティ強度を確保するためには推測不可能なランダム値を使う必要があります。
以下は推測不可能なランダム値ではありません。セキュリティが求められる場面では使ってはいけません。
Math.randomなどの疑似乱数- 日付やユーザ情報のハッシュ
ではどのような値が適切なのでしょうか?
/dev/randomと/dev/urandom
Linuxには
- /dev/random
- /dev/urandom
という特別なファイルがあり、セキュリティの求められる場面でも利用可能な乱数を取得することができます。
/dev/randomはシステムのノイズを利用した真の乱数を返します。ノイズを貯めたエントロピープールが枯渇するとノイズが溜まるまでブロックされるというデメリットがあります。SSH鍵やTLSサーバ鍵など長期間利用され、高度な保護が求められる場合に使用します。
/dev/urandomは、/dev/randomに品質で劣りますがトークンなどの使い捨ての値には十分な強度があります。またエントロピープールが枯渇してもブロッキングされません。
Webアプリケーションなどのリアルタイム用途ではブロッキングを避けたいのと、大抵の場合、暗号鍵ほどの強度は求められないので /dev/urandomを使うのが良いでしょう。
java.security.SecureRandom
プログラミングでは/dev/randomや/dev/urandomを直接利用せずライブラリやAPIを通してアクセスします。
Javaではjava.security.SecureRandomというクラスが用意されています。実装はJDKによって異なっていたり、設定によって切り替わるのですが、概ね
SecureRandom.getStrongInstance()-/dev/randomに相当する真の乱数生成器を取得、エントロピー枯渇時にブロッキングする。new SecureRandom()-/dev/urandomに相当する暗号論的疑似乱数生成器を取得。ブロッキングしない。
となっているようです。なお、他の言語では
- PHP: random_bytes
- Ruby: SecureRandom
- Python: secrets.SystemRandom
- Node.JS: crypto.randomBytes
- Web API: crypto.getRandomValues
といった安全なランダム値を取得するAPIが用意されています。
サンプルコード
アカウント登録時やメールアドレス変更時に以下のような文面のメールを送ることがあると思います。
以下のURLにアクセスしてメールアドレス変更を行ってください http://exmaple.com/mail/confirm?token=laLg4Whuf0NA27AYLDyigk2i6_X16g6tNBbBSR_eLlugTHMQDptfqbePrPP4lT62
このメール文面にあるtokenを安全に作成するコードを例示します。
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
public class Token {
public static void main(String[] args) {
System.out.println(generateMailVerificationToken("test@example.com"));
}
public static String generateMailVerificationToken(String email) {
ByteBuffer buf = ByteBuffer.allocate(48);
// 16byteのランダム文字列
byte[] randomBytes = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(randomBytes);
buf.put(randomBytes);
// 32byteのemailハッシュ値
byte[] emailHashBytes = DigestUtils.sha256(
email + Hex.encodeHexString(randomBytes));
buf.put(emailHashBytes);
return Base64.encodeBase64URLSafeString(buf.array());
}
}
ポイントはMath.randomやjava.util.Randomではなく、java.security.SecureRandomを使うことです。トークンとしてはランダム部分の16バイトで十分な長さがあり衝突の心配はありません。今回はメールアドレスのsha256ハッシュ値32バイトを追加していますが、この部分は本質的には不要です
というわけで、今回は地味目な話題でしたがトークンってどう作ればいいのかな?と思った時には参考にしていただけると幸いです