Google Tinkで詳しくなくても安全に暗号を扱う
アプリケーションの中で暗号技術を使うのは、暗闇でチェーンソーをジャグリングしているようなものであっちゃだめだよね。
Tinkのドキュメントの序文だ。暗号を安全に扱うための、Googleの精鋭セキュリティエンジニアが作成したライブラリだ。今回は、このライブラリで暗号を扱う方法を学ぶ。
このスライドに書かれていることを、ざっと理解してみる。
まず、セキュリティもユーザー中心で設計されるべきだよね。という話が前置きである。これは、人間中心デザインみたいな、わかりやすさ、みんなへの使いやすさ意識の流れの一環であろう。
開発者にとって、アルゴリズムの選択って難しいよね。特にセキュアにアルゴリズムを組み合わせるのは責任が大きすぎる!!!
RSA with PKCS #1v1.5 encryptionなんて、ほぼ誰も知らない!
だし、インターフェースも分かりづらい!
開発者のせいにするんじゃなくて、コアな機能は書き換えづらくしたり、経験のない開発者にも使いやすいAPIにしようぜ!というのがTinkライブラリが作られた背景。使いづらかったら、検索して一番上に出てきた、動くけれど邪道でやるべきこと全て放棄するようなコードをコピペしてしまうもんな。開発者というのは。だから、このライブラリはとてもありがたい。Googleが開発しているというだけで、脳死で採用するに値する。
ただ、この記事を書いている途中で気づいたのだが、どうやらTypescript用のリポジトリーはメンテナンスされていないようだ。外部に接続するコストが高すぎて、メンテナンスするリソースがないとのことであった。業務ではnodeによる実装を想定していたので、出鼻をくじかれた形になる。だが、実践したことは発信しておいたほうが得なので、そのまま続けることにする。
実践
やってみよう。とは言ったものの、どこまでやるかを決めたほうが良い。ということで、今回は
- 暗号化と復号が両方できること
- 暗号化して複合しても、同じデータであること
これを確認できるところまでを範囲とする。また、コードはclojureで書いていく予定だ。実態はjavaである。
環境構築
環境構築は、nix-shellを利用して、leiningenをインストールし、replを基本として開発する。以下、説明モードに入るためですます調にする。
環境構築
nix-shellを利用しています。nix-shellを使うことで、環境を汚さずに環境を構築できるので、重宝しています。以下のようなshell.nixを用意しました。buildInputsの欄を見ると、leiningenとjdkをインストールしていることがわかりますね。
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.leiningen
pkgs.jdk20
];
}
nix-shell
lein new myapp
2023-09-21時点で、Tink for Javaのバージョンは1.10.0でした。
clojureのパッケージ管理ツールであるleiningenを利用しているので、project.cljを編集し、Tink for Javaをインストールします。
(defproject myapp "0.1.0-SNAPSHOT"
...
:dependencies [...
[com.google.crypto.tink/tink "1.10.0"]]
...)
理解してくべきコンセプト
公式のドキュメントを参考にしながら、理解しておくべきコンセプトを確認していきます。今回実行したいのは公開鍵による暗号化です。それを実装するために必要な概念を拾っていきましょう。そのままドキュメントをまとめるというより、どのようにドキュメントを読んでいったかを記録していく方針で書きます。
ドキュメントの概要部分を見ていくと、どうやら、理解するべき概念は4つあるようです。
- プリミティブ
- キーのタイプ
- 鍵のセット
- キーセットハンドル
名前をざっと見て、理解しやすそうなものと理解しづらそうな概念にあたりをつけます。ぱっと見、プリミティブとキーセットハンドルが分かりづらいですね。一つひとつ見ていきます。
プリミティブとは
プリミティブの日本語訳は原始的な、ですがここではどういう意味で原始的なのでしょうか?何かを構成する最小限の単位、のような概念ということでしょうか?
暗号アルゴリズムの詳細と鍵のタイプを定義
とありました。アルゴリズムについては、なんとなく想像できます。RSA暗号、みたいな暗号化のためのアルゴリズムを言っているのだろうと予測できます。
ただ、鍵のタイプとは何でしょうか?
ちょっと詳しく確認してみます。プリミティブ
プリミティブは、なんらかのタスクを安全に実行するすべてのアルゴリズムに対応する_数学的_オブジェクトです。たとえば、AEAD プリミティブは Tink が Aead に必要なセキュリティ プロパティを満たすすべての暗号化アルゴリズムで構成されています。
ほう、どうやらプリミティブというのは、暗号化についてを完全に数学的に、抽象的に切り離すために必要な用語というわけですね。個別具体的なRSAではなく、それら全ての総称をプリミティブと呼ぶことで、特定の言語限定の言葉(インターフェース
など)を使ってしまうことを避けたかった。暗号化についてだけを考える層を追加するために必要な言葉だったと。
ということで、プリミティブを見れば、数学的な側面における暗号化の情報が書かれているということですね。
鍵のタイプ、というのは明示的にはわかりませんでしたが、暗号アルゴリズムの詳細
と並列で紹介されているので、おそらく暗号アルゴリズムの名前、といった意味合いで使われる言葉かと思います。そして、暗号アルゴリズムの詳細というのは、暗号アルゴリズムの実装自体ということでしょう。プリミティブを解説しているページでは、「数学的な暗号化手法と、それにアクセスするインターフェースを明示的に分離している」ということが強調されていました。
キーセットハンドルとは
では、キーセットハンドルについて見ていきます。キーのセットのハンドル、ということです。特に、ハンドルと言う言葉が分かりづらいです。ハンドルと言うコンピューターサイエンスの言葉でパッと思い当たるのは、ハンドラー
ですね。ハンドラーとは、何かを操作する関数、みたいな意味合いだったと思います。
違いました、イベントに対して応答するロジック・関数のことを指すようです。うーん、キーセットハンドルとはあまり関係なさそう。ということで、辞書を引いてみたところ、以下の記述がありました。
ハンドル◆プログラミングにおいて、リソース(例えばファイル)にアクセスするための識別子の一種。通例「その識別子が内部的にどう使われるのか、コードを呼び出す側には分からない場合」が含意される。
まさにこれがハンドルの意味として適していそうです。なぜなら、なるべく開発者も鍵を操作しないようにしたいですからね。勝手に秘密鍵を漏洩できるような作りだと、Tinkのテーマであるユーザーフレンドリーなライブラリ、に反します。
では、これを念頭に置きながらドキュメントを読んでみます。
実際の機密性の高い鍵マテリアルの露出を制限します
とある通り、安全に鍵を操作するための概念で間違いなさそうです。鍵を操作するための取っ手、ということですね。
では、残り二つの概念を見ていきましょう。おそらく、これらを理解してどの暗号化アルゴリズムを使うか決定すれば、実装に入れるはずです。よし、あと少しだ。
キーのタイプ
これは、特定のプリミティブを実装するものだそうです。あれ、先程のプリミティブの項目では鍵のタイプは暗号アルゴリズムの名前、みたいに一旦理解したのですが、違いそうです。ちょっと英語に直して読んでみたほうが良いかも。
A key type implements a specific primitive.
どうやら、キーのタイプというのは、プリミティブのなかでも実装手法を指定するためのもの、のよう。プリミティブ自身、複数のキータイプを持っている。そして、実装上の要件に従ってキータイプを選択する。(実行時間や容量など)
AEADはプリミティブですが、その中にもAES128_GCMなど複数キータイプが存在するようです。要件に応じて選択する必要があるというわけですね。キータイプとプリミティブの関係について、おそらく正確に理解できていないと思います。ですが、実装上必要と予測されるのは、「要件の種類に応じてキーのタイプを選択する」ということが分かれば十分だと思いますので、深入りは避けようと思います。
鍵のセット
これは簡単に予想がつき、おそらく公開鍵や秘密鍵のセットのことだと思います。見てみましょう。
キーのローテーションを容易にする一連のキー
ん、ちょっと違いますね。公開鍵や秘密鍵ではなく、それらをローテーションするのを用意にするキー、とのことでした。ローテーションとは何でしょうか?セキュリティを高めて、ユーザーに不用意に大事な情報を露出させないために必要な行為、ではあると思いますが。
とりあえず理解できることをまとめてみます。
- キーセット内には複数のキーがある
- それぞれのキーはIDを持っていて識別可能
- どのキーが使われたのかを示すために、様々な値の接頭辞にIDは使われる
- 全てのキーは、プリミティブは同じでなくてはいけない。キータイプは違っても構わない。
つまり、キーセットは、同じプリミティブの一連のキーで、それらをローテーションすることで、漏洩のリスクをより避けようという意図のもと必要なもの、と言えるだろう。
ちなみに、鍵を生成するために、Google TinkはTinkeyというCLIを用意している。
ユースケースを見定める
ここまでで、実際に使うために必要な概念を学びました。ここから、実際に使っていきたいと思います。さて、私は公開鍵を使って、暗号化をしたいのでした。そこでドキュメントを見てみると、Tinkの設定の他に、複数のユースケースが存在しているのが見て取れます。
2つのページが関連していそうです。
ただ、ここでの確定的という言葉がわかりません。念の為、英語のリンクも確認しますと、deterministicallyという語でした。EthereumのWalletでhierarchical deterministic walletと言うものがあったのを思い出します。そこでは、確定的というのは一つのマスター鍵から階層的に子孫鍵を生成するという方法をとるWalletでした。
今回私は、シンプルに暗号化、復号ができれば良いので一番はじめのページのやり方を採用することとします。
実際に使ってみる
概念を理解し、やり方も決まったので、ようやく実装に入ることができます。やることはシンプルに2つだけです。
- Tinkを設定する
- データの暗号化に沿って実装する
それでは、早速やっていきましょう。
Tinkを設定する
1.Tinkを設定する、と言われてちょっとハードルが高く感じていたんですが、ただの依存関係を設定するだけでした。完了済みです。
データの暗号化
2.暗号化メソッドはどうすればいいでしょうか?ドキュメントによると、
ほとんどのデータ暗号化のユースケースでは、AES128_GCM 鍵タイプを含む AEAD プリミティブをおすすめします。
との事でしたので、AEADプリミティブを選択することとします。そして、clojureで暗号化と復号をする実装をしていきましょう。
鍵を生成する方法は複数ありまして、公式で推奨されているのはTinkeyを利用する方法です。今回は暗号化して複合できることを確認するだけなので、APIを利用して生成します。
公式のサンプルを見ながら、手順を考えましょう。まずやらなければいけないのはAeadConfigの設定です。そして、Keysetを生成し(平文でキーセットを生成する)、バイト配列を暗号化・復号します。
(defn test [plaintext]
(AeadConfig/register) ;; 設定を初期化する
(let [handle (KeysetHandle/generateNew (KeyTemplates/get "AES128_GCM"))] ;; KeysetHandleを作成します。
(try
(let [aead (.getPrimitive handle (Class/forName "com.google.crypto.tink.Aead"))] ;; aeadプリミティブを作成します。これが、暗号化と復号をしてくれるものです。
(println plaintext)
(let [ciphertext (.encrypt aead (.getBytes plaintext "UTF-8") (byte-array 0))] ;; 暗号化する。バイトにしないと暗号化できないことに注意。
(println ciphertext)
(let [decryptedtext (String. (.decrypt aead ciphertext (byte-array 0)) "UTF-8")]
(println decryptedtext)
(= decryptedtext plaintext))))
(catch GeneralSecurityException ex
(println (str "Cannot create primitive, got error: " ex))))
))
(test "plaintext") => true
ということで、暗号化と復号を確認することができました!
総括
今回は暗号化と復号をすることだけを主眼としてきました。実際の業務で利用する際は、鍵の保存などに神経を使うことになります。そのため、KMS(鍵管理システム)などを利用する事になりそうです。
ここまでシンプルに暗号化して復号する事ができる、ということがわかって素晴らしいなとつくづく思いました。ただ、typescriptに対応していない、ということが残念です。
Discussion