Web Crypto API完全ガイド2026 - 暗号化、署名、ハッシュ、鍵管理、実践パターン
Web Crypto API完全ガイド2026
Web Crypto APIは、ブラウザで暗号化処理を安全に実行できる標準APIです。本記事では、暗号化の基本から実践的な使い方まで解説します。
目次
- Web Crypto APIとは
- 基本概念
- ハッシュ化
- 対称鍵暗号(AES)
- 非対称鍵暗号(RSA)
- デジタル署名
- 鍵管理
- 実践パターン
- セキュリティベストプラクティス
Web Crypto APIとは
特徴と用途
/**
* Web Crypto API の特徴
*
* 1. ブラウザネイティブ
* - 追加ライブラリ不要
* - すべてのモダンブラウザでサポート
*
* 2. セキュア
* - 暗号化処理はネイティブコード
* - 鍵をメモリに安全に保持
*
* 3. 非同期API
* - Promise ベース
* - メインスレッドをブロックしない
*
* 4. 標準アルゴリズム
* - AES, RSA, ECDSA
* - SHA-256, SHA-384, SHA-512
* - HMAC, PBKDF2
*/
// Web Crypto API へのアクセス
const crypto = window.crypto
const subtle = crypto.subtle
サポート状況
// ブラウザサポートチェック
function checkCryptoSupport(): boolean {
return (
typeof window !== 'undefined' &&
'crypto' in window &&
'subtle' in window.crypto
)
}
if (!checkCryptoSupport()) {
throw new Error('Web Crypto API not supported')
}
基本概念
CryptoKey
// CryptoKey の型
interface CryptoKeyDetails {
type: 'secret' | 'private' | 'public'
extractable: boolean
algorithm: Algorithm
usages: KeyUsage[]
}
// KeyUsage の種類
type KeyUsage =
| 'encrypt' // 暗号化
| 'decrypt' // 復号
| 'sign' // 署名
| 'verify' // 検証
| 'deriveKey' // 鍵導出
| 'deriveBits' // ビット導出
| 'wrapKey' // 鍵ラップ
| 'unwrapKey' // 鍵アンラップ
データ型変換
// ユーティリティ関数
class CryptoUtils {
// 文字列 → ArrayBuffer
static stringToBuffer(str: string): ArrayBuffer {
return new TextEncoder().encode(str)
}
// ArrayBuffer → 文字列
static bufferToString(buffer: ArrayBuffer): string {
return new TextDecoder().decode(buffer)
}
// ArrayBuffer → Base64
static bufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
// Base64 → ArrayBuffer
static base64ToBuffer(base64: string): ArrayBuffer {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes.buffer
}
// ArrayBuffer → Hex
static bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
// Hex → ArrayBuffer
static hexToBuffer(hex: string): ArrayBuffer {
const bytes = new Uint8Array(hex.length / 2)
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
}
return bytes.buffer
}
}
ハッシュ化
SHA-256ハッシュ
// SHA-256 ハッシュ生成
async function sha256(data: string): Promise<string> {
const buffer = CryptoUtils.stringToBuffer(data)
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
return CryptoUtils.bufferToHex(hashBuffer)
}
// 使用例
const hash = await sha256('Hello, World!')
console.log(hash) // b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
// ファイルのハッシュ計算
async function hashFile(file: File): Promise<string> {
const buffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
return CryptoUtils.bufferToHex(hashBuffer)
}
その他のハッシュアルゴリズム
// 汎用ハッシュ関数
async function hash(
data: string,
algorithm: 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512'
): Promise<string> {
const buffer = CryptoUtils.stringToBuffer(data)
const hashBuffer = await crypto.subtle.digest(algorithm, buffer)
return CryptoUtils.bufferToHex(hashBuffer)
}
// 各アルゴリズムの使用例
const sha1 = await hash('data', 'SHA-1') // 160 bits
const sha256 = await hash('data', 'SHA-256') // 256 bits
const sha384 = await hash('data', 'SHA-384') // 384 bits
const sha512 = await hash('data', 'SHA-512') // 512 bits
HMAC(メッセージ認証コード)
// HMAC 鍵生成
async function generateHMACKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{
name: 'HMAC',
hash: 'SHA-256',
},
true, // extractable
['sign', 'verify']
)
}
// HMAC 署名生成
async function createHMAC(
key: CryptoKey,
data: string
): Promise<string> {
const buffer = CryptoUtils.stringToBuffer(data)
const signature = await crypto.subtle.sign('HMAC', key, buffer)
return CryptoUtils.bufferToBase64(signature)
}
// HMAC 検証
async function verifyHMAC(
key: CryptoKey,
data: string,
signature: string
): Promise<boolean> {
const dataBuffer = CryptoUtils.stringToBuffer(data)
const signatureBuffer = CryptoUtils.base64ToBuffer(signature)
return await crypto.subtle.verify(
'HMAC',
key,
signatureBuffer,
dataBuffer
)
}
対称鍵暗号(AES)
AES-GCM暗号化
// AES-GCM 鍵生成
async function generateAESKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256, // 128, 192, または 256
},
true, // extractable
['encrypt', 'decrypt']
)
}
// 暗号化
async function encryptAES(
key: CryptoKey,
plaintext: string
): Promise<{ ciphertext: string; iv: string }> {
// 初期化ベクトル(IV)生成
const iv = crypto.getRandomValues(new Uint8Array(12))
const buffer = CryptoUtils.stringToBuffer(plaintext)
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
buffer
)
return {
ciphertext: CryptoUtils.bufferToBase64(encrypted),
iv: CryptoUtils.bufferToBase64(iv),
}
}
// 復号
async function decryptAES(
key: CryptoKey,
ciphertext: string,
iv: string
): Promise<string> {
const ciphertextBuffer = CryptoUtils.base64ToBuffer(ciphertext)
const ivBuffer = CryptoUtils.base64ToBuffer(iv)
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivBuffer,
},
key,
ciphertextBuffer
)
return CryptoUtils.bufferToString(decrypted)
}
// 使用例
const key = await generateAESKey()
const { ciphertext, iv } = await encryptAES(key, 'Secret message')
const decrypted = await decryptAES(key, ciphertext, iv)
console.log(decrypted) // 'Secret message'
パスワードベースの暗号化(PBKDF2)
// パスワードから鍵を導出
async function deriveKeyFromPassword(
password: string,
salt: Uint8Array
): Promise<CryptoKey> {
// パスワードを CryptoKey に変換
const passwordBuffer = CryptoUtils.stringToBuffer(password)
const passwordKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
'PBKDF2',
false,
['deriveKey']
)
// PBKDF2 で鍵導出
return await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000, // 推奨値
hash: 'SHA-256',
},
passwordKey,
{
name: 'AES-GCM',
length: 256,
},
false, // extractable = false(セキュリティ向上)
['encrypt', 'decrypt']
)
}
// パスワードで暗号化
async function encryptWithPassword(
password: string,
plaintext: string
): Promise<{ ciphertext: string; iv: string; salt: string }> {
const salt = crypto.getRandomValues(new Uint8Array(16))
const key = await deriveKeyFromPassword(password, salt)
const iv = crypto.getRandomValues(new Uint8Array(12))
const buffer = CryptoUtils.stringToBuffer(plaintext)
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
buffer
)
return {
ciphertext: CryptoUtils.bufferToBase64(encrypted),
iv: CryptoUtils.bufferToBase64(iv),
salt: CryptoUtils.bufferToBase64(salt),
}
}
// パスワードで復号
async function decryptWithPassword(
password: string,
ciphertext: string,
iv: string,
salt: string
): Promise<string> {
const saltBuffer = CryptoUtils.base64ToBuffer(salt)
const key = await deriveKeyFromPassword(password, new Uint8Array(saltBuffer))
const ciphertextBuffer = CryptoUtils.base64ToBuffer(ciphertext)
const ivBuffer = CryptoUtils.base64ToBuffer(iv)
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: ivBuffer },
key,
ciphertextBuffer
)
return CryptoUtils.bufferToString(decrypted)
}
非対称鍵暗号(RSA)
RSA鍵ペア生成
// RSA 鍵ペア生成
async function generateRSAKeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048, // または 4096
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: 'SHA-256',
},
true, // extractable
['encrypt', 'decrypt']
)
}
// RSA 暗号化(公開鍵で暗号化)
async function encryptRSA(
publicKey: CryptoKey,
plaintext: string
): Promise<string> {
const buffer = CryptoUtils.stringToBuffer(plaintext)
const encrypted = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
buffer
)
return CryptoUtils.bufferToBase64(encrypted)
}
// RSA 復号(秘密鍵で復号)
async function decryptRSA(
privateKey: CryptoKey,
ciphertext: string
): Promise<string> {
const buffer = CryptoUtils.base64ToBuffer(ciphertext)
const decrypted = await crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
privateKey,
buffer
)
return CryptoUtils.bufferToString(decrypted)
}
// 使用例
const keyPair = await generateRSAKeyPair()
const encrypted = await encryptRSA(keyPair.publicKey, 'Secret message')
const decrypted = await decryptRSA(keyPair.privateKey, encrypted)
console.log(decrypted) // 'Secret message'
楕円曲線暗号(ECDH)
// ECDH 鍵ペア生成
async function generateECDHKeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256', // または 'P-384', 'P-521'
},
true,
['deriveKey', 'deriveBits']
)
}
// 共有秘密鍵の導出
async function deriveSharedSecret(
privateKey: CryptoKey,
publicKey: CryptoKey
): Promise<CryptoKey> {
return await crypto.subtle.deriveKey(
{
name: 'ECDH',
public: publicKey,
},
privateKey,
{
name: 'AES-GCM',
length: 256,
},
false,
['encrypt', 'decrypt']
)
}
// 使用例(Diffie-Hellman 鍵交換)
const aliceKeyPair = await generateECDHKeyPair()
const bobKeyPair = await generateECDHKeyPair()
// Alice と Bob がお互いの公開鍵から同じ共有秘密を導出
const aliceSharedSecret = await deriveSharedSecret(
aliceKeyPair.privateKey,
bobKeyPair.publicKey
)
const bobSharedSecret = await deriveSharedSecret(
bobKeyPair.privateKey,
aliceKeyPair.publicKey
)
// 同じ共有秘密を使って暗号化通信
デジタル署名
RSA署名
// RSA 署名用鍵ペア生成
async function generateRSASignKeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: 'RSA-PSS',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
true,
['sign', 'verify']
)
}
// 署名生成
async function signRSA(
privateKey: CryptoKey,
data: string
): Promise<string> {
const buffer = CryptoUtils.stringToBuffer(data)
const signature = await crypto.subtle.sign(
{
name: 'RSA-PSS',
saltLength: 32,
},
privateKey,
buffer
)
return CryptoUtils.bufferToBase64(signature)
}
// 署名検証
async function verifyRSA(
publicKey: CryptoKey,
data: string,
signature: string
): Promise<boolean> {
const dataBuffer = CryptoUtils.stringToBuffer(data)
const signatureBuffer = CryptoUtils.base64ToBuffer(signature)
return await crypto.subtle.verify(
{
name: 'RSA-PSS',
saltLength: 32,
},
publicKey,
signatureBuffer,
dataBuffer
)
}
ECDSA署名
// ECDSA 署名用鍵ペア生成
async function generateECDSAKeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256',
},
true,
['sign', 'verify']
)
}
// 署名生成
async function signECDSA(
privateKey: CryptoKey,
data: string
): Promise<string> {
const buffer = CryptoUtils.stringToBuffer(data)
const signature = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: 'SHA-256',
},
privateKey,
buffer
)
return CryptoUtils.bufferToBase64(signature)
}
// 署名検証
async function verifyECDSA(
publicKey: CryptoKey,
data: string,
signature: string
): Promise<boolean> {
const dataBuffer = CryptoUtils.stringToBuffer(data)
const signatureBuffer = CryptoUtils.base64ToBuffer(signature)
return await crypto.subtle.verify(
{
name: 'ECDSA',
hash: 'SHA-256',
},
publicKey,
signatureBuffer,
dataBuffer
)
}
鍵管理
鍵のエクスポート・インポート
// 鍵をエクスポート(JSON形式)
async function exportKey(key: CryptoKey): Promise<string> {
const exported = await crypto.subtle.exportKey(
key.type === 'secret' ? 'raw' : 'jwk',
key
)
if (exported instanceof ArrayBuffer) {
return CryptoUtils.bufferToBase64(exported)
} else {
return JSON.stringify(exported)
}
}
// 鍵をインポート
async function importAESKey(keyData: string): Promise<CryptoKey> {
const buffer = CryptoUtils.base64ToBuffer(keyData)
return await crypto.subtle.importKey(
'raw',
buffer,
{ name: 'AES-GCM' },
true,
['encrypt', 'decrypt']
)
}
async function importRSAKey(
jwk: JsonWebKey,
isPublic: boolean
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
'jwk',
jwk,
{
name: 'RSA-OAEP',
hash: 'SHA-256',
},
true,
isPublic ? ['encrypt'] : ['decrypt']
)
}
セキュアストレージ
// IndexedDB で鍵を保存
class KeyStore {
private dbName = 'crypto-keys'
private storeName = 'keys'
private db: IDBDatabase | null = null
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = () => {
const db = request.result
db.createObjectStore(this.storeName)
}
})
}
async saveKey(name: string, key: CryptoKey): Promise<void> {
if (!this.db) throw new Error('Database not initialized')
const exported = await exportKey(key)
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([this.storeName], 'readwrite')
const store = transaction.objectStore(this.storeName)
const request = store.put(exported, name)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve()
})
}
async loadKey(
name: string,
algorithm: string
): Promise<CryptoKey | null> {
if (!this.db) throw new Error('Database not initialized')
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([this.storeName], 'readonly')
const store = transaction.objectStore(this.storeName)
const request = store.get(name)
request.onerror = () => reject(request.error)
request.onsuccess = async () => {
if (!request.result) {
resolve(null)
return
}
const key = await importAESKey(request.result)
resolve(key)
}
})
}
}
実践パターン
セキュアなローカルストレージ
// 暗号化されたストレージ
class SecureStorage {
private key: CryptoKey | null = null
async init(password: string): Promise<void> {
const salt = crypto.getRandomValues(new Uint8Array(16))
this.key = await deriveKeyFromPassword(password, salt)
// salt を保存(平文で OK)
localStorage.setItem('salt', CryptoUtils.bufferToBase64(salt))
}
async set(key: string, value: string): Promise<void> {
if (!this.key) throw new Error('Not initialized')
const { ciphertext, iv } = await encryptAES(this.key, value)
localStorage.setItem(
key,
JSON.stringify({ ciphertext, iv })
)
}
async get(key: string): Promise<string | null> {
if (!this.key) throw new Error('Not initialized')
const data = localStorage.getItem(key)
if (!data) return null
const { ciphertext, iv } = JSON.parse(data)
return await decryptAES(this.key, ciphertext, iv)
}
}
// 使用例
const storage = new SecureStorage()
await storage.init('user-password')
await storage.set('secret', 'My secret data')
const secret = await storage.get('secret')
ファイル暗号化
// ファイル暗号化
async function encryptFile(
file: File,
password: string
): Promise<Blob> {
// パスワードから鍵を導出
const salt = crypto.getRandomValues(new Uint8Array(16))
const key = await deriveKeyFromPassword(password, salt)
// ファイルを読み込み
const fileBuffer = await file.arrayBuffer()
// 暗号化
const iv = crypto.getRandomValues(new Uint8Array(12))
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
fileBuffer
)
// salt + iv + ciphertext を結合
const result = new Uint8Array(
salt.length + iv.length + encrypted.byteLength
)
result.set(salt, 0)
result.set(iv, salt.length)
result.set(new Uint8Array(encrypted), salt.length + iv.length)
return new Blob([result])
}
// ファイル復号
async function decryptFile(
encryptedBlob: Blob,
password: string
): Promise<Blob> {
const buffer = await encryptedBlob.arrayBuffer()
const data = new Uint8Array(buffer)
// salt, iv, ciphertext を分離
const salt = data.slice(0, 16)
const iv = data.slice(16, 28)
const ciphertext = data.slice(28)
// 鍵を導出
const key = await deriveKeyFromPassword(password, salt)
// 復号
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
ciphertext
)
return new Blob([decrypted])
}
セキュリティベストプラクティス
安全な実装ガイドライン
/**
* セキュリティベストプラクティス
*
* 1. ランダム値生成
* ✅ crypto.getRandomValues() を使用
* ❌ Math.random() は使用しない
*
* 2. 鍵管理
* ✅ extractable=false で鍵をエクスポート不可に
* ✅ IndexedDB や SessionStorage で保存
* ❌ LocalStorage に平文で保存しない
*
* 3. アルゴリズム選択
* ✅ AES-256-GCM(対称鍵)
* ✅ RSA-2048 以上(非対称鍵)
* ✅ SHA-256 以上(ハッシュ)
* ❌ 古いアルゴリズム(MD5, SHA-1)
*
* 4. パスワードハッシュ
* ✅ PBKDF2 100,000回以上
* ✅ ランダムな salt 使用
* ❌ 単純な SHA-256 ハッシュ
*
* 5. HTTPS 必須
* ✅ 本番環境は必ず HTTPS
* ❌ HTTP では Web Crypto API 制限あり
*/
// 安全な実装例
class CryptoService {
// ✅ 安全なランダム値生成
generateRandomBytes(length: number): Uint8Array {
return crypto.getRandomValues(new Uint8Array(length))
}
// ✅ 安全な鍵生成(extractable=false)
async generateSecureKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
false, // extractable=false
['encrypt', 'decrypt']
)
}
// ✅ 安全なパスワードハッシュ
async hashPassword(password: string): Promise<{
hash: string
salt: string
}> {
const salt = this.generateRandomBytes(16)
const passwordBuffer = CryptoUtils.stringToBuffer(password)
const passwordKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
'PBKDF2',
false,
['deriveBits']
)
const hashBuffer = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256',
},
passwordKey,
256
)
return {
hash: CryptoUtils.bufferToBase64(hashBuffer),
salt: CryptoUtils.bufferToBase64(salt),
}
}
}
まとめ
Web Crypto APIは、ブラウザで暗号化処理を安全に実行できる強力なツールです。
主要ポイント:
- ネイティブAPI: ライブラリ不要、全ブラウザ対応
- 多様なアルゴリズム: AES, RSA, ECDSA, SHA-256
- 非同期処理: Promise ベースで使いやすい
- セキュア: ネイティブコードで高速・安全
- 標準準拠: W3C標準仕様
2026年のベストプラクティス:
- AES-256-GCM で対称鍵暗号化
- RSA-2048 以上で非対称鍵暗号化
- PBKDF2 100,000回以上でパスワードハッシュ
- extractable=false で鍵を保護
- HTTPS 環境で使用
Web Crypto APIを活用して、セキュアなWebアプリケーションを構築しましょう。