はじめに
ちょっと色々と勘違いしていたりしたことがわかったので修正中。
最近なら AES256/GCM 使っておけば大丈夫な気がするので、その使い方。
バイト列を入力して暗号化されたバイト列を取得、復号してバイト列を得るまで。
JavaでCipherを使う場合、GCMのtagは暗号文に含まれる模様。このため、.NETなど、別のライブラリで復号する時はtagを分離しないといけないっぽい。
暗号鍵
AES256にするので、鍵長は当然256bitにする必要がある。
ちゃんとやるなら任意の文字列(普通に使われるパスワード)から暗号で使う鍵を生成する方法がある。(PBKDF, HKDFとかで調べると良さそう?)
ここではパスワードの安全性が本題ではないので、とりあえずSHA-256のハッシュをそのまま使っている。(例外処理は適当なので、使うときはちゃんと作る)
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
.
.
.
public SecretKey getKeyFromPassword(String pass)
{
try {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
sha.update(pass.getBytes(StandardCharsets.UTF_8));
byte[] shakey = sha.digest();
SecretKey ret = new SecretKeySpec(shakey, "AES");
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
自動生成で安全な鍵を取得するならこんな感じ。生成した鍵を共有しないと復号できないので、SecretKey.getEncoded()でbyte配列を取得する。
byte配列から鍵を作るのが getKeyFromBytes(byte[] src); になっている。これを復号側で使う。
ちなみにここで生成した鍵が盗聴されると全部読まれるので、基本的に通信経路で送るべきでは無い。
TLSとかで使われる鍵交換でも、今時はこのような鍵そのものが通信経路を通過することは無い。
import javax.crypto.SecretKey;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
.
.
.
public SecretKey generateRandomKey()
{
try {
KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecretKey ret = keygen.generateKey();
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
public SecretKey getKeyFromBytes(byte[] src)
{
try {
SecretKey ret = new SecretKeySpec(src, "AES");
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
IVとGCMパラメータ生成
同じデータを同じキーで暗号化した時、必ず同じ出力になると平文が推測されやすくなる。これを防ぐために初期化ベクトル(IV)を使う。
GCMの場合、IVは12byteのランダムデータ(nonce)と4byteのカウンタで生成される。
カウンタは自動で処理されるので、nonceを与えて GCMParameterSpec を生成する。
このカウンタが自動で処理されるという話は、昔見た気がするけど今探すと出てこない。IVは使い回し厳禁(暗号文生成のたびに別の値を使う必要あり)なので、毎回ランダム値を生成して、暗号文と一緒に送るのが良い。
復号する時にnonceが必要なので、これもbyte配列で取っておくこと。IVは第三者に知られても大丈夫なので、暗号文と一緒に送っても問題ない。
import java.security.SecureRandom;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
.
.
.
public byte[] generateNonce()
{
try {
byte[] ret = new byte[12];
SecureRandom randgen = new SecureRandom();
randgen.nextBytes(ret);
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
public GCMParameterSpec generateGCMParameter(byte[] nonce)
{
try {
GCMParameterSpec ret = new GCMParameterSpec(128, nonce);
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
AAD
通信パケットのヘッダのように暗号化すると使いにくくなる情報を一緒に送る時、暗号化はしないけれど改ざん検知はするということのために使うデータ。
例ではとりあえず文字列「AADは追加認証データ、中身はなんでも良いが64kBytes以内にする」を送っている。
まあ、普通はあまり使わないかも。
暗号化
平文と鍵、GCMパラメータがあれば暗号化できる。
AADはオプションなので使わなくても良い。使わない場合はupdateAADの呼び出しが不要。
IVは毎回必ず変える必要があるので、paramは毎回生成する。
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.Cipher;
.
.
.
public byte[] encrypt(byte[] src, SecretKey key, GCMParameterSpec param)
{
byte[] aad = "AADは追加認証データ、中身はなんでも良いが64kBytes以内にする".getBytes(StandardCharsets.UTF_8);
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, param);
cipher.updateAAD(aad);
byte[] ret = cipher.doFinal(src);
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
復号化
暗号文と鍵、GCMパラメータ、AADがあれば復号化できる。
AADは一緒に送られてきたものをそのまま使って、改ざんされていたら復号に失敗する。
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.Cipher;
.
.
.
public byte[] decrypt(byte[] src, SecretKey key, GCMParameterSpec param)
{
byte[] aad = "AADは追加認証データ、中身はなんでも良いが64kBytes以内にする".getBytes(StandardCharsets.UTF_8);
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, param);
cipher.updateAAD(aad);
byte[] ret = cipher.doFinal(src);
return ret;
}
catch (Exception e) {
Log.warn(e.toString());
return null;
}
}
まとめ
実際に暗号化・復号化する時はこんな感じ。
// 暗号化
SecretKey key = getKeyFromPassword("P@ssW0rd");
byte[] nonce = generateNonce(); // 復号化で必要なので取っておく
GCMParameterSpec param = generateGCMParameter(nonce);
byte[] encdata = encrypt(srcdata, key, param);
// 復号化
SecretKey key = getKeyFromPassword("P@ssW0rd");
byte[] nonce = <取っておいたbyte配列>;
GCMParameterSpec param = generateGCMParameter(nonce);
byte[] decdata = decrypt(encdata, key, param);