# 회원 연동 서명 규격

> 회원 연동 user_data 문자열의 필드, 서명(hash) 만드는 순서, 시간 연동 검사, 검증용 고정 값이에요.

회원 연동의 `user_data`는 내 서버가 회원 정보에 채팅방 토큰으로 서명한 문자열이에요. 이 문서는 필드와 서명 순서, 서버가 검사하는 규칙의 정본이에요. 설치 흐름과 언어별 예제는 [회원 연동](/doc/2.1)을 보세요.

## 필드 {#fields}

아래 필드 중 **값이 있는 것만** 서명에 들어가요. 서명 순서는 필드 이름순이라 넣는 순서는 상관없어요. 서명 코드는 넣은 필드를 모두 서명하므로 이 표에 없는 필드를 넣으면 오류 202가 나요. 나중에 필드가 늘어나도 새 필드를 넣지 않는 사이트의 서명은 그대로예요.

| 필드 | 형식 | 설명 |
| --- | --- | --- |
| `auth` | `0`~`3` 또는 `guest`, `member`, `subadmin`, `admin` | 권한. `0` 손님, `1` 회원, `2` 부관리자, `3` 관리자. 비우면 `id`가 있을 때 회원, 없을 때 손님 |
| `icons` | 주소 목록 | 닉네임 옆 아이콘. 쉼표로 여러 개, `@` 앞은 닉네임 앞·뒤는 닉네임 뒤. 예: `/img/a.png,/img/b.png@/img/c.png` |
| `id` | 문자열 | 회원 아이디. 차단·채팅금지·권한 부여의 기준 |
| `level` | 숫자 | 레벨. 채팅방 기능 조건의 레벨 비교에 쓰여요 |
| `nick` | 문자열 | 닉네임. 서버가 15자로 자른 뒤 공백을 지워요 |
| `nickcon` | 주소 | 닉네임 대신 보일 이미지 |
| `other` | 문자열 | 추가 표시. 낱말은 공백 하나로 구분해요. 보통 비워 둬요 |
| `room` | 채팅방 아이디 | 필수. `<u-chat room>` 값과 같아야 해요 |
| `time` | Unix 시각(초) | 필수. 서명을 만든 현재 시각. 시간 연동을 끄면 검사하지 않지만 항상 넣어요 |

서명 키는 채팅방 토큰이에요. 토큰은 관리 페이지 **채팅방 정보 → 토큰**에서 확인하고, 결과 문자열에는 넣지 않아요. 서버 REST API의 API 키와 다른 값이에요. 토큰이 새면 누구든 관리자로 접속할 수 있으므로 서버 코드에만 두세요.

## 서명 순서 {#algorithm}

1. 값이 있는 필드만 골라요. 빈 값과 넣지 않은 필드는 빠지고, 토큰도 넣지 않아요.
2. 필드를 이름순(영문 소문자, 바이트 순서)으로 정렬해요.
3. 필드마다 `이름=값` 한 줄을 만들어요. 값은 UTF-8 바이트를 소문자 16진수로 쓴 것이에요.
4. `uchat-v2` 뒤에 3의 줄을 줄바꿈(`\n`) 하나씩으로 이어 붙여요.
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자)
```

- 값은 자바스크립트 `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 - 유챗 서버 현재 시각`을 계산해요.
- 저장된 기준이 없으면 이번 `offset`을 기준으로 저장해요(첫 접속).
- 기준이 있으면 `offset`이 기준보다 **300초 넘게 작을 때**(오래된 접속 정보) 오류 205로 거절해요. 미래 시각은 막지 않아요.
- 같은 `hash`는 채팅 서버가 처음 받은 뒤 300초까지만 받아요. 그 뒤에는 오류 205예요.
- 처음 받는 `hash`로 통과할 때만 기준을 이번 `offset`으로 바꿔요. 같은 `user_data`를 다시 써도 기준은 움직이지 않아요.
- 표준시간 고정을 켜면 기준을 0으로 둬요. `time`이 유챗 서버 시각보다 300초 넘게 과거면 거절해요. 내 서버 시계가 정확할 때 쓰세요.
- 내 서버를 옮기거나 시계를 뒤로 300초 넘게 바꿨다면 관리 페이지에서 시간 기준을 초기화해요.
- 시간 연동을 켠 채팅방에서 `time`이 숫자가 아니면 오류 101이에요.

`user_data`를 페이지에 캐시해 두면 5분 뒤부터 오류 205가 날 수 있어요. 회원 연동 페이지는 캐시하지 않거나 `user_data_url`([u-chat 속성](/doc/3.1))로 그때그때 받으세요. 오류별 해결은 [회원 연동](/doc/2.1#troubleshooting)을 보세요.
