0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

外部ライブラリなしでAES暗号化ツールを作る(Web Crypto APIだけで)

0
Posted at

はじめに

Torinoa Tools という個人開発の開発者向けツール集の中に、ブラウザだけでAES暗号化・復号ができるAES Encrypt / Decryptツールがあります。

CryptoJSのような外部ライブラリは使わず、ブラウザ標準の SubtleCrypto(Web Crypto API)だけで実装しています。この記事では、その実装の中身を紹介します。

なぜ外部ライブラリを使わないか

  • ブラウザ完結ツールとして配布サイズを増やしたくない
  • SubtleCrypto はネイティブ実装なので、JSでの実装より高速・安全
  • 依存が少ないほどメンテナンスが楽

一方で、SubtleCrypto は低レベルAPIなので、パスフレーズから鍵を作る部分や、salt/IVの管理は自前で組む必要があります。

パスフレーズから鍵を導出する(PBKDF2)

ユーザーが入力するのは「パスフレーズ」であって、AESの生の鍵ではありません。そこでPBKDF2で鍵導出しています。

const PBKDF2_ITERATIONS = 100_000;

async function deriveKey(
  passphrase: string,
  salt: Uint8Array,
  algorithm: AesAlgorithm,
  keySize: AesKeySize,
): Promise<CryptoKey> {
  const enc = new TextEncoder();
  const baseKey = await crypto.subtle.importKey(
    "raw",
    enc.encode(passphrase),
    "PBKDF2",
    false,
    ["deriveKey"],
  );
  return crypto.subtle.deriveKey(
    { name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
    baseKey,
    { name: algorithm, length: keySize },
    false,
    ["encrypt", "decrypt"],
  );
}

ポイントは2段階になっていることです。

  1. importKey("raw", ..., "PBKDF2", ...) でパスフレーズをPBKDF2用の「鍵の材料」として読み込む
  2. deriveKey(...) で実際にAES用の鍵を導出する

反復回数は10万回(SHA-256)にしています。ブラウザ内で完結させる都合上、あまり回数を増やすとUIがブロックされるため、セキュリティと体感速度のバランスを見て決めた数値です。

saltとIVを暗号文に埋め込む自己完結フォーマット

AESは同じ鍵・同じIVで暗号化すると危険なので、暗号化のたびに新しいsaltとIVをランダム生成する必要があります。復号時にこれらが必要になるため、暗号文と一緒に1本のバイト列にまとめてBase64化しています。

export async function aesEncrypt(
  plaintext: string,
  passphrase: string,
  opts: AesOptions,
): Promise<AesEncryptResult> {
  const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
  const iv = crypto.getRandomValues(new Uint8Array(ivLengthFor(opts.algorithm)));
  const key = await deriveKey(passphrase, salt, opts.algorithm, opts.keySize);

  const cipherBuffer = await crypto.subtle.encrypt(
    opts.algorithm === "AES-GCM"
      ? { name: "AES-GCM", iv }
      : { name: "AES-CBC", iv },
    key,
    new TextEncoder().encode(plaintext),
  );
  const ciphertext = new Uint8Array(cipherBuffer);

  const header = new Uint8Array([algoToByte(opts.algorithm), opts.keySize / 8]);
  const combined = new Uint8Array(
    HEADER_LENGTH + salt.length + iv.length + ciphertext.length,
  );
  combined.set(header, 0);
  combined.set(salt, HEADER_LENGTH);
  combined.set(iv, HEADER_LENGTH + salt.length);
  combined.set(ciphertext, HEADER_LENGTH + salt.length + iv.length);

  return { combined: uint8ToBase64(combined), /* ...hex表示用の内訳 */ };
}

バイナリレイアウトはこうなっています。

[アルゴリズム種別(1byte)] [鍵長/8(1byte)] [salt(16byte)] [IV(12 or 16byte)] [暗号文]

先頭2バイトにアルゴリズムと鍵長を持たせているのは、復号時にAES-GCMかAES-CBCか、鍵長は128/192/256のどれかを、暗号文自体から判別できるようにするためです。これにより、復号する側は「パスフレーズ」だけ入力すればよく、アルゴリズムや鍵長を別途指定させる必要がありません。UIがシンプルになる、実装上のちょっとした工夫です。

復号とタンパリング検知はタダでついてくる

export async function aesDecrypt(
  combinedBase64: string,
  passphrase: string,
): Promise<string> {
  const combined = base64ToUint8(combinedBase64.trim());
  const algorithm = byteToAlgo(combined[0]);
  const keySize = (combined[1] * 8) as AesKeySize;
  // ... salt, iv, ciphertext を切り出す

  const key = await deriveKey(passphrase, salt, algorithm, keySize);

  // パスフレーズ違いや破損データの場合、SubtleCryptoが例外を投げる
  // (AES-GCMは認証タグの検証も兼ねるため改ざん検知にもなる)
  const plainBuffer = await crypto.subtle.decrypt(
    algorithm === "AES-GCM" ? { name: "AES-GCM", iv } : { name: "AES-CBC", iv },
    key,
    ciphertext,
  );
  return new TextDecoder().decode(plainBuffer);
}

AES-GCMは暗号文の末尾に認証タグ(MAC)を含む認証付き暗号(AEAD)なので、パスフレーズが違う・データが壊れている・改ざんされている、のどれであっても crypto.subtle.decrypt が単に例外を投げてくれます。自分で改ざん検知ロジックを書く必要がありません。これはWeb Crypto APIを直接使う一番のメリットだと感じています。

注意点:バイナリ互換は意図していない

このフォーマットは「このツール自身で暗号化・復号が完結する」ことを目的にした自己完結フォーマットであり、OpenSSLなど外部ツールとのバイナリ互換は意図していません。例えば openssl enc -aes-256-cbc で作った暗号文をこのツールに貼り付けても復号できません(ヘッダーのフォーマットが違うため)。あくまで「ブラウザだけで暗号化・復号を完結させたい」というユースケースに特化しています。

おわりに

Web Crypto APIは低レベルなぶん、鍵導出やフォーマット設計は自分で考える必要がありますが、その分「何が起きているか」を細かく制御でき、外部ライブラリへの依存も減らせます。ブラウザで動く暗号ツールを作る際の参考になれば幸いです。

ツールはこちらから試せます: https://tools.torinoa.com/tools/aes-encrypt-decrypt/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?