【Java初心者】Google Tinkを使ったハイブリッド暗号

# ハイブリッド暗号とは
Wikipediaによると
```
公開鍵暗号方式と共通鍵暗号方式を組み合わせた暗号方式である。
公開鍵暗号方式と共通鍵暗号方式の欠点を補い合い、
2つの暗号方式のそれぞれ利点を同時に利用することができる。
```
とあります。

具体的な方法については
```
1. Aは共通鍵を生成する
2. Aは共通鍵を公開鍵暗号方式で暗号化してBに送信する
3. AとBは安全に共有された共通鍵を用いて共通鍵暗号方式で通信を暗号化する
```
となっている。

これならば、公開鍵暗号でもいいのではないかと思うが 上記wikipediaでは
```
公開鍵暗号方式には、共通鍵暗号方式と比較して処理速度が非常に低速という欠点と、
事前の鍵共有が不要という利点がある。
そのため、公開鍵暗号方式のみで通信を暗号化するのは非効率的である
(中略)
ハイブリッド暗号方式には、公開鍵暗号方式と比較して処理速度が非常に高速という利点と、
事前の鍵共有が不要という利点がある。
つまり、ハイブリッド暗号方式は公開鍵暗号方式と共通鍵暗号方式の利点を
どちらも同時に利用できる、優れたそして大きな欠点のない暗号方式である。
```
と記載されている。

Tinkを使った実際のコードでは、公開鍵暗号と似たような方法になる。

# Javaで書いてみました

Tinkは、バージョンアップのたびに、APIが変わり、以前書いたコードが非推奨となることが多いため、その都度ドキュメントを見ながら修正しています。

以下に示すコードはtink-1.12.0.jarを用いています。依存ライブラリーとして protobuf-java と gson が必要です。

```java
package sample2024;

import com.google.crypto.tink.CleartextKeysetHandle;
import com.google.crypto.tink.HybridDecrypt;
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.JsonKeysetReader;
import com.google.crypto.tink.KeyTemplates;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.hybrid.HybridConfig;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Base64;

public class HybridTest {

    public static void main(String[] args) {
        try {
            String plainText = "こんにちは"; // 平文テキスト
            String password = "password"; // パスワード(ただ実際の運用では空白か)
            HybridConfig.register();
            System.out.println("Hybrid秘密鍵を生成");
            KeysetHandle privateHybridKeysetHandle = KeysetHandle.generateNew(KeyTemplates.get("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM"));
            String hyb_privateKey
                    = TinkJsonProtoKeysetFormat.serializeKeyset(privateHybridKeysetHandle, InsecureSecretKeyAccess.get());
            // 秘密鍵をファイルに保存する場合(セキュリティー上推奨されていません)
            // String hybrKey = "hybr_priv.key";
            // Path hybyFile = Paths.get(hybrKey);
            // System.out.println("Hybrid秘密鍵を保存");
            // Files.write(hybyFile, serializedHybridKeyset.getBytes(UTF_8));
            //
            // 秘密鍵をファイルから読み込む(セキュリティー上推奨されていません)
            // serializedHybridKeyset = Files.readString(hybyFile, UTF_8);
            System.out.println("Hybrid暗号用の鍵を生成:");
            String hyb_publicKey = getPublicKey(hyb_privateKey);
            System.out.println("Hybrid暗号用の鍵(相手に渡すもの):" + hyb_publicKey);
            // ハイブリッド暗号化(相手がやってくれる)
            String HybridEncrypt = HybridEncrypt(plainText, hyb_publicKey, password);
            System.out.println("Hybrid暗号化:" + HybridEncrypt);
            // ハイブリッド暗号の復号(相手からデータが届いてこちらの秘密鍵で復号)
            String HybridDecrypt = HybridDecrypt(HybridEncrypt, hyb_privateKey, password);
            System.out.println("Hybrid暗号の復号化:" + HybridDecrypt);
        } catch (IOException | GeneralSecurityException e) {
            System.err.println(e.getMessage());
        }
    }

    /**
     * 秘密鍵から公開鍵を出力
     *
     * @param secKey 秘密鍵
     * @return 公開鍵
     * @throws java.io.IOException
     * @throws java.security.GeneralSecurityException
     */
    private static String getPublicKey(String secKey) throws IOException, GeneralSecurityException {
        KeysetHandle keysetHandle = CleartextKeysetHandle.read(
                JsonKeysetReader.withString(secKey));
        KeysetHandle publicKeysetHandle
                = keysetHandle.getPublicKeysetHandle();
        return TinkJsonProtoKeysetFormat.serializeKeyset(publicKeysetHandle, InsecureSecretKeyAccess.get());
    }

    /**
     * ハイブリッド暗号による暗号化
     *
     * @param plaintext 平文
     * @param pubkey 公開鍵
     * @param password パスワード
     * @return
     */
    private static String HybridEncrypt(String plaintext, String pubkey, String password) throws GeneralSecurityException, IOException {
        KeysetHandle keysetHandle = CleartextKeysetHandle.read(
                JsonKeysetReader.withString(pubkey));
        HybridEncrypt encryptor = keysetHandle.getPrimitive(HybridEncrypt.class);
        byte[] cipherText = encryptor.encrypt(plaintext.getBytes(), password.getBytes());
        return Base64.getEncoder().encodeToString(cipherText);
    }

    /**
     * ハイブリッド暗号を復号化する
     *
     * @param enctext 暗号化テキスト
     * @param seckey 秘密鍵(自分の)
     * @param password パスワード
     * @return 平文
     */
    private static String HybridDecrypt(String enctext, String seckey, String password) throws GeneralSecurityException, IOException {
        KeysetHandle keysetHandle = CleartextKeysetHandle.read(
                JsonKeysetReader.withString(seckey));
        HybridDecrypt decryptor = keysetHandle.getPrimitive(HybridDecrypt.class);
        return new String(decryptor.decrypt(Base64.getDecoder().decode(enctext), password.getBytes()));

    }

}
```

# 実行結果

```
Hybrid秘密鍵を生成
Hybrid暗号用の鍵を生成:
Hybrid暗号用の鍵(相手に渡すもの):{"primaryKeyId":1257658048,"key":[{"keyData":{"typeUrl":"type.googleapis.com/google.crypto.tink.HpkePublicKey","value":"EgYIARABGAIaIGO34vQvJl2ppcyIxirWatZcQ3gZMm0nZXmOG1WX4rVk","keyMaterialType":"ASYMMETRIC_PUBLIC"},"status":"ENABLED","keyId":1257658048,"outputPrefixType":"TINK"}]}

Hybrid暗号化:AUr2VsDz8bzDIdkQk3nvUWW2BPrgjWPK1R5YbTVh3Hsf2pt2OcNRnrs9U7w4TyPpMv2v5YK6tup7bxlVO5msKyM3sGs=
Hybrid暗号の復号化:こんにちは
```
となります。