👩‍💻

#112 Node.js × AES256 〜暗号化アルゴリズムの基礎知識から各モードの実装まで

に公開

はじめに

Node.jsでAES256を用いた暗号化・復号処理の実装方法について調べる機会があったため、その備忘録をまとめていきたいと思います。
本記事は基本知識としてAES256や暗号化処理の概要について触れてから、実装例を紹介する構成としています。

暗号化アルゴリズムについて

暗号化アルゴリズムとは「平文を暗号文に変換する処理における手順や規則」のことを指します。
データを暗号化アルゴリズムに基づいて第三者に解読できない形に変換することで、機密性を保ちつつ安全にデータを保管・転送することができます。
もちろん、そのためにはデータの種類や保護レベルに応じた暗号化アルゴリズムの選定と適用が重要です。


データの変換プロセスには、一般的に暗号化・復号を行うための情報として「鍵」を使用します。
暗号化アルゴリズムの主要な方式としては、下記の3種類が挙げられます。

  • 共通鍵暗号方式
  • 公開鍵暗号方式
  • ハイブリッド暗号方式

以下ではこの3種類の方式について、簡単にではありますが確認していきたいと思います。

共通鍵暗号方式

共通鍵暗号方式では、暗号化と復号に同じ鍵を使用します。
代表的なアルゴリズムとしては、AES、DES、3DESなどが挙げられます。
処理速度が速いため、大量のデータを迅速に暗号化する際には適した暗号方式と言えるでしょう。

課題1:鍵の配送問題

データの送信者と受信者が共通の鍵を使用するため、鍵情報を交換する際にこれが外部に漏れてしまえば、鍵を取得した第三者が容易に暗号データを復号することができてしまいます。
そのため、この方式を使用する場合には、鍵を安全に共有するための追加のセキュリティ対策が求められます。

課題2:鍵の管理が煩雑になりやすい

通信相手ごとにそれぞれ共通鍵を用意する必要があるため、相手が増えれば増えるほど鍵の管理が煩雑になる可能性があります。

公開鍵暗号方式

共通鍵暗号方式における鍵配送問題を解決したのが、公開鍵暗号方式です。
こちらの代表的なアルゴリズムとしては、RSA、DSA、ECDSAなどが挙げられます。


この方式では、暗号化用と復号用はペアとなるそれぞれ別の鍵を使用します。
暗号化用の鍵は公開鍵と呼ばれる、原則誰でも入手できる鍵情報でデータを暗号化します。
これは事前に受信者から送信者に共有される鍵ですが、先にも述べている通り、第三者が取得しても問題ない(=復号できない)情報です。
公開鍵で暗号化されたデータは受信者だけが持つ復号用の鍵、秘密鍵によって復号されます。
秘密鍵を受信者以外が所持する必要性がないため、鍵情報の漏洩リスクが低い暗号方式と言えます。
また、公開鍵暗号方式は公開鍵と秘密鍵のペアがあれば良いので、通信相手ごとに鍵を用意する必要がなく、共通鍵暗号方式に比べてシンプルに鍵の管理ができます。

課題:処理速度が遅い

共通鍵暗号方式と比較して、より複雑な暗号化・復号を行うため、それらの処理速度も遅くなります。

ハイブリッド暗号方式

ハイブリッド暗号方式は、共通鍵暗号方式と公開鍵暗号方式を組み合わせた暗号方式です。
共通鍵暗号方式における鍵の配送問題対策として公開鍵暗号方式を用いるイメージ、といったところでしょうか。


具体的には、

  1. 送信者が任意の共通鍵を生成し、データを暗号化する
  2. 受信者が公開鍵と秘密鍵を生成し、送信者に公開鍵を共有する
  3. 送信者は受け取った公開鍵を使用して共通鍵を暗号化し、①で暗号化したデータと共に受信者に送る
  4. 受信者は②で生成した秘密鍵を使用して受け取った共通鍵を復号し、復号した共通鍵で暗号化されたデータを復号する

となります。
公開鍵暗号方式による処理を共通鍵の暗号化・復号のみとすることで、共通鍵暗号方式の処理速度と公開鍵暗号方式の安全性を兼ね備えた暗号方式を実現しています。


ハイブリッド暗号方式を使用している代表的な例としては、HTTPS通信が挙げられます。

AESとは

AESは「Advanced Encryption Standard」の略称で、NIST(米国国立標準技術研究所)によって2001年に承認されて以来、2024年11月現在も標準的に使用されている共通鍵暗号方式の暗号化アルゴリズムです。
また、CRYPTREC暗号リスト(電子政府推奨暗号リスト)の共通鍵暗号_128ビットブロック暗号として推奨されている暗号技術でもあります。


AESは無線LANや圧縮ファイルの暗号化などにも利用されており、鍵長を128bit、192bit、256bitの3パターンから選んで使用することができます。
bit数が大きいほど暗号強度が高くなるため、AES256が最も暗号強度の高いAESであると言えますね。

AESの変換処理

下記4種類の変換処理、

  • SubBytes
  • ShiftRows
  • MixColumns
  • AddRoundKey

を1セットとして、選択した鍵長に応じた回数繰り返すことで、データを暗号化します。
復号する時はこれと逆の変換を行います。

ブロック暗号の暗号利用モード

共通鍵暗号方式はブロック暗号とストリーム暗号の2種類に分けることができます。

  • ブロック暗号
    • 平文を特定の長さに区切り、その単位ごとに暗号化するアルゴリズム
    • 安全性の検証はブロック暗号の方が進んでいる
  • ストリーム暗号
    • 1bitや1byteなどの細かい単位で暗号化するアルゴリズム
    • よりリアルタイム性が求められる通信に向いている

このブロック暗号における、ブロック長よりも長いデータを暗号化するための方法として、下記のような暗号利用モードが挙げられます。

  • ECBモード
    • 平文をブロックごとに暗号化していく方法
    • 脆弱性が多く、使用は推奨されない
  • CBCモード
    • IVと平文をXORした結果を暗号化し、その次のブロックでは前の暗号文ブロックと平文ブロックをXORした結果を暗号化し…と続けていく方法
      • IV(Initialization Vector):初期化ベクトル
      • XOR(XOR Operation):XOR演算
    • いくつかの脆弱性が発見されている
  • CTRモード
    • カウンタ値を暗号化した結果と平文をXORする方法
  • GCMモード
    • CTRモードにガロア認証を行うことでデータ改ざんを検知する方法
      • ガロア認証:カウンタ値や暗号文ブロックなどから求めたハッシュ値(GHASH)より生成されたタグを用いる認証方法
    • 2024年11月現在の主流なモード

AES256の暗号利用モードとしては、GCMモードとCBCモードが提供されているようです。

Node.js Cryptoを用いた暗号化/復号の実装例

AESによる暗号化を行うためには、事前に秘密鍵を生成しておく必要があります。
これは外部に漏らしてはいけない重要な情報となるため、実際に使用する場合は取り扱いに十分注意しましょう。


Node.jsのcryptoには、秘密鍵を生成するためのメソッドが提供されています。
AES用の秘密鍵を生成するメソッドとしては以下のようなものがあり、callback関数を引数に持ちたいかどうかで使い分けることができます。


構文:

crypto.generateKey(type, options, callback)
  • type
    • aes/hmac
  • options
    • length: aesの場合、鍵長を指定(128、192、256)
  • callback
    • err: Error
    • key: keyObject

実装例:

crypto.generateKey("aes", { length: 256 }, (err, key) => {
  if (err) throw err;

  console.log(key.export().toString("hex"));
});

構文:

crypto.generateKeySync(type, options)
  • type
    • aes/hmac
  • options
    • length: aesの場合、鍵長を指定(128、192、256)

実装例:

const secretKey = crypto.generateKeySync("aes", { length: 256 }).export();

.export()を使用することで、バッファ形式に変換することができます。


それでは実際に、秘密鍵の値を変数secretKeyに格納している、としてCBCモード・GCMモードそれぞれの実装例を確認していきましょう。

CBCモード

import crypto from "crypto";
// 秘密鍵として使用する値は既にSECRET_KEYに格納済みとする
import SECRET_KEY from "./xxx";

/**
 * AES256(CBCモード)による暗号化
 * @param inputSentence - 暗号化される入力値
 * @return 暗号化されたデータ
 */
export function encryptModeCbc(inputSentence: string) {
  // 使用するアルゴリズムを指定
  const algorithm = "aes-256-cbc";

  // iv(初期化ベクトル)を作成
  const iv = crypto.randomBytes(16);

  // 暗号を作成
  const cipher = crypto.createCipheriv(algorithm, SECRET_KEY, iv);

  // 暗号化されたデータを作成
  try {
    const encryptedData = Buffer.concat([
      cipher.update(inputSentence,"utf-8"),
      cipher.final(),
    ]).toString("hex");
  } catch (error) {
    return error;
  }

  return {encryptedData: encryptedData, iv: iv.toString("hex")};
}

/**
 * AES256(CBCモード)による復号
 * @param encryptedData - 暗号化されたデータ
 * @param iv - 初期化ベクトル
 * @return 復号されたデータ
 */
export function decryptModeCbc(encryptedData: string, iv: string) {
  // 使用するアルゴリズムを指定
  const algorithm = "aes-256-cbc";

  // 解読器を作成
  const decipher = crypto.createDecipheriv(algorithm, SECRET_KEY, Buffer.from(iv, "hex"));

  // 復号されたデータを作成
  try {
    const decryptedData = Buffer.concat([
      decipher.update(Buffer.from(encryptedData, "hex")),
      decipher.final(),
    ]).toString("utf-8");
  } catch (error) {
    return error;
  }

  return decryptedData;
}

const inputSentence = "柿くへば鐘が鳴るなり法隆寺"として実行してみたところ、暗号化・複合処理の結果それぞれ以下のようになりました。

  • 暗号化
    • encryptedData:"e28f74c5a6bcfcad3f8bba2f8df5b610bd..."
    • iv:"8b2a5d7c92bc4f1d891a9dbed8bdbd1e"
  • 復号
    • "柿くへば鐘が鳴るなり法隆寺"

※暗号化処理の戻り値は実行ごとに異なります


CBCモードのivサイズは16byteです。
ちなみに、ivは暗号化処理ごとにユニークであることが大切です。
ただし、iv自体は機密情報に含まれないため、公開しても問題ありません。(GCMモード共通)

GCMモード

import crypto from "crypto";
// 秘密鍵として使用する値は既にSECRET_KEYに格納済みとする
import SECRET_KEY from "./xxx";

/**
 * AES256(GCMモード)による暗号化
 * @param inputSentence - 暗号化される入力値
 * @return 暗号化されたデータ
 */
export function encryptModeGcm(inputSentence: string) {
  // 使用するアルゴリズムを指定
  const algorithm = "aes-256-gcm";

  // iv(初期化ベクトル)を作成
  const iv = crypto.randomBytes(12);

  // 暗号を作成
  const cipher = crypto.createCipheriv(algorithm, SECRET_KEY, iv);

  try {
    // 暗号化されたデータを作成
    const encryptedData = Buffer.concat([
      cipher.update(inputSentence, "utf-8"),
      cipher.final(),
    ]).toString("hex");
    
    // 認証タグを取得
    const authTag = cipher.getAuthTag();
  } catch (error) {
    return error;
  }

  return {
    encryptedData: encryptedData,
    iv: iv.toString("hex"),
    authTag: authTag.toString("hex"),
  };
}

/**
 * AES256(GCMモード)による復号
 * @param encryptedData - 暗号化されたデータ
 * @param iv - 初期化ベクトル
 * @param authTag - 認証タグ
 * @return 復号されたデータ
 */
export function decryptModeGcm(
  encryptedData: string,
  iv: string,
  authTag: string
) {
  // 使用するアルゴリズムを指定
  const algorithm = "aes-256-gcm";

  // 解読器を作成
  const decipher = crypto.createDecipheriv(algorithm, SECRET_KEY, Buffer.from(iv, "hex"));

  // 認証タグを設定
  decipher.setAuthTag(Buffer.from(authTag, "hex"));

  // 復号されたデータを作成
  try {
    const decryptedData = Buffer.concat([
      decipher.update(Buffer.from(encryptedData, "hex")),
      decipher.final(),
    ]).toString("utf-8");
  } catch (error) {
    return error;
  }

  return decryptedData;
}

CBCモードと同じくconst inputSentence = "柿くへば鐘が鳴るなり法隆寺"として実行してみたところ、暗号化・複合処理の結果それぞれ以下のようになりました。

  • 暗号化
    • encryptedData:"98a6f73e1c4e84a69f7b6c53aef5733b..."
    • iv:"8b9e7c7e31f1a9c8f7128e0c"
    • authTag:"5c2a3db849fb923d7d3a82af4d95a5ef"
  • 復号
    • "柿くへば鐘が鳴るなり法隆寺"

※こちらも暗号化処理の戻り値は実行ごとに異なります


GCMモードのivの推奨サイズは12byteです。
参考:CRYPTREC暗号リスト(電子政府推奨暗号リスト)の電子政府における調達のために参照すべき暗号のリスト(注4)


CBCモードと比較すると、認証タグの処理が追加されていることがわかるかと思います。

おわりに

今回はNode.jsのCryptoを使用した暗号化・復号について、その基本知識となる暗号化アルゴリズムなどにも触れながら、実装例を確認してみました。


AESは電子政府推奨暗号リストにも掲載されている信頼性の高いアルゴリズムということでしたが、使い方や情報管理の仕方によってはその信頼性を担保できなくなってしまう危険性があるため、実際に使用する際には取り扱いに注意していきたいと思います。
ちなみに、Node.jsにはCryptoとは別にWeb Crypto APIというものがあるようです。こちらについてはまた別の機会に。


また、暗号方式について調べている際、共通鍵暗号方式に比べて公開鍵暗号方式でかかる時間は数百〜数千倍になると記載しているサイトもありました。
実際にどの程度処理速度に差が生まれるのか比較してみても面白そうだなと思いました。


今回は取り上げる範囲が広くなってしまったため、全体的には概要をまとめる程度となってしまいました。
特に、暗号利用モードについては簡単にしか触れられていないので、興味のある方はぜひ調べてみてください。

参考

暗号化アルゴリズムについて

AESについて

ブロック暗号のモードについて

その他

Discussion