배너를 통한 자동 로그인¶
프로모션 배너를 클릭하여 하이브 웹 로그인을 적용한 웹사이트에 자동 로그인하려면 게임 서버 측에서는 해당 웹사이트 URL로 전달되는 배너 파라미터를 복호화해야 합니다.
본 가이드에서는 프로모션 배너 파라미터(hivepromotion_p
) 값을 복호화하는 방법을 안내합니다.
배너 파라미터 복호화란¶
배너 파라미터(hivepromotion_p
)란 콘솔에서 설정한 자동 로그인 키의 의해 암호화된 값으로, 유저가 인게임에서 프로모션 배너를 클릭하여 웹사이트로 이동 시 해당 웹사이트 URL에 포함되어 GET 방식으로 전송됩니다 (예: https://your-website-url.com?hivepromotion_p={암호화된계정정보}).
이동된 웹사이트에서 자동 로그인 처리를 하기 위해서는 프로모션 배너 파라미터 hivepromotion_p
키 값을 복호화해야 합니다.
사전 준비¶
파라미터 복호화를 수행하기에 앞서 hivepromotion_p
키 값을 포함하는 URL이 전달되어야 합니다. 자세한 내용은 콘솔 가이드 > 프로모션 > 자동 로그인 키 관리에서 자동 로그인용 웹사이트 도메인 등록 방법을 참고하세요.
복호화 순서¶
프로모션 배너 파라미터 hivepromotion_p
를 복호화하는 순서는 아래와 같습니다.
-
Safe Base64 디코딩
전달받은 문자열을 Safe Base64 방식으로 디코딩합니다. -
AES-256-CBC 복호화
디코딩된 데이터를 AES-256-CBC 알고리즘으로 복호화합니다. -
Zero Padding 제거 복호화된 결과에서 Zero Padding을 제거합니다.
-
Gzip 압축 해제
Padding이 제거된 데이터를 Gzip 형식으로 압축 해제합니다. -
JSON 변환
최종적으로 압축 해제된 문자열을 JSON 형식으로 변환합니다.
복호화 예시 코드¶
복호화 코드 실행 시 입력값에 해당하는 hivepromotion_p
파라미터의 전제 사항은 아래와 같습니다.
- URL-safe Base64로 인코딩되어 전달됨
- 암호문은
IV || CIPHERTEXT
(IV가 선두에 붙음) 형태 - 키는 64자 길이의 hex 문자열(32바이트)
<?php
function decodeSecure($encoded, $opensslKey = "")
{
$key = hex2bin($opensslKey);
$decoded = safeBase64Decode($encoded);
$ivLength = openssl_cipher_iv_length("AES-256-CBC");
$iv = substr($decoded, 0, $ivLength);
$encrypted = substr($decoded, $ivLength);
$decrypted = openssl_decrypt($encrypted, "AES-256-CBC", $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
return json_decode(gzuncompress(zeroUnpadding($decrypted)), true);
}
function safeBase64Decode(string $encoded): string
{
$base64 = str_replace(['-', '_'], ['+', '/'], $encoded);
$remainder = strlen($base64) % 4;
if ($remainder) {
$base64 .= str_repeat('=', 4 - $remainder);
}
return base64_decode($base64);
}
function zeroUnpadding(string $paddedData): string
{
$padChar = chr(0);
$padLength = 0;
$dataLength = strlen($paddedData);
for ($i = $dataLength - 1; $i >= 0; $i--) {
if ($paddedData[$i] === $padChar) {
$padLength++;
} else {
break;
}
}
return substr($paddedData, 0, $dataLength - $padLength);
}
// TEST
$sampleEncryptedData = "pn126XOrtRWEt8maRZtapHzAIHNWSdD45abmOkHQ4-wx4PqPRYjYNnhzHe_Mv5gqpXeNcrFgkvihRGo6fSN2ZSWyVGrocK2LxfYHtPJ8XRU5SZ_LDG0Mvquebusurpix0_iiOHn5bmMaxlSDeEVHTM5CoRQpPMDY8j9D44QJL9tw5R_2h-utzs244r0OcAJRkFyHggZDRnhC5rqUQgyRu1mEYVhXvmiX0wIjEpnPapkbmngEm2f-IPWIsdhunBXoCyf1OcpVutpGZ4ARZRbuhQ";
$sampleEncryptionKey = "5725f257285ac6a56e6ec94b0cac84d565e8d7dc8ea4828446b04e8d2d3f0e2d";
print_r(decodeSecure($sampleEncryptedData, $sampleEncryptionKey));
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.zip.Inflater;
public class Main {
public static void main(String[] args) throws Exception {
String sampleEncryptedData = "pn126XOrtRWEt8maRZtapHzAIHNWSdD45abmOkHQ4-wx4PqPRYjYNnhzHe_Mv5gqpXeNcrFgkvihRGo6fSN2ZSWyVGrocK2LxfYHtPJ8XRU5SZ_LDG0Mvquebusurpix0_iiOHn5bmMaxlSDeEVHTM5CoRQpPMDY8j9D44QJL9tw5R_2h-utzs244r0OcAJRkFyHggZDRnhC5rqUQgyRu1mEYVhXvmiX0wIjEpnPapkbmngEm2f-IPWIsdhunBXoCyf1OcpVutpGZ4ARZRbuhQ";
String sampleEncryptionKey = "5725f257285ac6a56e6ec94b0cac84d565e8d7dc8ea4828446b04e8d2d3f0e2d";
String json = decodeSecure(sampleEncryptedData, sampleEncryptionKey);
System.out.println(json);
}
public static String decodeSecure(String encoded, String hexKey) throws Exception {
byte[] key = hexToBytes(hexKey);
byte[] decoded = safeBase64Decode(encoded);
int ivLength = 16; // AES block size
byte[] iv = Arrays.copyOfRange(decoded, 0, ivLength);
byte[] encrypted = Arrays.copyOfRange(decoded, ivLength, decoded.length);
byte[] decryptedNoPad = aes256CbcNoPadding(encrypted, key, iv);
byte[] unpadded = zeroUnpad(decryptedNoPad);
byte[] inflated = zlibInflate(unpadded);
return new String(inflated, StandardCharsets.UTF_8);
}
private static byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
private static byte[] safeBase64Decode(String input) {
String base64 = input.replace('-', '+').replace('_', '/');
int mod = base64.length() % 4;
if (mod != 0) base64 += "====".substring(mod);
return Base64.getDecoder().decode(base64);
}
private static byte[] aes256CbcNoPadding(byte[] ciphertext, byte[] key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(ciphertext);
}
private static byte[] zeroUnpad(byte[] data) {
int i = data.length - 1;
while (i >= 0 && data[i] == 0) i--;
return Arrays.copyOfRange(data, 0, i + 1);
}
// Uses zlib (RFC1950) format. Set nowrap=false in Inflater.
private static byte[] zlibInflate(byte[] data) throws Exception {
Inflater inflater = new Inflater(false);
inflater.setInput(data);
byte[] buffer = new byte[4096];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
while (!inflater.finished()) {
int count = inflater.inflate(buffer);
if (count == 0 && inflater.needsInput()) break;
if (count > 0) baos.write(buffer, 0, count);
}
} finally {
inflater.end();
}
return baos.toByteArray();
}
}
const crypto = require('crypto');
const zlib = require('zlib');
function safeBase64Decode(input) {
let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
const mod = base64.length % 4;
if (mod !== 0) base64 += '='.repeat(4 - mod);
return Buffer.from(base64, 'base64');
}
function zeroUnpad(buf) {
let end = buf.length;
while (end > 0 && buf[end - 1] === 0x00) end--;
return buf.subarray(0, end);
}
function decodeSecure(encoded, hexKey) {
const key = Buffer.from(hexKey, 'hex'); // 32 bytes
const decoded = safeBase64Decode(encoded);
const ivLength = 16; // AES block size
const iv = decoded.subarray(0, ivLength);
const encrypted = decoded.subarray(ivLength);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
decipher.setAutoPadding(false); // we will handle zero padding ourselves
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
const unpadded = zeroUnpad(decrypted);
// zlib inflate in Node.js
const inflated = zlib.inflateSync(unpadded);
return JSON.parse(inflated.toString('utf8'));
}
// TEST
const sampleEncryptedData = 'pn126XOrtRWEt8maRZtapHzAIHNWSdD45abmOkHQ4-wx4PqPRYjYNnhzHe_Mv5gqpXeNcrFgkvihRGo6fSN2ZSWyVGrocK2LxfYHtPJ8XRU5SZ_LDG0Mvquebusurpix0_iiOHn5bmMaxlSDeEVHTM5CoRQpPMDY8j9D44QJL9tw5R_2h-utzs244r0OcAJRkFyHggZDRnhC5rqUQgyRu1mEYVhXvmiX0wIjEpnPapkbmngEm2f-IPWIsdhunBXoCyf1OcpVutpGZ4ARZRbuhQ';
const sampleEncryptionKey = '5725f257285ac6a56e6ec94b0cac84d565e8d7dc8ea4828446b04e8d2d3f0e2d';
try {
const json = decodeSecure(sampleEncryptedData, sampleEncryptionKey);
console.log(json);
} catch (e) {
console.error('Decrypt error:', e);
}
복호화 결과 예시 및 필드 설명¶
복호화 결과 JSON 객체 예시는 아래와 같습니다.
{
"player_id": "20000033086",
"player_token": "bbaeb710f7e5f54645469f44cd651b",
"appid": "com.com2us.hivesdk.normal.freefull.apple.global.ios.universal",
"did": "104079054",
"server_id": "server_002",
"game_language": "ko",
"device_type": null,
"is_webview": 0,
"os": "I"
}
복호화 결과 JSON 객체를 구성하는 각 필드의 상세 사항은 아래와 같습니다.
필드명 | 설명 | 타입 | 필수 여부 |
---|---|---|---|
player_id | HIVE Player ID (배너를 클릭한 사용자 식별자) | String | Y |
player_token | 보안 검증용 토큰 (요청 유효성 확인에 사용) | String | Y |
appid | 게임 앱의 패키지/번들 ID | String | Y |
did | 단말(디바이스) ID | String | Y |
server_id | 접속(또는 추천) 대상 게임 서버 식별자 | String | Y |
game_language | 게임 내 사용 언어 코드 (예: ko, en) | String | Y |
device_type | 단말 유형/모델 정보. 제공되지 않을 수 있으며 null일 수 있음 | String or null | N |
is_webview | WebView 호출 여부 (0: 아님, 1: WebView) | Integer | N |
os | 운영체제 코드 (I: iOS, A: Android 등) | String | N |
복호화 결과를 활용한 로그인 처리¶
응답값에 해당하는 복호화 결과 데이터는 로그인(또는 자동 로그인)을 보다 안전하게 처리하도록 활용해볼 수 있습니다.
hivepromotion_p
복호화 결과 데이터를 로그인(또는 자동 로그인)에 활용하는 순서는 아래와 같습니다.
1. 기본 자체 검증(선택)¶
- 필수 필드 존재 여부 확인:
player_id
,player_token
,appid
,did
,server_id
- 앱 식별자 교차 검증: 복호화된
appid
가 현재 서비스 중인 앱(또는 허용된 앱 목록)과 일치하는지 확인 - 서버/환경 검증(선택):
server_id
가 허용된 서버 목록에 속하는지 확인
2. 토큰 검증(필수)¶
- 복호화 값의
player_token
으로 HIVE 인증 API를 호출하여 토큰 유효성을 반드시 확인합니다. - 참고 문서: Auth v4 VerifyToken
- 검증 시 확인 권장 항목:
- 토큰 유효성(만료/위변조 여부)
- 응답 내 식별 정보
player_id
가 복호화된 값player_id
와 일치 여부 - 필요 시 채널/플랫폼 등의 부가 정보 일치 여부
3. 로그인/세션 처리¶
- 검증 성공 시
- 자체적으로 로그인 처리를 수행한다.
- 검증 실패 시
- 자체적으로 로그인 실패 처리를 수행한다.
4. 예시 흐름(의사 코드)¶
try {
data = decrypt(hivepromotion_p)
// 자체 검증
assert has(data.player_id, data.player_token, data.appid)
assert isAllowedApp(data.appid)
// 토큰 검증 (필수)
verify = callAuthV4VerifyToken(player_token = data.player_token)
assert verify.isValid
assert verify.player_id == data.player_id
// 세션 발급
session = issueSession(verify.player_id)
return success(session)
} catch (e) {
return error(401 or 403)
}
Warning
player_token
은 민감 정보애 해당되므로 로그에 남지 않도록 주의하세요.- 네트워크 오류 대비 재시도 정책과 타임아웃을 설정하세요.
VerifyToken
응답 스펙은 반드시 관련 참고 문서를 준수하여 설계하세요.