콘텐츠로 이동

배너를 통한 자동 로그인

프로모션 배너를 클릭하여 하이브 웹 로그인을 적용한 웹사이트에 자동 로그인하려면 게임 서버 측에서는 해당 웹사이트 URL로 전달되는 배너 파라미터를 복호화해야 합니다.

본 가이드에서는 프로모션 배너 파라미터(hivepromotion_p) 값을 복호화하는 방법을 안내합니다.

배너 파라미터 복호화란

배너 파라미터(hivepromotion_p)란 콘솔에서 설정한 자동 로그인 키의 의해 암호화된 값으로, 유저가 인게임에서 프로모션 배너를 클릭하여 웹사이트로 이동 시 해당 웹사이트 URL에 포함되어 GET 방식으로 전송됩니다 (예: https://your-website-url.com?hivepromotion_p={암호화된계정정보}).

이동된 웹사이트에서 자동 로그인 처리를 하기 위해서는 프로모션 배너 파라미터 hivepromotion_p 키 값을 복호화해야 합니다.

사전 준비

파라미터 복호화를 수행하기에 앞서 hivepromotion_p 키 값을 포함하는 URL이 전달되어야 합니다. 자세한 내용은 콘솔 가이드 > 프로모션 > 자동 로그인 키 관리에서 자동 로그인용 웹사이트 도메인 등록 방법을 참고하세요.

복호화 순서

프로모션 배너 파라미터 hivepromotion_p를 복호화하는 순서는 아래와 같습니다.

  1. Safe Base64 디코딩
    전달받은 문자열을 Safe Base64 방식으로 디코딩합니다.

  2. AES-256-CBC 복호화
    디코딩된 데이터를 AES-256-CBC 알고리즘으로 복호화합니다.

  3. Zero Padding 제거 복호화된 결과에서 Zero Padding을 제거합니다.

  4. Gzip 압축 해제
    Padding이 제거된 데이터를 Gzip 형식으로 압축 해제합니다.

  5. 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 응답 스펙은 반드시 관련 참고 문서를 준수하여 설계하세요.