はじめに
前回の続きで、鍵交換までできたので暗号通信してみようという話
まあ、AES-GCM使っておけば良いでしょうという雰囲気なのでその通りにする
鍵交換
で鍵交換しているので、これを AES の共通鍵として使う
コードは前回同様 Java と C#(.NET 8)
暗号化(Java)
処理としては以下の手順になる
- IVを生成
- GCMParameterSpecを生成
- 暗号化
- IVとつなげて送信
重要なのは同じ鍵で同じIVを使わないこと、必ず暗号文生成のたびにIVを作る
同じIVを使いまわすと秘密鍵漏洩の可能性がある
今回は暗号文とIVをつなげてやり取りするようにしているので、暗号文は平文より必ず12Bytes多くなってしまう(タグもあるのでもっと増えるけど)
これがキツいような用途では前のIVから決まった変化をするような仕組みを用意しても良いが、一度出た値が二度と出ないように注意しないといけないことと、ズレると復号できなくなるので、エラー処理とか面倒になる可能性が高いことを考慮しないといけない
あと、Javaではタグがつながって返ってくるので、C#で使うときは分離する必要がある(後ろの16Bytesがタグ)
コードとしては単純でこれだけ、aesKey は前回の鍵交換で取得したもの
public byte[] encryptAes(byte[] src)
{
try {
// IV を生成(IVは12バイトが推奨されているらしい)
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
// GCMParameterSpec を生成(128はタグの長さ)
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
// 暗号化
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), spec);
// IV とつなげて返す(これを送信すれば良い)
return concatBuffer(nonce, cipher.doFinal(src));
}
catch (Exception e) {
Log.write("encryptAes exception: " + e.toString());
return null;
}
}
// 2つの byte[] を連結
// なんか分かりやすくてお気に入りなので ByteArrayOutputStreamを使っているけど、多分 System.arraycopy とかの方が速い
private static byte[] concatBuffer(byte[] src1, byte[] src2)
{
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream(src1.length + src2.length);
bos.write(src1);
bos.write(src2);
return bos.toByteArray();
}
catch (Exception e) {
Log.write("concatBuffer exception: " + e.toString());
return null;
}
}
暗号化(C#)
こちらもIVを生成して暗号化、つなげるところは同様
ただ、C#だとタグは別扱いしないといけない
aesKey は Java の時と同じ
public byte[] EncryptAes(byte[] src)
{
// IV を生成
byte[] nonce = new byte[12];
RandomNumberGenerator.Fill(nonce);
// 暗号文用のバッファとタグ用のバッファを別々に用意する
byte[] ciphertext = new byte[src.Length];
byte[] tag = new byte[16];
// 暗号化(16はタグの長さ)
using var aes = new AesGcm(aesKey, 16);
aes.Encrypt(nonce, src, ciphertext, tag);
// 全部つなげて返す(本筋では無いのでパフォーマンス無視なやり方、気になるなら変えましょう)
byte[] ret_temp = ConcatBuffer(nonce, ciphertext);
byte[] ret = ConcatBuffer(ret_temp, tag);
return ret;
}
private static byte[] ConcatBuffer(byte[] src1, byte[] src2)
{
byte[] result = new byte[src1.Length + src2.Length];
Buffer.BlockCopy(src1, 0, result, 0, src1.Length);
Buffer.BlockCopy(src2, 0, result, src1.Length, src2.Length);
return result;
}
復号化(Java)
IV を分離して、GCMParameterSpecを生成、そして復号という流れ
というわけで、渡す enc_data は上で作ったように IV が先に入っている必要あり
encryptAes の返値をそのまま渡す感じになる
aesKey は暗号化と同じく、鍵交換で取得したもの
public byte[] decryptAes(byte[] enc_data)
{
try {
// IV と暗号文を分離
byte[] nonce = new byte[12];
System.arraycopy(enc_data, 0, nonce, 0, nonce.length);
byte[] enc_raw = new byte[enc_data.length - nonce.length];
System.arraycopy(enc_data, nonce.length, enc_raw, 0, enc_raw.length);
// GCMParameterSpec を生成して復号化
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), spec);
return cipher.doFinal(enc_raw);
}
catch (Exception e) {
Log.write("decryptAes exception: " + e.toString());
return null;
}
}
復号化(C#)
.NET ではタグを分離しないといけない、というわけで、IV, 暗号文, タグに分離して使う
public byte[] DecryptAes(byte[] src)
{
// IV, 暗号文, タグに分離
byte[] nonce = new byte[12];
byte[] tag = new byte[16];
int cipherLen = src.Length - nonce.Length - tag.Length;
byte[] cipherText = new byte[cipherLen];
Buffer.BlockCopy(src, 0, nonce, 0, nonce.Length);
Buffer.BlockCopy(src, nonce.Length, cipherText, 0, cipherLen);
Buffer.BlockCopy(src, src.Length - tag.Length, tag, 0, tag.Length);
// 復号化(16はタグの長さ)
byte[] ret = new byte[cipherLen];
using var aes = new AesGcm(aesKey, 16);
aes.Decrypt(nonce, cipherText, tag, ret);
return ret;
}