# 会員連携の署名仕様

> 会員連携のuser_data文字列のフィールド、署名(hash)の作り方、時刻連携のチェック、検証用の固定値です。

会員連携の `user_data` は、自分のサーバーが会員情報にチャットルームのトークンで署名した文字列です。このページは、フィールド、署名の手順、サーバーがチェックするルールの正式な仕様です。導入の流れと言語別のサンプルは[会員連携](/ja/doc/2.1)をご覧ください。

## フィールド {#fields}

次のフィールドのうち**値のあるものだけ**が署名に入ります。署名はフィールド名の順番で行うので、入れる順番は関係ありません。署名コードは入れたフィールドをすべて署名するため、この表にないフィールドを入れるとエラー202になります。今後フィールドが増えても、新しいフィールドを入れないサイトの署名は変わりません。

| フィールド | 形式 | 説明 |
| --- | --- | --- |
| `auth` | `0`〜`3` または `guest`、`member`、`subadmin`、`admin` | 権限。`0` ゲスト、`1` 会員、`2` モデレーター、`3` 管理者。空にすると、`id` があれば会員、なければゲスト |
| `icons` | URLのリスト | ニックネームの横のアイコン。カンマで複数指定でき、`@` より前はニックネームの前、後ろはニックネームの後ろに表示されます。例: `/img/a.png,/img/b.png@/img/c.png` |
| `id` | 文字列 | 会員ID。BAN・ミュート・権限付与の基準になります |
| `level` | 数値 | レベル。チャットルームの機能の条件でレベルを比較するときに使われます |
| `nick` | 文字列 | ニックネーム。サーバーが15文字で切ったあと、空白を取り除きます |
| `nickcon` | URL | ニックネームの代わりに表示する画像 |
| `other` | 文字列 | 追加表示。単語は半角スペース1つで区切ります。通常は空にしておきます |
| `room` | チャットルームID | 必須。`<u-chat room>` の値と同じである必要があります |
| `time` | Unix時刻(秒) | 必須。署名を作成した現在時刻。時刻連携をオフにするとチェックされませんが、常に入れてください |

署名の鍵はチャットルームのトークンです。トークンは管理ページの**チャットルーム情報 → トークン**で確認でき、結果の文字列には含めません。サーバーREST APIのAPIキーとは別の値です。トークンが漏れると誰でも管理者として入室できるため、サーバーのコードにだけ置いてください。

## 署名の手順 {#algorithm}

1. 値のあるフィールドだけを選びます。空の値と入れていないフィールドは除き、トークンも入れません。
2. フィールドを名前順(英小文字、バイト順)に並べます。
3. フィールドごとに `名前=値` の1行を作ります。値はUTF-8のバイト列を小文字の16進数で書いたものです。
4. `uchat-v2` のあとに、3の行を改行(`\n`)1つずつでつなげます。
5. 4の文字列について、チャットルームのトークンを鍵としてHMAC-SHA256を計算します。小文字16進数64文字の前に `v2.` を付けたものが `hash` です。
6. 同じ順番で `名前 + 空白 + encodeURIComponent(値)` を `|` でつなぎ、最後に `hash 値` を付けます。

結果の形:

```text
icons ...|id ...|level ...|nick ...|room my-room|time 1700000000|hash v2.0123...(64文字)
```

- 値はJavaScriptの `encodeURIComponent` と同じようにエンコードします。空白は `%20` です。PHPでは `rawurlencode` を使います。`rawurlencode` のように `~`、`*`、`!` などを余分にエンコードしても、デコードすれば同じ値なのでhashは同じです。
- チャット画面は各値を `decodeURIComponent` でデコードしてサーバーに送り、サーバーは同じ順番でhashを再計算して比較します。`hash` の16進数は大文字・小文字を区別しません。
- HMAC-SHA256と16進数変換は、ほとんどの言語の標準ライブラリにあります(PHP 5.6以上の `hash_hmac`・`bin2hex`、Node.jsの `crypto`、Pythonの `hmac`・`hashlib`、Javaの `javax.crypto.Mac`、C#の `HMACSHA256`、Goの `crypto/hmac`、Rubyの `OpenSSL::HMAC`)。

参考実装(Node.js):

```js
const crypto = require('node:crypto');

function signUserData(user, token) {
	const fields = Object.entries({ ...user, time: Math.floor(Date.now() / 1000) })
		.filter(([, v]) => v !== undefined && v !== null && v !== '')
		.map(([k, v]) => [k, String(v)])
		.sort(([a], [b]) => (a < b ? -1 : 1));
	const message = ['uchat-v2', ...fields.map(([k, v]) => k + '=' + Buffer.from(v, 'utf8').toString('hex'))].join('\n');
	const hash = crypto.createHmac('sha256', token).update(message, 'utf8').digest('hex');
	return fields
		.map(([k, v]) => k + ' ' + encodeURIComponent(v))
		.concat('hash v2.' + hash)
		.join('|');
}

// signUserData({ room: 'my-room', nick: '홍길동', id: 'user1', level: 5 }, process.env.UCHAT_TOKEN)
```

## 検証用の固定値 {#test-vector}

自分で書いたコードが正しいかどうか、次の入力で確認してください。同じ結果になるはずです。

| 入力 | 値 |
| --- | --- |
| トークン | `test-token-1234` |
| `time` | `1700000000` |
| `room` | `my-room` |
| `nick` | `홍 길동~*!` |
| `id` | `user@example.com` |
| `level` | `5` |
| `auth` | (空の値) |
| `icons` | `/img/a.png,/img/b.png@/img/c.png` |

HMACに渡す文字列(手順4。空の値の `auth` は行がありません):

```text
uchat-v2
icons=2f696d672f612e706e672c2f696d672f622e706e67402f696d672f632e706e67
id=75736572406578616d706c652e636f6d
level=35
nick=ed998d20eab8b8eb8f997e2a21
room=6d792d726f6f6d
time=31373030303030303030
```

結果の `user_data`:

```text
icons %2Fimg%2Fa.png%2C%2Fimg%2Fb.png%40%2Fimg%2Fc.png|id user%40example.com|level 5|nick %ED%99%8D%20%EA%B8%B8%EB%8F%99~*!|room my-room|time 1700000000|hash v2.80d95cf251e7aeef90b37547dbe95ef5336f6c7f8cb235a6cf6cee91031e2ae6
```

`auth` は空なので、`user_data` と署名の両方から省かれました。PHPの `rawurlencode` で作ると `~`、`*`、`!` が `%7E`、`%2A`、`%21` になり、文字列は変わりますが `hash` は同じで、そのまま動作します。

## サーバーのチェック {#checks}

会員連携をオンにしたチャットルームでは、入室時にチャットサーバーが次の順番でチェックします。

| チェック | 失敗時のエラー |
| --- | --- |
| `hash` があるか | 203 |
| hashが正しいか(上の手順で再計算) | 202 |
| 時刻連携がオンなら、`time` が基準より古くなく、同じ署名を最初に使ってから300秒が経っていないか | 205 |

署名には使い捨ての値(nonce)がありません。時刻連携をオフにすると、一度作った `user_data` をコピーしていつでも再利用できてしまうため、時刻連携はオンにしておいてください。

## 時刻連携 {#time-sync}

管理ページの会員連携画面で**時刻連携**をオンにしたときだけ `time` をチェックします。オフにすると `time` は署名に含まれるだけで、値はチェックされません。

- `offset = time - uchat サーバーの現在時刻` を計算します。
- 保存された基準がなければ、今回の `offset` を基準として保存します(初回接続)。
- 基準があり、`offset` が基準より **300秒を超えて小さい**場合(古い接続情報)は、エラー205で拒否します。未来の時刻はブロックしません。
- 同じ `hash` は、チャットサーバーが最初に受け取ってから300秒間だけ受け付けます。それ以降はエラー205です。
- 初めて受け取る `hash` で通過したときだけ、基準を今回の `offset` に更新します。同じ `user_data` を再利用しても基準は動きません。
- **標準時刻に固定**をオンにすると基準を0にします。`time` がuchatサーバーの時刻より300秒を超えて過去なら拒否します。自分のサーバーの時計が正確な場合に使ってください。
- 自分のサーバーを移転した場合や、時計を300秒を超えて巻き戻した場合は、管理ページで**時刻基準**を初期化します。
- 時刻連携をオンにしたチャットルームで `time` が数値でない場合はエラー101です。

`user_data` をページにキャッシュしておくと、5分後からエラー205が出ることがあります。会員連携のページはキャッシュしないか、`user_data_url`([u-chat の属性](/ja/doc/3.1))でその都度取得してください。エラー別の対処は[会員連携](/ja/doc/2.1#troubleshooting)をご覧ください。
