# 生の IP アドレスを保存しない

> 訪問者の IP アドレスを保存せず、HMAC だけを残すしくみと理由を説明します。

goodnight-web は、訪問者の IP アドレスを保存もログ出力もしません。残すのは秘密の鍵を使ったハッシュ `HMAC-SHA256(ip, key)` だけで、回数制限や同じ送信者の確認にはこれで足ります。鍵は `APP_SECRET` から作ります。`IP_HASH_SECRET` を設定した場合はその値を使います。

## 何をするか

- 問い合わせが送信されるたびに、Worker は `ip_hash = HMAC-SHA256(ip, key)` を計算します。
- Worker は、[HKDF](https://goodnight-web.dev/ja/docs/glossary.md#hkdf) と IP アドレスのハッシュ専用の決まったラベルを使って、`APP_SECRET` から鍵を作ります。ログインセッションに署名する鍵は別のラベルで作るので、どちらの鍵からも、もう一方の鍵や `APP_SECRET` はわかりません。
- D1 に保存するのは `ip_hash` だけです。生の IP アドレスは D1 に書かず、ログにも出しません。
- Rate Limiting binding のキーにも、同じ方法で作った HMAC を使います。IPv6 アドレスでは、アドレス全体ではなく /64 のプレフィックスからハッシュを作ります。

## コードでは

問い合わせ API は、`cf-connecting-ip` ヘッダーから IP アドレスを読み、ハッシュだけを残します。生の IP アドレスを渡す先は、Turnstile の検証 API（siteverify）の `remoteip` だけです。

```ts title="packages/server/src/features/contact/contact-routes.ts"
const ip = c.req.header('cf-connecting-ip');
const ipHash = await hmacSha256Hex(ipHashSecret, ip ?? 'unknown');
```

`hmacSha256Hex()` は、Workers にある Web Crypto API を使います。

```ts title="packages/server/src/shared/crypto.ts"
export const hmacSha256Hex = async (
  secret: string,
  message: string,
): Promise<string> => {
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const signature = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(message),
  );
  return [...new Uint8Array(signature)]
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
};
```

回数制限のキーも同じ方法で作ります。違いは 1 つだけで、IPv6 アドレスでは先頭の 64 ビット（/64 のプレフィックス）だけから HMAC を作ります。IPv6 の契約者 1 人には、ふつう /64 がまるごと割り当てられます。アドレスごとに数えると、アドレスを変えるだけで上限に届かなくなるからです。IPv4 アドレスと、IPv6 の形で書かれた IPv4 アドレス（`::ffff:192.0.2.1`）は、そのまま数えます。セットアップと Passkey でのログインも同じキーを使います。接頭辞（`setup:` か `auth:`）を付けるので、それぞれ別に数えます。

```ts title="packages/server/src/features/contact/contact-routes.ts"
// 4. Rate limit per client: the IPv4 address, or the IPv6 /64 prefix,
//    hashed like ip_hash.
const { success } = await limiter.limit({
  key: await ipRateLimitKey(ipHashSecret, ip),
});
if (!success) return apiError(c, 429, 'rate_limited');
```

## なぜこう作るか

- **IP アドレスは個人情報です。** 個人を特定する手がかりになるので、保存すれば守るべき個人情報が増えます。
- **ハッシュでも用は足ります。** 同じ IP アドレスからは必ず同じ `ip_hash` ができます。回数制限も、複数の問い合わせが同じ送信者から来たことの確認もできます。
- **ただのハッシュでは足りません。** IP アドレスをそのまま SHA-256 にしただけでは、簡単に元に戻せます。IPv4 アドレスは約 43 億通りしかなく、すべて試せるからです。秘密の鍵を使う HMAC なら、鍵がなければ戻せません。鍵は Worker の中にとどまります。
- **`APP_SECRET` は変えないでください。** `IP_HASH_SECRET` をご自身で設定した場合は、その値も同じです。変えると新しいハッシュが古いハッシュと一致しなくなり、新しい問い合わせが以前の問い合わせと同じ送信者から来たことを確かめられなくなります。
