Appearance
セキュリティ・データ保護設計指針 (Security Guideline)
本仕様書は、「放課後リンク」プラットフォーム全体におけるセキュリティ設計、パスワードの堅牢化、監査ログの完全性検証チェーン、および児童・保護者の個人識別情報(PII: Personally Identifiable Information)の保護設計について、開発者が遵守すべき暗号実装および基準を定めたものです。
本書では、目標とするセキュリティ方針と、2026-08-18時点の実装済みの値を分けて記載します。実装済みの値が目標に達していない項目は、本番リリースの未完了ゲートです。
1. パスワードハッシュの堅牢化 (Password Hashing)
本システムは、不正アクセスや内部犯行によるパスワード漏洩リスクを低減するため、クライアント側ストレッチとAPI側のPBKDF2-HMAC-SHA512を組み合わせています。
1.1 アルゴリズムとパラメータ設定
新規ユーザー作成およびパスワード変更時の現行実装は次のとおりです。
- アルゴリズム: PBKDF2 (Password-Based Key Derivation Function 2)
- 疑似乱数生成関数 (PRF): HMAC-SHA512
- クライアント側ストレッチ: 250,000回。ソルトはログインIDを小文字化した値で、APIへ送信する128文字の16進文字列を生成します。
- API側の現行反復回数: 1,000回。16バイトの暗号論的乱数ソルトを使用します。
- 目標反復回数: 600,000回以上。これは開発方針上の目標であり、現行API実装は未達です。本番リリース前に引き上げて性能・タイムアウトを検証するか、方針変更を承認記録に残してください。
- ハッシュ出力長: 64バイト (512ビット)
- 保存フォーマット (v2形式):
$v2$[Rounds]$[Salt_Hex]$[Hash_Hex]
初期パスワードは8文字ちょうどで、大文字・小文字・数字・記号を各1文字以上含めます。APIは、クライアントでストレッチ済みの128文字16進値をプロビジョニング互換入力として受け付けます。
1.2 自動再ハッシュ (Auto-Rehash) ロジック
デモ用の $demo$ シードプレフィックスには対応しており、ログイン検証に成功すると現行の $v2$1000$... 形式へ再ハッシュします。現行コードの「最新」は1,000回であり、600,000回以上への自動再ハッシュはまだ実装されていません。
typescript
// ログイン処理時の疑似ロジック
async function verifyAndUpgradePassword(password: string, storedHashObj: StoredPassword) {
const { isValid, needsRehash } = await verifyPassword(password, storedHashObj.hash);
if (!isValid) throw new AuthenticationError();
// 現行実装では $v2$1000$... へ再ハッシュする
if (needsRehash) {
const newHash = await hashPassword(password);
await db.user.update({
where: { id: storedHashObj.userId },
data: { passwordHash: newHash }
});
}
}2. 監査ログ完全性チェーン (Audit Log Integrity Chain)
管理者による操作履歴の改ざんを検知するため、監査ログは施設単位のハッシュチェーンで接続されています。これは改ざん検知の仕組みであり、同時書き込み時の直列化や外部の改ざん耐性まで保証するものではありません。
2.1 ハッシュチェーン生成アルゴリズム
新たな監査ログレコードを挿入する際、同一施設の created_at が最も新しい監査ログの current_hash を previous_hash として取得し、次の値を結合してSHA-256でハッシュ化します。
$$\text{current_hash} = \text{SHA256}(\text{previous_hash} \parallel \text{user_id} \parallel \text{action} \parallel \text{target_table} \parallel \text{target_id} \parallel \text{old_value} \parallel \text{new_value})$$
typescript
async function writeAuditLog(db: PrismaClient, userId: number, params: AuditLogParams) {
// 1. 直前のログレコードを取得
const lastLog = await db.auditLog.findFirst({
where: { facility_id: params.facilityId },
orderBy: { created_at: 'desc' }
});
const previousHash = lastLog ? lastLog.current_hash : "genesis";
const oldValue = params.oldValue !== undefined ? JSON.stringify(params.oldValue) : '';
const newValue = params.newValue !== undefined ? JSON.stringify(params.newValue) : '';
// 2. 結合対象文字列の生成
const dataToHash = `${previousHash}|${userId}|${params.action}|${params.targetTable}|${params.targetId}|${oldValue}|${newValue}`;
// 3. SHA-256によるハッシュ化
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(dataToHash);
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
const currentHash = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// 4. DBへ保存
await db.auditLog.create({
data: {
facility_id: params.facilityId,
action: params.action,
user_id: userId,
target_table: params.targetTable,
target_id: params.targetId,
old_value: oldValue || null,
new_value: newValue || null,
previous_hash: previousHash,
current_hash: currentHash
}
});
}2.2 改ざん検知・完全性検証スクリプト
監査ログ全件の再計算は、app/api/scripts/verify-audit-log-chain.ts で実行できます。これはDBへ直接接続せず、Wrangler D1のJSON出力を入力にして検証するため、読み取り専用の運用検査として使用します。
- 検証手順:
AuditLogの必要列をD1からJSONへ出力する。npx tsx scripts/verify-audit-log-chain.ts --file audit-logs.jsonを実行する。- 後続レコードの
previous_hashと、src/utils/audit.tsの結合順序で再計算したcurrent_hashを確認する。 ok: falseまたはissueが出た場合は、対象施設・レコード・発生時刻を記録し、運用責任者へエスカレーションする。
bash
cd app/api
npx wrangler d1 execute houkago_link_d1 --remote --command "SELECT id,user_id,facility_id,action,target_table,target_id,old_value,new_value,created_at,previous_hash,current_hash FROM AuditLog ORDER BY facility_id,created_at,id" --json > audit-logs.json
npx tsx scripts/verify-audit-log-chain.ts --file audit-logs.jsonaudit-logs.json は検証後に削除し、リポジトリへコミットしない。出力にはPIIが含まれる可能性があるため、結果の共有時は ok、件数、issue種別だけを使用する。
3. PII (個人識別情報) のフィールド・ローカル保護設計
APIは、児童・保護者・職員の対象フィールドを ENCRYPTION_KEY で暗号化して保存します。Management と Kiosk は別のローカル保護方式を持つため、APIの暗号仕様と混同しないでください。
3.1 暗号仕様
- APIアルゴリズム: AES-256-GCM (認証付き暗号)。12バイトIVと認証タグを暗号文に付加します。
- API鍵の設定: base64形式の32バイト鍵を
ENCRYPTION_KEYSecret として設定します。 - 主なAPI暗号化対象: 児童の
child_id、氏名・カナ、医療メモ、服薬情報、職員名など。対象フィールドは各ルートの実装を正本とします。 - Management: 32バイトのローカルマスターキーをWindows DPAPI(CurrentUser)で保護し、ローカルデータをAESで暗号化します。
- Kiosk: WindowsではDPAPI、その他の対応OSではアプリ側のAES-HMAC方式を使用します。端末・OS別の詳細はKioskのコードと運用手順を確認してください。
4. API通信セキュリティ (CORS & JWT)
- CORS制限: API サーバーの CORS (Cross-Origin Resource Sharing) は、ワイルドカード(
*)を禁止し、本番環境のドメイン(https://guardian.amga.jpなど)のみをホワイトリスト登録します。 - JWT署名: 各種トークンは、256ビット以上の秘密鍵(
JWT_SECRET)で署名された HS256 JWT を使用し、有効期限(Expiration)は必要最小限の時間に設定してリプレイアタックを防ぎます。