NodeJS 速習チュートリアル

Node.js Crypto モジュール

1. Crypto モジュールとは?

Crypto モジュールは、以下のような暗号化機能を提供する Node.js のビルトインモジュールです:

  • ハッシュ関数(SHA-256、SHA-512 など)
  • HMAC(Hash-based Message Authentication Code)
  • 対称鍵暗号(AES、DES など)
  • 非対称鍵暗号(RSA、ECDSA など)
  • 電子署名とその検証
  • 安全な ランダムデータ生成

Crypto モジュールは、機密情報を安全に扱う必要があるアプリケーションにとって不可欠です。このモジュールは OpenSSL ライブラリをラップしており、確立されテスト済みの暗号アルゴリズムへのアクセスを提供します。

このモジュールは、主に以下のような機密データの処理に使用されます:

  • ユーザー認証とパスワードの保存
  • 安全なデータ転送
  • ファイルの暗号化と復号
  • セキュアな通信チャネルの構築

2. Crypto モジュールの始め方

以下は、Crypto モジュールを使用して文字列をハッシュ化する簡単な例です。

基本的なハッシュ化の例

const crypto = require('crypto');

// 文字列の SHA-256 ハッシュを作成
const hash = crypto.createHash('sha256')
  .update('Hello, Node.js!')
  .digest('hex');

console.log('SHA-256 ハッシュ:', hash);

2.1 Crypto モジュールのインポート

Crypto モジュールは Node.js に標準で含まれているため、インストールは不要です。スクリプト内で require するだけで使用できます。

const crypto = require('crypto');

3. ハッシュ関数

ハッシュ化 とは、データを固定長の文字列に変換する一方通行の処理です。ハッシュ関数にはいくつかの重要な性質があります:

  • 決定論的(Deterministic): 同じ入力は常に同じ出力を生成します
  • 固定長(Fixed Length): 入力サイズに関わらず、出力は常に同じサイズになります
  • 一方向性(One-Way): 出力から元の入力を復元することは極めて困難です
  • 雪崩効果(Avalanche Effect): 入力のわずかな違いが、出力に劇的な変化をもたらします

主なユースケース:

  • パスワードの保存
  • データの完全性(Integrity)の検証
  • 電子署名
  • コンテンツアドレッシング(例:Git, IPFS)

3.1 ハッシュの作成

const crypto = require('crypto');

// ハッシュオブジェクトを作成
const hash = crypto.createHash('sha256');

// データをハッシュに追加
hash.update('Hello, World!');

// 16進数形式でダイジェスト(ハッシュ値)を取得
const digest = hash.digest('hex');
console.log(digest);

この例の解説:

  • createHash(): 指定したアルゴリズムでハッシュオブジェクトを作成します
  • update(): 指定したデータでハッシュの内容を更新します
  • digest(): ダイジェストを計算し、指定した形式で出力します

3.2 一般的なハッシュアルゴリズム

const crypto = require('crypto');
const data = 'Hello, World!';

// MD5 (セキュリティが重要な用途には非推奨)
const md5 = crypto.createHash('md5').update(data).digest('hex');
console.log('MD5:', md5);

// SHA-1 (セキュリティが重要な用途には非推奨)
const sha1 = crypto.createHash('sha1').update(data).digest('hex');
console.log('SHA-1:', sha1);

// SHA-256
const sha256 = crypto.createHash('sha256').update(data).digest('hex');
console.log('SHA-256:', sha256);

// SHA-512
const sha512 = crypto.createHash('sha512').update(data).digest('hex');
console.log('SHA-512:', sha512);

警告:MD5SHA-1 は暗号学的に弱いとされており、セキュリティが重要なアプリケーションでは使用すべきではありません。代わりに SHA-256SHA-384、または SHA-512 を使用してください。

4. パスワードセキュリティ

パスワードを扱う際は、総当たり攻撃(ブルートフォース攻撃)を防ぐために、意図的に計算負荷を高く設計された専用のパスワードハッシュ関数を使用することが極めて重要です。

単純なハッシュが不十分な理由:

  • パスワードをプレーンテキストや、MD5・SHA-1 のような単純なハッシュで保存してはいけません。
  • これらは レインボーテーブル や総当たり攻撃で簡単に解読されてしまいます。

4.1 パスワードセキュリティの重要概念

  • ソルト(Salting): ハッシュ化の前に、各パスワードに一意のランダムな値を追加します
  • ストレッチング(Key Stretching): ハッシュプロセスを意図的に遅くし、総当たり攻撃を困難にします
  • 計算コスト(Work Factor): ハッシュ化にどれだけの計算リソースを費やすかを制御します

4.2 ソルト(Salt)とは?

ソルトはユーザーごとに固有のランダムな文字列です。ハッシュ化の前にパスワードと組み合わせることで、たとえ 2 人のユーザーが同じパスワードを持っていても、ハッシュ値が異なるようになります。これにより、攻撃者が計算済みテーブル(レインボーテーブル)を使用して複数のパスワードを一度に解読することを防ぎます。

4.3 Node.js でのパスワードハッシュ化の実装

const crypto = require('crypto');

// パスワードをハッシュ化する関数
function hashPassword(password) {
  // ランダムなソルトを生成 (16バイト)
  const salt = crypto.randomBytes(16).toString('hex');

  // パスワードハッシュ化に scrypt を使用 (推奨)
  const hash = crypto.scryptSync(password, salt, 64).toString('hex');

  // 保存のためにソルトとハッシュの両方を返す
  return { salt, hash };
}

// パスワードを検証する関数
function verifyPassword(password, salt, hash) {
  const hashedPassword = crypto.scryptSync(password, salt, 64).toString('hex');
  // 取得したハッシュと保存されているハッシュを比較
  return hashedPassword === hash;
}

// 使用例
const password = 'mySecurePassword';

// 保存用のハッシュを作成
const { salt, hash } = hashPassword(password);
console.log('ソルト:', salt);
console.log('ハッシュ:', hash);

// ログイン試行の検証
const isValid = verifyPassword(password, salt, hash);
console.log('パスワード有効:', isValid); // true

       注意: プロダクション環境でのパスワードハッシュ化には、安全なパスワード処理のために特別に設計された bcryptargon2 といった専用ライブラリの使用も検討してください。

5. HMAC (Hash-based Message Authentication Code)

HMAC は、暗号ハッシュ関数と秘密の暗号鍵を組み合わせた特定のタイプのメッセージ認証コードです。データの 完全性認証(送信元が正しいこと)の両方を提供します。

5.1 HMAC の使用場面

  • API リクエストの検証
  • セキュアなクッキーやセッション
  • データの完全性チェック
  • Webhook の検証

5.2 HMAC のセキュリティ特性

  • メッセージの完全性: メッセージが少しでも変更されると、異なる HMAC が生成されます
  • 真正性(Authenticity): 秘密鍵を知っている者だけが有効な HMAC を生成できます
  • 非暗号化: HMAC はメッセージを暗号化するものではなく、整合性を検証するだけのものです

5.3 HMAC の実装

const crypto = require('crypto');

const secretKey = 'mySecretKey';

// HMAC オブジェクトを作成
const hmac = crypto.createHmac('sha256', secretKey);

// データを更新
hmac.update('Hello, World!');

// ダイジェストを取得
const hmacDigest = hmac.digest('hex');
console.log('HMAC:', hmacDigest);

5.4 HMAC によるメッセージ検証

const crypto = require('crypto');

function createSignature(message, key) {
  const hmac = crypto.createHmac('sha256', key);
  hmac.update(message);
  return hmac.digest('hex');
}

// メッセージ署名の検証
function verifySignature(message, signature, key) {
  const expectedSignature = createSignature(message, key);
  // タイミング攻撃を防ぐために timingSafeEqual を使用
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

// 使用例
const secretKey = 'verySecretKey';
const message = '検証が必要な重要なメッセージ';

const signature = createSignature(message, secretKey);
console.log('署名:', signature);

const isValid = verifySignature(message, signature, secretKey);
console.log('署名は有効か:', isValid); // true

       注意: セキュリティ上重要な値を比較する際は、タイミング攻撃(Timing Attacks) を防ぐために必ず crypto.timingSafeEqual() を使用してください。

6. 対称鍵暗号(共通鍵暗号)

対称鍵暗号 は、暗号化と復号に同じ鍵を使用します。一般的に非対称鍵暗号よりも高速で、以下に適しています:

  • 大量データの暗号化
  • データベースの暗号化
  • ファイルシステムの暗号化
  • セキュアなメッセージング(鍵交換と組み合わせて)

6.1 一般的な対称鍵アルゴリズム

アルゴリズム鍵サイズブロックサイズ備考
AES-256256 bits128 bits現在の標準、広く使用されている
ChaCha20256 bits512 bitsソフトウェア処理が高速、TLS 1.3 で採用
3DES168 bits64 bitsレガシー、新規システムには非推奨

       注意: 可能な限り、機密性と真正性の両方を提供する AES-GCMAES-CCM などの 認証付き暗号モード を使用してください。

6.2 AES (Advanced Encryption Standard) の実装

const crypto = require('crypto');

// 暗号化関数
function encrypt(text, key) {
  // ランダムな初期化ベクトル (IV) を生成
  const iv = crypto.randomBytes(16);

  // AES-256-CBC でサイファーを作成
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');

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

// 復号関数
function decrypt(encryptedData, iv, key) {
  const decipher = crypto.createDecipheriv(
    'aes-256-cbc',
    key,
    Buffer.from(iv, 'hex')
  );

  let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

// 使用例
const key = crypto.scryptSync('secretPassword', 'salt', 32); // 256 bits
const message = 'これは秘密のメッセージです';

const { iv, encryptedData } = encrypt(message, key);
console.log('暗号化データ:', encryptedData);

const decrypted = decrypt(encryptedData, iv, key);
console.log('復号結果:', decrypted);

       警告: 同じ鍵に対して 初期化ベクトル (IV) を決して再利用しないでください。暗号化操作ごとに必ず新しいランダムな IV を生成してください。

7. 非対称鍵暗号(公開鍵暗号)

非対称鍵暗号 は、数学的に関連付けられた一対の鍵を使用します:

  • 公開鍵(Public Key): 公開可能で、暗号化に使用されます
  • 秘密鍵(Private Key): 秘密に保持する必要があり、復号に使用されます

7.1 非対称鍵暗号の主な用途

  • 安全な鍵交換(例:TLS/SSL ハンドシェイク)
  • 電子署名
  • メールの暗号化(PGP/GPG)
  • ブロックチェーンと暗号資産

7.2 ハイブリッド暗号方式

非対称鍵暗号は対称鍵暗号に比べて非常に低速です。そのため、大量のデータを暗号化する場合は以下の ハイブリッド方式 が一般的です:

  1. ランダムな対称鍵(セッション鍵)を生成する
  2. データを対称鍵(AES など)で暗号化する
  3. 受信者の 公開鍵 で対称鍵を暗号化する
  4. 暗号化されたデータと暗号化された鍵の両方を送信する

7.3 RSA (Rivest-Shamir-Adleman) の実装

const crypto = require('crypto');

// RSA 鍵ペアを生成
function generateKeyPair() {
  return crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048,
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
  });
}

// 公開鍵で暗号化
function encryptWithPublicKey(text, publicKey) {
  const buffer = Buffer.from(text, 'utf8');
  const encrypted = crypto.publicEncrypt(
    {
      key: publicKey,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
    },
    buffer
  );
  return encrypted.toString('base64');
}

// 秘密鍵で復号
function decryptWithPrivateKey(encryptedText, privateKey) {
  const buffer = Buffer.from(encryptedText, 'base64');
  const decrypted = crypto.privateDecrypt(
    {
      key: privateKey,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
    },
    buffer
  );
  return decrypted.toString('utf8');
}

const { publicKey, privateKey } = generateKeyPair();
const encrypted = encryptWithPublicKey('RSAで暗号化されたメッセージ', publicKey);
const decrypted = decryptWithPrivateKey(encrypted, privateKey);
console.log('復号メッセージ:', decrypted);

8. 電子署名

電子署名 は、メッセージ、ソフトウェア、またはデジタルドキュメントの真正性と完全性を検証する手段を提供します。

const crypto = require('crypto');

const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

// メッセージに署名する関数
function signMessage(message, privateKey) {
  const signer = crypto.createSign('sha256');
  signer.update(message);
  return signer.sign(privateKey, 'base64');
}

// 署名を検証する関数
function verifySignature(message, signature, publicKey) {
  const verifier = crypto.createVerify('sha256');
  verifier.update(message);
  return verifier.verify(publicKey, signature, 'base64');
}

const message = 'このメッセージに署名が必要です';
const signature = signMessage(message, privateKey);
console.log('署名:', signature.substring(0, 50) + '...');

const isValid = verifySignature(message, signature, publicKey);
console.log('署名は有効か:', isValid); // true

9. ランダムデータの生成

鍵、ソルト、初期化ベクトル (IV) を作成するために、セキュアなランダムデータを生成することは暗号技術において非常に重要です。

const crypto = require('crypto');

// ランダムなバイト列を生成 (16進数)
const randomBytes = crypto.randomBytes(16);
console.log('ランダムバイト:', randomBytes.toString('hex'));

// セキュアなランダム数値を生成 (min から max まで)
function secureRandomNumber(min, max) {
  const range = max - min + 1;
  const bytesNeeded = Math.ceil(Math.log2(range) / 8);
  const randomBytes = crypto.randomBytes(bytesNeeded);
  const randomValue = randomBytes.readUIntBE(0, bytesNeeded);
  
  return min + (randomValue % range);
}

console.log('ランダム数値:', secureRandomNumber(1, 100));

10. セキュリティ・ベストプラクティス

Crypto モジュールを使用する際は、以下の原則を守ってください:

  1. モダンなアルゴリズムを使用する: MD5 や SHA-1 などの古いアルゴリズムは避けてください。
  2. 鍵管理を厳重にする: 鍵は安全に保存し、定期的に更新し、決してハードコードしないでください。
  3. ランダムな IV を使用する: 暗号化ごとに必ず新しい IV を生成してください。
  4. 認証を追加する: 可能な限り GCM のような認証付き暗号モードを使用してください。
  5. 定数時間比較: セキュリティ上重要な値の比較には必ず crypto.timingSafeEqual() を使用してください。
  6. 鍵派生関数: パスワードベースの鍵には、scryptbcrypt、または PBKDF2 を使用してください。
  7. 最新の状態を維持: セキュリティ修正を受けるために Node.js を最新版に保ってください。
警告: 暗号技術は非常に複雑であり、小さなミスが深刻な脆弱性に繋がります。重要な機能を実装する際は、専門家のレビューを受けるか、実績のあるライブラリの使用を検討してください。

11. まとめ

Node.js の Crypto モジュールは、以下のような幅広い暗号化機能を提供します:

  • データの完全性を検証する ハッシュ関数
  • 認証と整合性をチェックする HMAC
  • 共有鍵でデータを守る 対称鍵暗号
  • セキュアな通信と電子署名を実現する 非対称鍵暗号
  • あらゆる暗号操作の基盤となる セキュアなランダムデータ生成

これらの概念を正しく理解し実装することで、機密データと通信を保護する堅牢なアプリケーションを構築できます。