Javaで暗号化

# はじめに
最近なら AES256/GCM 使っておけば大丈夫な気がするので、その使い方。
バイト列を入力して暗号化されたバイト列を取得、復号してバイト列を得るまで。
JavaでCipherを使う場合、GCMのtagは暗号文に含まれる模様。このため、.NETなど、別のライブラリで復号する時はtagを分離しないといけないっぽい。

# 暗号鍵
AES256にするので、鍵長は当然256bitにする必要がある。
パスワードを入力して復号するなら、パスワードのSHA256を鍵に使えば良いかも。自動生成することもできる。

文字列からSHA256を取得して、鍵にするならこんな感じ。（例外処理は適当なので、使うときはちゃんと作る）

```Java
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);　になっている。これを復号側で使う。
```Java
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 を生成する。
復号する時にnonceが必要なので、これもbyte配列で取っておくこと。IVは第三者に知られても大丈夫なので、暗号文と一緒に送っても問題ない。

```java
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
GCMでは追加認証データを使って、改ざんされていないことを確認できる。
AADはbyte配列の適当なデータで良いが、復号する時にも必要。
サンプルでは「AADは追加認証データ、中身はなんでも良いが64kBytes以内にする」の文字列をUTF-8のバイト列として使っている。
あくまでも認証用なので、暗号強度が上がるわけではない。

# 暗号化
平文と鍵、GCMパラメータがあれば暗号化できる。
AADはオプションなので使わなくても良い。使わない場合はupdateAADの呼び出しが不要。
```java
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があれば復号化できる。
```java
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;
    }
}
```

# まとめ
実際に暗号化・復号化する時はこんな感じ。

```java
    // 暗号化
    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);
```