lesson-3_対称鍵を同期しよう
♻️ 対称鍵を同期しよう
このレッスンでは、対称鍵の同期処理を実装します。同期処理は、3つのケースに応じて行う処理が異なります。
- ユーザーが初めてログインをしたとき
- ユーザーが再ログインをしたとき
- ユーザーが別のデバイスで初めてログインをしたとき
1. ユーザーが初めてログインをしたとき
このとき、初めて対称鍵の生成が行われる(init関数を参照)ので、自身のみが対称鍵を持っている状態です。そのため、新たなデバイスが追加されたときは対称鍵をそのデバイスに同期してあげる必要があります。具体的には以下の3つの処理を行います。
- バックエンドキャニスターから、公開鍵の一覧を取得する
- 取得した公開鍵を用いて、自身の所有する対称鍵を暗号化する
- 暗号化された対称鍵を、暗号化に用いた公開鍵とペアにしてバックエンドキャニスターにアップロードする
上記の処理を行う関数の雛形が、cryptoService.ts内にsyncSymmetricKey
という名前で定義されています。順番に処理を実装していき、この関数を完成させましょう!
まずは、バックエンドキャニスターから公開鍵を取得しましょう。syncSymmetricKey関数内const unsyncedPublicKeys: string[] = [];
の右辺を、下記のように更新します。
// 暗号化された対称鍵を持たない公開鍵一覧を取得します。
const unsyncedPublicKeys: string[] = await this.actor.getUnsyncedPublicKeys();
次に、取得した公開鍵を使って対称鍵を暗号化していきます。この処理は既に実装されているので、コードの確認だけ行いましょう。
受け取った公開鍵を1つずつ使用して、自身の保有する対称鍵を暗号 化していきます。バックエンドキャニスターに公開鍵を登録する際、鍵をエクスポートしてbase64に変換したことを覚えていますか? つまり今受け取った公開鍵は、string型(base64)になっています。このままでは、対称鍵の暗号化に使用できません。もとのCryptoKeyオブジェクトに戻す必要があります。そのため、今度は逆の変換(base64 → バイナリデータ → CryptoKey)を行います。base64からバイナリデータへの変換はbase64ToArrayBuffer
関数が、バイナリデータからCryptoKeyオブジェクトへの変換はimportKeyメソッドが行います。暗号化された対称鍵は、公開鍵と一緒に保存しておきます。
// 自身が保有する対称鍵を公開鍵で暗号化します。
for (const unsyncedPublicKey of unsyncedPublicKeys) {
const publicKey: CryptoKey = await window.crypto.subtle.importKey(
"spki",
this.base64ToArrayBuffer(unsyncedPublicKey),
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
["wrapKey"]
);
const wrappedSymmetricKeyBase64: string = await this.wrapSymmetricKey(
symmetricKey,
publicKey
);
// 公開鍵と暗号化された対称鍵をペアにして保存します。
encryptedKeys.push([unsyncedPublicKey, wrappedSymmetricKeyBase64]);
}
すべての公開鍵で暗号化の処理が終わったら、バックエンドキャニスターに公開鍵と暗号化された対称鍵のペアをアップロードします。下記のコードをfor文の下に追加しましょう。
// 公開鍵と暗号化された対称鍵のペアをアップロードします。
const result = await this.actor.uploadEncryptedSymmetricKeys(encryptedKeys);
if ("Err" in result) {
if ("UnknownPublicKey" in result.Err) {
throw new Error("Unknown public key");
}
if ("DeviceNotRegistered" in result.Err) {
throw new Error("Device not registered");
}
}
これで、対称鍵を同期するsyncSymmetricKey関数が完成しました! それでは、完成したsyncSymmetricKey関数をinit関数から呼び出しましょう。init関数内、/** STEP10: 対称鍵を同期します。 */
の部分に下記のコードを記述します。
/** STEP10: 対称鍵を同期します。 */
console.log("Synchronizing symmetric keys...");
if (this.intervalId === null) {
this.intervalId = window.setInterval(() => this.syncSymmetricKey(), 5000);
}
では、追加したコードを確認しましょう。syncSymmetricKey関数をwindow.setInterval
という関数内で呼び出しました。セクション2でも登場しましたが、setIntervalは一定の時間ごとに指定した関数を実行してくれます。今回は5秒ごとにsyncSymmetricKey関数を実行するように設定しました。setIntervalは、作成されたタイマーを識別する 数値であるIDを返します。このIDが既に設定されているときは、条件式を通過しないので重複してタイマーが設定されないようになっています。
if (this.intervalId === null) {
this.intervalId = window.setInterval(() => this.syncSymmetricKey(), 5000);
}
これで、初めてログインをしたときに行う同期処理が完成しました。
2. ユーザーが再ログインをしたとき
このとき、既にバックエンドキャニスターには自身の公開鍵で暗号化された対称鍵がアップロードされているはずです。具体的には以下の3つの処理を行います。
- バックエンドキャニスターから暗号化された対称鍵を取得する
- 暗号化された対称鍵を復号する
- 対称鍵を同期する
上記の処理を行う関数の雛形が、trySyncSymmetricKey
という名前で定義されています。まずはこの関数を完成させましょう!
最初に、インタフェースで定義した型をインポートします。
import {
_SERVICE,
Result_1,
Result,
} from "../../../declarations/encrypted_notes_backend/encrypted_notes_backend.did";
次に、trySyncSymmetricKey関数を下記のコードで更新しましょう。
public async trySyncSymmetricKey(): Promise<boolean> {
// 対称鍵が同期されているか確認します。
const syncedSymmetricKey: Result =
await this.actor.getEncryptedSymmetricKey(this.exportedPublicKeyBase64);
if ('Err' in syncedSymmetricKey) {
// エラー処理を行います。
if ('UnknownPublicKey' in syncedSymmetricKey.Err) {
throw new Error('Unknown public key');
}
if ('DeviceNotRegistered' in syncedSymmetricKey.Err) {
throw new Error('Device not registered');
}
if ('KeyNotSynchronized') {
console.log('Symmetric key is not synchronized');
return false;
}
} else {
// 暗号化された対称鍵を取得して復号します。
this.symmetricKey = await this.unwrapSymmetricKey(
syncedSymmetricKey.Ok,
this.privateKey,
);
return true;
}
}