本文へ移動
🔑

共通鍵と公開鍵・秘密鍵の違いとは?暗号化とハッシュ化も図解

51分
もくじ

会社が情報流出を発表するとき、公表文に「パスワードは暗号化されています」「ハッシュ化されたパスワード情報」と書かれることがある。暗号化とハッシュ化は別の処理で、データが流出した後に何が守られるかも違う。

その違いは、鍵を見ると分かる。共通鍵、公開鍵と秘密鍵、パスワード、API キーを、図と短いコードで1つずつ確かめる。

鍵は、だれが持つかで種類が分かれる

鍵は、種類ごとに、だれが持ち、だれに配ってよいかが違う。データが流出した後に何が守られるかは、そのデータを元に戻す鍵がどこにあったかで決まる。

暗号化は、読める文を、鍵を知らない人には読めない文に変える処理だ。変えた後の文を暗号文と呼ぶ。復号は、暗号文を元の文に戻す処理だ。鍵は、暗号化と復号の計算に使う値である。

情報処理推進機構の「暗号技術 Q&A」は、文字を3つ後ろにずらす暗号を例に挙げている。「ABCDE」を暗号化すると「DEFGH」になる。この「3」が鍵だ。3を知っている人は、「DEFGH」を3つ前にずらして「ABCDE」に戻せる。

Web サービスでは、鍵と、鍵のように秘密にする値が、利用者の頭の中、利用者の端末、会社のサーバー、データベースに分かれて置かれている。

インターネット API キーを付けたリクエスト 利用者パスワードを覚えている 利用者のアプリやブラウザーサーバーの公開鍵を受け取る 会社のサーバー秘密鍵、共通鍵、API キーを持つ データベースパスワードから作った照合用の値暗号化した会員の情報 外部のサービスの API

サーバーは、インターネットにつながっていて、アプリや Web サイトから求められたデータを返すコンピューターだ。API(Application Programming Interface)は、サーバーのプログラムのうち、アプリや Web サイトからの求めを受け取って、データを返す部分だ。アプリがサーバーに送る求めを、リクエストと呼ぶ。

鍵だれが持つかだれに配ってよいか何に使うか
共通鍵暗号化する人と、復号する人その2者のほかには配らない同じ鍵で暗号化して、復号する
公開鍵だれでもだれに配ってもよい暗号化する。署名を確かめる
秘密鍵公開鍵と秘密鍵の組の持ち主だけだれにも配らない復号する。署名を作る
パスワード利用者本人だけ。サーバーには、パスワードから作った照合用の値だけを保存するだれにも教えないログインする人が本人かを確かめる
API キーAPI を呼び出すプログラムの持ち主利用者に配るコードには書かないどの利用者やプロジェクトからのリクエストかを見分ける

共通鍵: 同じ鍵で暗号化して復号する

暗号化するときと復号するときに同じ鍵を使う方式を、共通鍵暗号方式と呼ぶ。その鍵が共通鍵だ。情報処理推進機構の「暗号技術 Q&A」は、この方式では、暗号化する人と復号する人が同じ鍵を持ち、その鍵を他人に知られないことが重要だと説明している。

共通鍵暗号方式は、資料によっては「秘密鍵暗号方式」とも呼ばれる。この記事の「秘密鍵」は、公開鍵と組になる鍵だけを指す。

暗号文(共通鍵で暗号化した電話番号) 暗号文は見えるが、共通鍵が無いので読めない 同じ共通鍵で復号する 山田さん 通信を盗み見る第三者 佐藤さん
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";

// 暗号化する関数
//   key:  共通鍵
//   text: 暗号化したい文
function encrypt(key: Buffer, text: string) {
  // iv(initialization vector、初期化ベクトル)は、暗号化のたびに新しく作る値
  // 秘密にしなくてよく、暗号文と一緒に相手へ渡す
  const iv = randomBytes(12);

  // "aes-256-gcm" は、共通鍵を使う暗号の方式の名前
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const data = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);

  // tag(認証タグ)は、暗号文が書き換えられていないかを、復号するときに確かめるための値
  const tag = cipher.getAuthTag();

  return { iv, data, tag };
}

// 復号する関数
//   key: 共通鍵
//   box: encrypt が返した値
function decrypt(key: Buffer, box: { iv: Buffer; data: Buffer; tag: Buffer }) {
  const decipher = createDecipheriv("aes-256-gcm", key, box.iv);
  decipher.setAuthTag(box.tag);
  return Buffer.concat([decipher.update(box.data), decipher.final()]).toString("utf8");
}

// 共通鍵を作る。中身は、でたらめに選んだ 256 ビット(32 バイト)の値
const key = randomBytes(32);
console.log("共通鍵:", key.toString("hex"));

// 山田さんが、共通鍵で電話番号を暗号化する
const box = encrypt(key, "090-0000-0001");
console.log("暗号文:", box.data.toString("hex"));

// 佐藤さんが、同じ共通鍵で復号する
console.log("同じ鍵で復号:", decrypt(key, box));

// 第三者が、別の鍵で復号しようとする
try {
  const otherKey = randomBytes(32);
  console.log("別の鍵で復号:", decrypt(otherKey, box));
} catch {
  console.log("別の鍵で復号: 失敗した");
}
共通鍵: e44bd3723ea30002ec014aaacb83dfe94232f5decfeb0596107680956ecf98b3
暗号文: 3faf203c82d020d8d8e91c8461
同じ鍵で復号: 090-0000-0001
別の鍵で復号: 失敗した

共通鍵の実物は、でたらめな値を、0から9とaからfの文字で表した64文字である。コードは実行のたびに新しい共通鍵を作るので、共通鍵と暗号文の値は毎回変わる。暗号文を元の電話番号に戻せるのは、同じ共通鍵を使ったときだけだ。

共通鍵は、相手に渡す途中で盗まれると役に立たない

山田さんと佐藤さんが同じ共通鍵を持つには、どちらかが作った共通鍵を、もう一方に届ける必要がある。届ける途中の共通鍵を第三者がコピーすると、第三者も暗号文を復号できる。

共通鍵を送る 暗号文 送られる途中の共通鍵をコピーする コピーした共通鍵で、暗号文を復号できる 山田さん 通信を盗み見る第三者 佐藤さん

やり取りする相手が増えると、鍵の数も増える。山田さんが100人と別々に暗号文をやり取りするなら、相手ごとに違う共通鍵を100個持つことになる。

実際のところ: 方式で迷うことは無く、事故は鍵の置き場所で起きる

共通鍵の暗号の方式は、自分で選んで悩むものではない。暗号技術の安全性を評価する国のプロジェクトの CRYPTREC(Cryptography Research and Evaluation Committees)は、政府が調達のために参照する暗号のリストを公開している(最終更新は2026年3月30日)。このリストで、利用を推奨する共通鍵の暗号に載っているのは、AES(Advanced Encryption Standard)、Camellia、KCipher-2 の3つだけだ。上のコードの aes-256-gcm は、AES を使っている。古い方式の Triple DES は、推奨のリストから外れ、互換性を保つ目的でだけ使い続けることを認めるリストに移っている。

自分で考えた暗号の方式を使うのは、やってはいけない。Web アプリケーションの安全性についての資料を公開している団体の OWASP(Open Worldwide Application Security Project)は、暗号の使い方をまとめた資料の中で、独自の方式について「Don't do this.(やるな)」の一言で済ませている。

そして、強い方式を選んでも、それだけではデータを守れない。米国国立標準技術研究所の資料は、鍵の管理がまずいと、強い方式を使っていても、安全は簡単に損なわれると書いている。情報処理推進機構の「暗号鍵管理ガイドライン」も、方式が安全なだけでは不十分で、鍵も安全に管理されている必要があるとしている。鍵をソースコードに書いたり、暗号化したデータと同じ場所に置いたりすれば、AES を使っていても、データと一緒に鍵も持ち出されかねない。

公開鍵と秘密鍵: 配ってよい鍵と、手元に残す鍵

公開鍵暗号方式は、暗号化する鍵と復号する鍵が違う方式だ。2つの鍵は組になっていて、一方を公開鍵、もう一方を秘密鍵と呼ぶ。情報処理推進機構の「暗号技術 Q&A」は、この方式の特徴を、一方の鍵を公開しても、もう一方の鍵を計算できないことだと説明している。

だから、公開鍵はだれに配ってもよい。秘密鍵は、持ち主の手元だけに置き、だれにも配らない。

公開鍵で暗号化し、秘密鍵で復号する

佐藤さんに暗号文を送りたい山田さんは、佐藤さんの公開鍵で暗号化する。その暗号文を復号できるのは、組になる秘密鍵を持つ人だ。佐藤さんが秘密鍵をだれにも渡していなければ、復号できるのは佐藤さんだけになる。米国国立標準技術研究所の用語集は、秘密鍵を、持ち主が秘密にしておく鍵で、対応する公開鍵で暗号化されたメッセージの復号に使うものと説明している。

佐藤さんの公開鍵を送る 暗号文(佐藤さんの公開鍵で暗号化した電話番号) 公開鍵は、見られてもよい 公開鍵では、暗号文を復号できない 自分だけが持つ秘密鍵で復号する 山田さん 通信を盗み見る第三者 佐藤さん

共通鍵のときと違い、第三者に知られては困る鍵が、通信の上を一度も通らない。

import { generateKeyPairSync, privateDecrypt, publicEncrypt } from "node:crypto";

// 佐藤さんが、公開鍵と秘密鍵の組を作る
// "rsa" は、公開鍵と秘密鍵を使う暗号の方式の名前
const sato = generateKeyPairSync("rsa", { modulusLength: 2048 });

// 山田さんが、佐藤さんの公開鍵で電話番号を暗号化する
const encrypted = publicEncrypt(sato.publicKey, Buffer.from("090-0000-0001"));
console.log("暗号文の先頭:", encrypted.toString("hex").slice(0, 32));

// 佐藤さんが、自分の秘密鍵で復号する
const decrypted = privateDecrypt(sato.privateKey, encrypted);
console.log("佐藤さんの秘密鍵で復号:", decrypted.toString("utf8"));

// 第三者が、自分で作った別の秘密鍵で復号しようとする
try {
  const other = generateKeyPairSync("rsa", { modulusLength: 2048 });
  const result = privateDecrypt(other.privateKey, encrypted);
  console.log("別の秘密鍵で復号:", result.toString("utf8"));
} catch {
  console.log("別の秘密鍵で復号: 失敗した");
}
暗号文の先頭: 26e82537a12821c3836816c9c71e6b92
佐藤さんの秘密鍵で復号: 090-0000-0001
別の秘密鍵で復号: 失敗した

秘密鍵で署名し、公開鍵で確かめる

公開鍵と秘密鍵の組には、もう1つの使い方がある。秘密鍵を持つ人が、文に署名を付ける使い方だ。署名は、文と秘密鍵から計算した値である。米国国立標準技術研究所の用語集は、秘密鍵で署名を計算し、対応する公開鍵でその署名を検証できると説明している。

デジタル庁の資料は、電子署名を、情報の作成者を示す目的で行われ、改変が行われていないかを確認できるものと説明している。つまり、署名を確かめた人には、2つのことが分かる。その文に署名を付けたのは、公開鍵と組になる秘密鍵を持つ人である。そして、署名の後に文は書き換えられていない。ただし、その公開鍵が本当に相手のものかは、別の方法で確かめる必要がある。

文と署名 秘密鍵が無いので、合う署名を作れない サーバーの公開鍵で、署名を確かめる サーバー 書き換える第三者 受け取る人
import { generateKeyPairSync, sign, verify } from "node:crypto";

// サーバーが、公開鍵と秘密鍵の組を作る
// "ed25519" は、署名に使う方式の名前
const server = generateKeyPairSync("ed25519");

// 公開鍵は配ってよいので、中身を表示してみる
console.log(server.publicKey.export({ type: "spki", format: "pem" }));

// サーバーが、自分の秘密鍵で、文に署名を付ける
const message = "会員番号 1 番の人がログインした";
const signature = sign(null, Buffer.from(message), server.privateKey);
console.log("署名:", signature.toString("hex").slice(0, 32), "(先頭だけ)");

// 受け取った人が、サーバーの公開鍵で、署名を確かめる
console.log("元の文を確かめる:", verify(null, Buffer.from(message), server.publicKey, signature));

// 途中で、会員番号を 2 に書き換えた文を確かめる
const changed = "会員番号 2 番の人がログインした";
console.log(
  "書き換えた文を確かめる:",
  verify(null, Buffer.from(changed), server.publicKey, signature),
);
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAxZzfRr243XXrWhy/q74mGEVab/rG4yIkZ4P+eL55ACA=
-----END PUBLIC KEY-----

署名: bbdc00e3f8c8537f89f1797f13ef5237 (先頭だけ)
元の文を確かめる: true
書き換えた文を確かめる: false

最初の3行が公開鍵の実物だ。会員番号を1から2に書き換えた文は、署名と合わない。

ログインした利用者にサーバーが渡すデータにも、この署名が使われる。クロスドメイン認証の記事で扱った JWT(JSON Web Token)は、サーバーが署名を付けて利用者に渡すデータの形式である。

HTTPS は、2つの方式を組み合わせている

ブラウザーのアドレス欄が https:// で始まるページでは、ブラウザーとサーバーの間の通信が暗号化されている。この通信の方式を HTTPS(Hypertext Transfer Protocol Secure)と呼び、暗号化は TLS(Transport Layer Security)という仕組みが受け持つ。

情報処理推進機構の「TLS 暗号設定ガイドライン」によると、ブラウザーとサーバーは、暗号化した通信を始める前に、3つのことを順に行う。使う暗号の方式を決める。サーバー証明書でサーバーを確認する。その通信で使う鍵を共有する。サーバー証明書は、サイトの身分証明にあたるデータで、認証局という第三者の機関が発行する。公開鍵が本当に相手のものかを確かめる役目を、この証明書が受け持つ。

サーバーの確認には、公開鍵と秘密鍵を使う署名が使われる。鍵を共有した後の実際のデータは、共有した鍵を使い、共通鍵の方式で暗号化される。

サーバー証明書 共有した鍵で暗号化したリクエスト 共有した鍵で暗号化したデータ 証明書を確かめ、相手が本物のサーバーだと確認する その通信で使う鍵を、2者で共有する ブラウザー サーバー

「暗号技術 Q&A」は、電子メールを例に、2つの方式を組み合わせる理由を説明している。共通鍵の方式は暗号化と復号が速く、公開鍵の方式は遅い。そこで、メッセージは共通鍵で暗号化し、その共通鍵を公開鍵の方式で暗号化して相手に渡す。

パスワードの代わりに使われ始めたパスキーも、公開鍵と秘密鍵を使う。パスキーの規格を作っている団体の FIDO アライアンスは、パスキーを、公開鍵と秘密鍵の暗号を使う認証の方法と説明している。

実際のところ: 今の公開鍵の方式は、いつまでも使えるわけではない

公開鍵を知っても秘密鍵を計算できない、という性質は、いつまでも続くものではない。理由が2つある。

1つめは、鍵の長さだ。CRYPTREC は、政府のシステム向けに、どの長さの鍵をいつまで使ってよいかの基準を定めている。この基準は、新しく暗号化や署名をするときは、原則として、3072ビットの RSA にあたる強さ以上を選ぶべきだとしている。上のコードで使った2048ビットの RSA は、それより1段弱い。処理が短い時間で終わる場合や、既存のシステムを使い続ける必要がある場合に限り、より強い鍵へ移ることを前提に、2030年まで選ぶことが許されている。

2つめは、量子コンピューターだ。同じ基準は、大規模な量子コンピューターが使えるようになった場合、いま推奨されている公開鍵の方式のすべてにとって、理論的には大きな脅威になると注意している。ただし、それがいつになるかを予測することは困難だ、とも書いている(2022年6月の時点)。

2024年8月米国が、新しい方式の規格を公表した 2030年2048ビットの RSA が、条件つきで許される期限 2035年日本の政府機関が、新しい方式への移行を目指す年

備えは始まっている。米国国立標準技術研究所は、2024年8月13日に、量子コンピューターを持つ攻撃者に対しても安全だと考えられている新しい方式の規格を公表した。日本の政府も、2025年11月20日に、政府機関の暗号を、原則として2035年までに新しい方式へ移すことを目指すと公表した。

先の話でも、今のデータに関係がある。総務省の情報通信白書(令和8年版)は、今は解読できない暗号文を集めて保存しておき、量子コンピューターが発展した後で解読を試みる攻撃も想定されている、と書いている。何十年も秘密にしておきたいデータを今の方式で暗号化して送ると、攻撃者がその暗号文を集めて保存していた場合、将来、解読を試みられる対象になりうる。

僕のように Web サービスを作る側が、公開鍵の方式を自分で選ぶ場面は、ほとんど無い。使っている部品やサービスが新しい方式に対応したら、後回しにせずに更新する。当面できるのは、それである。

パスワード: サーバーには、元のパスワードを保存しない

パスワードは、利用者本人だけが知っている値だ。サーバーは、ログインのたびに利用者からパスワードを受け取る。ただし、受け取ったパスワードを、そのままデータベースに保存するべきではない。情報処理推進機構の「安全なウェブサイトの作り方」は、パスワードをサーバーの中で保管するときは、そのままの形ではなく、ソルト付きハッシュ値の形で保管することを勧めている。データベースの中身が流出しても、パスワードがすぐに悪用されないようにするためだ。

ハッシュ化は、ハッシュ関数という計算で、元の値から別の値(ハッシュ値)を作る処理である。ハッシュ関数は、どんな長さの入力からも、決まった長さの値を作る。暗号化と違い、ハッシュ化には、復号にあたる処理が無い。ハッシュ値を元の値に戻すための鍵も無い。

暗号化ハッシュ化
元の値に戻す処理ある。鍵を持つ人が復号する無い。元に戻すための鍵も無い
向いているデータ後で元の値を使うデータ(住所、電話番号など)同じかどうかだけ分かればよいデータ(パスワード)

サーバーは、パスワードを元に戻せなくても困らない。ログインのときに入力されたパスワードを同じ手順でハッシュ化し、保存してあるハッシュ値と同じかどうかを比べればよいからだ。

パスワード ソルトとハッシュ値を保存する パスワード 保存してあるソルトとハッシュ値 2つのハッシュ値が同じなら、ログインを認める ソルトを作り、パスワードをハッシュ化する ログインのとき 入力されたパスワードを、同じソルトでハッシュ化する 利用者 サーバー データベース

ソルトは、会員ごとに新しく作る、でたらめな値だ。サーバーは、パスワードとソルトを組み合わせてハッシュ化する。

import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";

// サーバーがデータベースに保存する値の形
type SavedPassword = {
  salt: Buffer; // ソルト
  hash: Buffer; // ハッシュ値
};

// 会員登録のとき: パスワードから、データベースに保存する値を作る
function register(password: string): SavedPassword {
  // ソルトは、会員ごとに新しく作る、でたらめな値
  const salt = randomBytes(16);

  // scrypt は、パスワードの保存のために、わざと計算に時間がかかるように作られた関数
  const hash = scryptSync(password, salt, 32);

  // 保存するのはソルトとハッシュ値だけ。パスワードそのものは保存しない
  return { salt, hash };
}

// ログインのとき: 入力されたパスワードが正しいかを調べる
function login(saved: SavedPassword, password: string) {
  // 入力されたパスワードを、保存してあるソルトを使って、登録のときと同じ手順でハッシュ化する
  const hash = scryptSync(password, saved.salt, 32);

  // 保存してあるハッシュ値と同じなら、正しいパスワードだと分かる
  // timingSafeEqual は、2つの値が同じかを調べる関数
  // 比べるのにかかる時間から値を推測されにくい方法で比べる
  return timingSafeEqual(saved.hash, hash);
}

// 山田さんと佐藤さんが、たまたま同じパスワードで会員登録する
const yamada = register("P@ssw0rd");
const sato = register("P@ssw0rd");

console.log("山田さんのソルト:", yamada.salt.toString("hex"));
console.log("山田さんのハッシュ値:", yamada.hash.toString("hex"));
console.log("佐藤さんのソルト:", sato.salt.toString("hex"));
console.log("佐藤さんのハッシュ値:", sato.hash.toString("hex"));

// 山田さんが、正しいパスワードと、違うパスワードでログインする
console.log("正しいパスワード:", login(yamada, "P@ssw0rd"));
console.log("違うパスワード:", login(yamada, "password"));
山田さんのソルト: 7bf7b0e24761671170863eb6689ae72e
山田さんのハッシュ値: b8e2176121161fd2722c32d0ba72e54b0ae4bf9858df269c509ad05f179bac2e
佐藤さんのソルト: 502919dab4e2833eb9a729c92f785feb
佐藤さんのハッシュ値: 35f78bb44147bf49927b419889ed9ebf05fc5ab552d641ce5b4103f4f6469349
正しいパスワード: true
違うパスワード: false

山田さんと佐藤さんのパスワードは同じ「P@ssw0rd」だが、ソルトが違うので、ハッシュ値も違う。データベースを見た人は、2人が同じパスワードを使っていることに気づけない。「安全なウェブサイトの作り方」は、ソルトを使う理由の1つに、これを挙げている。

「元に戻せない」は、絶対にできないという意味ではない

ハッシュ値は元の値に戻せない、という説明は、条件を省いている。

CRYPTREC は、2002年度の報告書の中で、暗号の安全性を2種類に分けている。1つは情報量的安全性で、どれだけの計算機の能力を費やしても解読できないことを指す。もう1つは計算量的安全性で、最良の解読の方法と最速の計算機を使っても、現実的には解読できないことを指す。報告書によると、情報量的安全性を満たすには、暗号化する文の量以上の鍵を毎回使う必要があり、現実的ではない。そのため、暗号の安全性は計算量的安全性で示される。

解読できない 解読できる 終わらない 終わる 暗号文が、第三者の手に渡った 計算機の能力に限りが無ければ、解読できるか 絶対にできない(情報量的安全性) 現実の計算機と時間で、計算が終わるか 事実上できない(計算量的安全性) 解読される

この分け方は暗号についてのものだが、ハッシュ関数の資料も「絶対にできない」とは書いていない。米国国立標準技術研究所の用語集は、ハッシュ値から元の入力を見つけることを、不可能とは書かず、計算量の面で実行できない(computationally infeasible)と定義している。「安全なウェブサイトの作り方」も、ハッシュ値から元の文字列を復元することは「一般的には困難」と書いている。どちらも、事実上できない、という側の書き方である。

僕は、この記事を書くまで、「絶対にできない」と「事実上できない」の区別がついていなかった。

事実上できない、というのは、試す候補の数が多すぎるという意味だ。SHA-256(Secure Hash Algorithm 256-bit)というハッシュ関数を例にする。SHA-256 のハッシュ値は256ビット、つまり0か1が256個並んだ値である。米国国立標準技術研究所の資料は、SHA-256 のハッシュ値から元の入力を見つけることへの、期待される強さを256ビットとしている。同じ資料の定義では、これは、作業の量にして2の256乗にあたる。2の256乗は、78桁の数である。

115792089237316195423570985008687907853269984665640564039457584007913129639936

米国航空宇宙局(NASA)は、銀河がすべて同じ大きさだと仮定した概算で、観測できる宇宙にある星の数を、1のあとに0が22個並ぶ数としている。こちらは23桁である。

10000000000000000000000

78桁と23桁で、桁の数が55個違う。観測できる宇宙の星を1個ずつ数え終わっても、候補の全体から見ると、まだ試し始めたばかりである。

候補が少ない値は、ハッシュ化しても割り出される

ところが、元の値の候補が少ないと、話が変わる。攻撃者は、ハッシュ値を元に戻す計算をしない。候補を順にハッシュ化して、流出したハッシュ値と同じになるものを探す。

候補「0000」 ハッシュ値(流出した値と違う) 候補「0001」 ハッシュ値(流出した値と違う) 候補「4827」 ハッシュ値(流出した値と同じ) 同じ手順を繰り返す 元の値は 4827 だと分かる 攻撃者 ハッシュ関数

数字4桁の暗証番号を、ソルトを使わずに SHA-256 でハッシュ化した場合で確かめる。

import { createHash } from "node:crypto";

// 文から、SHA-256 というハッシュ関数でハッシュ値を作る
function sha256(text: string) {
  return createHash("sha256").update(text).digest("hex");
}

// 流出したハッシュ値。元の値は、数字4桁の暗証番号
const leaked = sha256("4827");
console.log("流出したハッシュ値:", leaked);

// 攻撃者は、ハッシュ値を元に戻す計算はしない
// 0000 から 9999 までの候補を順にハッシュ化して、流出したハッシュ値と比べる
for (let i = 0; i < 10000; i++) {
  const candidate = String(i).padStart(4, "0");

  if (sha256(candidate) === leaked) {
    console.log("元の値:", candidate);
    console.log("試した回数:", i + 1);
    break;
  }
}
流出したハッシュ値: f16592d12000ffca0f1159286959f4c2470c82a7b48940020b1323a6d49abe27
元の値: 4827
試した回数: 4828

攻撃者は、4,828回試しただけで元の値を見つけている。ハッシュ関数は SHA-256 のままだ。弱いのは、元の値のほうである。

元の値候補の数
数字4桁(0000 から 9999)10,000
英字の小文字8文字208,827,064,576
でたらめに選んだ256ビットの値2の256乗(上の78桁の数)

人が決めるパスワードは、でたらめな256ビットの値より、ずっと候補が少ない。「安全なウェブサイトの作り方」は、弱いパスワードは、よく使われる言葉を順に試す攻撃(辞書攻撃)で元のパスワードを復元されると書いている。

ソルトと、わざと時間のかかる計算は、この攻撃への対策である。ソルトがあると、攻撃者は、あらかじめ計算しておいたハッシュ値の一覧を使い回せない。計算に時間がかかると、攻撃者が同じ時間で試せる候補の数が減る。計算を繰り返して時間をかける方法を、ストレッチングと呼ぶ。Node.js の公式の文書は、上のパスワードのコードで使った scrypt を、候補を順に試す攻撃を割に合わなくするために、計算に時間とメモリーがかかるように設計された関数だと説明している。

ただし、どちらの対策も、試すのにかかる時間を延ばすだけだ。「安全なウェブサイトの作り方」は、弱いパスワードや十分に長くないパスワードは、十分な時間をかければ復元されると書いている。

実際のところ: パスワードは、長くして、使い回さない

パスワードについては、昔よく言われたことのいくつかが、今は勧められていない。

昔よく言われたこと今の考え方
定期的に変える流出していなければ、変えなくてよい
大文字、数字、記号を必ず混ぜさせるサービスの側がこの決まりを課すことを、米国国立標準技術研究所の基準は禁じている
覚えられる長さにする長くして、覚えるのはパスワード管理ツールに任せる

総務省の「国民のためのサイバーセキュリティサイト」は、パスワードの定期的な変更は不要だと書いている。定期的に変えると、パスワードの作り方が決まった型になって簡単なものになることや、使い回しをするようになることのほうが問題になる、というのが理由だ。同じページは、パスワードを複数のサービスで使い回さないことと、パスワード管理ツールを使うことを勧めている。

米国国立標準技術研究所が米国の政府のシステム向けに定めた基準は、サービスを作る側に対して、さらに踏み込んでいる。パスワードの定期的な変更を利用者に求めてはならない。文字の種類を混ぜる決まりを課してはならない。パスワードだけで本人を確かめる場合は、最低15文字を求めなければならない。

ここは、資料どうしで食い違いがある。総務省のページは、数字や記号、大文字と小文字が混ざっていることが好ましい、とも書いている。この基準が禁じたのは、サービスの側が、混ぜることを利用者に強制することだ。どちらの資料も、ある程度の長さを求めている点は同じである。

サービスを作る側には、もう1つある。上で候補を試すコードに使った SHA-256 は、パスワードの保存には向かない。OWASP の資料は、SHA-256 のような速いハッシュ関数は、攻撃者が短い時間で大量の候補を試せるので、パスワードの保存に適さないと書いている。代わりに挙げているのは、Argon2id、bcrypt、PBKDF2 といった、わざと遅く作られた方式だ。パスワードのコードで使った scrypt も、同じ資料が選択肢に挙げている。

そして正直なところ、パスワードという仕組みそのものに限界がある。偽のサイトやメールで利用者をだまし、パスワードを入力させて盗む手口を、フィッシングと呼ぶ。利用者が偽のログイン画面に自分でパスワードを入力してしまえば、長さもハッシュ化も役に立たない。米国国立標準技術研究所の基準は、パスワードにはフィッシングへの耐性が無い、と明記している。個人情報保護委員会も、2026年10月7日の注意喚起で、対策の例に、フィッシングに耐性のあるものを含む多要素認証(複数の確認の方法を組み合わせる認証)の導入を挙げた。先に触れたパスキーについて、FIDO アライアンスは、パスワードと違ってフィッシングへの耐性があると説明している。

API キー: 利用者に配るコードに書くと取り出される

API キーは、暗号化や復号の計算に使う鍵ではない。API を呼び出すプログラムが、リクエストと一緒に送る文字列だ。API は、届いた API キーを手がかりに、どの利用者やプロジェクトからのリクエストかを見分け、利用の量を数えたり、制限したりする。

// 外部のサービスの API に、API キーを付けたリクエストを送る例
// (API キーをどこに書いて送るかは、サービスごとに決まっている)
await fetch("https://api.example.com/messages", {
  headers: { "X-API-Key": "demo-secret-key-12345" },
});

API は、送ってきたのが本来のプログラムなのか、API キーを知った別の人なのかを、API キーだけでは区別できない。だから、API キーを知った人は、本来のプログラムと同じようにリクエストを送れる。

スマートフォンのアプリと、ブラウザーで動くコードは、利用者の端末に届く。利用者は、自分の端末に届いたコードの中身を調べられる。セキュリティの事故の報告を受け付けている団体の JPCERT コーディネーションセンターは、2026年10月8日の注意喚起で、公開されているスマートフォンのアプリを解析して API キーを特定する手口を挙げている。

取り出した API キーを付けたリクエスト 会社のアプリからのリクエストと見分けられず、結果を返す アプリの中身を調べた人 外部のサービスの API

Google Cloud の公式の文書は、API キーを利用者の側で動くコードに含めないよう求めている。代わりに、利用者の側のコードは自社のサーバーにリクエストを送り、サーバーが API キーを付けて外部の API を呼び出す。

リクエスト(API キーは付いていない) API キーを付けたリクエスト 結果 結果 サーバーに置いてある API キーを付ける 利用者のアプリ 会社のサーバー 外部のサービスの API

この形では、API キーは会社のサーバーから出ない。API を作る人が確かめるほかの項目は、認証と認可の違いの記事に書いた。

実際のところ: API キーは、漏れる前提で扱う

正直なところ、API キーは、この記事の4つの中でいちばん素朴な仕組みだ。値を知った人なら使える文字列で、リクエストのたびに、相手の API へそのまま送る。秘密鍵は、署名を作るのに使うだけで、相手には送らない。そこが違う。

サーバーに置いても、漏れる道は残る。ソースコードを保管して共有するサービスの GitHub は、公式の文書で、API キーやパスワードをソースコードに書いたまま保管場所(リポジトリ)に登録すると、不正アクセスの標的になると書いている。JPCERT コーディネーションセンターの注意喚起にも、ほかのシステムへの侵入で盗んだ API キーを使う手口が載っている。

だから、漏らさない工夫に加えて、漏れたときの被害を小さくしておく。JPCERT コーディネーションセンターは、API に付けて送るトークンについて、対策を3つ挙げている。必要最小限の権限だけを与える。有効期限を設定し、長いあいだ有効なものを避ける。不要になったものや漏えいが疑われるものを、すぐ無効にできるようにする。Google Cloud の文書も、API キーに使い道の制限を付けることと、使っていない API キーを消すことを勧めている。

ここでのトークンは、API キーと同じように、リクエストに付けて送る値を指す。

流出のお知らせの「暗号化」をどう読むか

2026年に発表された公表文から、パスワードについての書き方を2つ挙げる。

発表した会社公表文の書き方
GMOリサーチ&AI(2026年10月5日の公表文)流出の対象となる項目に「パスワード(暗号化されたもの)」
さくらインターネット(2026年8月19日の第二報)「ハッシュ化されたパスワード情報」へのアクセスの可能性。注に「元のパスワードには復元が困難な状態にしたデータ」

公表文を読むときに確かめることは、3つある。

1つめは、暗号化なのか、ハッシュ化なのかだ。暗号化は、鍵を持つ人が元に戻せる。ハッシュ化には、復号にあたる処理が無い。

2つめは、暗号化の場合に、復号する鍵も第三者の手に渡ったかどうかだ。共通鍵のコードで確かめたとおり、暗号文は、鍵を持つ人なら元に戻せる。暗号文と鍵の両方が第三者の手に渡ったなら、暗号化はデータを守らない。

渡っていない 渡った 暗号化したデータが流出した 復号する鍵も、第三者の手に渡ったか 第三者は、暗号文を事実上読めない(暗号の方式と使い方が適切な場合) 第三者は、暗号文を復号して読める

個人情報の扱いを監督する国の機関である個人情報保護委員会も、鍵の管理を条件に入れている。個人データの漏えいには、会社が個人情報保護委員会に報告しなければならない場合がある。ただし、高度な暗号化などがされている場合は、報告を要しない。同委員会の Q&A(Q6-19)は、この場合に当たるために必要と解されることを2つ挙げている。第三者が読める状態にすることが困難になる暗号化などの措置がとられていること。そして、読める状態にするための手段が適切に管理されていること。後者に当たる場合の1つとして、暗号化した情報と復号鍵を分離し、復号鍵そのものの漏えいを防ぐ適切な措置をとっていることを挙げている。

3つめは、ハッシュ化の場合に、自分のパスワードが割り出されやすいものかどうかだ。短いパスワードや、よく使われる言葉のパスワードは、ハッシュ化されていても割り出される。GMOリサーチ&AI は、パスワードは暗号化されていると書いたうえで、同じパスワードをほかのサービスでも使っている場合は、そのサービスのパスワードを変えるよう会員に勧めている。公表文に「暗号化」「ハッシュ化」と書かれていても、利用者はこの勧めに従うほうがよい。

まとめ

共通鍵は、暗号化する人と復号する人が同じものを持つ鍵だ。公開鍵はだれに配ってもよく、秘密鍵は持ち主の手元だけに置く。パスワードは利用者だけが知っていて、サーバーにはソルト付きのハッシュ値だけを保存する。API キーは、利用者に配るコードに書かず、サーバーに置く。

方式は自分で考えず、評価されたものを使う。手間をかけるのは、鍵をどこに置き、漏れたときにどう止めるかのほうだ。

「元に戻せない」「解読できない」は、絶対にできないという意味ではなく、候補が多すぎて事実上できないという意味である。だから、鍵が一緒に盗まれた暗号文と、候補の少ないパスワードのハッシュ値は、守られない。

おわり😊

RELATED

つくることで、見える景色がある。