コンテンツにスキップ

バナーを通じた自動ログイン

プロモーションバナーをクリックしてHive Web Loginが適用されたウェブサイトに自動ログインするには、ゲームサーバー側でウェブサイト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. ゼロパディング除去 復号化結果からゼロパディングを除去します。

  4. Gzip展開
    パディングが除去されたデータをGzip形式で展開します。

  5. JSON変換
    最終的に展開された文字列をJSON形式に変換します。

復号化サンプルコード

復号化コードを実行する際のhivepromotion_pパラメータ入力値の前提条件は以下の通りです:

  • URL-safe Base64でエンコードされて配信
  • 暗号文はIV || CIPHERTEXT形式(IVが先頭に付加)
  • キーは64文字の16進文字列(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_idplayer_tokenappiddidserver_id
  • アプリ識別子のクロス検証:復号化されたappidが現在サービス中のアプリ(または許可されたアプリリスト)と一致することを確認
  • サーバー/環境検証(オプション):server_idが許可されたサーバーリストに属するかを確認

2. トークン検証(必須)

  • 復号化値のplayer_tokenでHIVE認証APIを呼び出してトークンの有効性を検証します。
  • 参考文書:Auth v4 VerifyToken
  • 検証時に確認を推奨する項目:
    • トークンの有効性(期限切れ/改ざん状態)
    • レスポンス内の識別情報player_idが復号化されたplayer_idと一致するか
    • 必要に応じて、チャネル/プラットフォーム等の追加情報の整合性

3. ログイン/セッション処理

  • 検証成功時
    • 独自にログイン処理を実行します。
  • 検証失敗時
    • 独自にログイン失敗処理を実行します。

4. 例フロー(疑似コード)

try {
    data = decrypt(hivepromotion_p)

    // Self validation
    assert has(data.player_id, data.player_token, data.appid)
    assert isAllowedApp(data.appid)

    // Token validation (required)
    verify = callAuthV4VerifyToken(player_token = data.player_token)
    assert verify.isValid
    assert verify.player_id == data.player_id

    // Session issuance
    session = issueSession(verify.player_id)
    return success(session)
} catch (e) {
    return error(401 or 403)
}
Warning
  • player_tokenは機密情報に該当するため、ログに残らないよう注意してください。
  • ネットワークエラーに備えて再試行ポリシーとタイムアウトを設定してください。
  • VerifyTokenレスポンス仕様は、必ず関連参考文書に準拠して設計してください。
  • VerifyToken 응답 스펙은 반드시 관련 참고 문서를 준수하여 설계하세요.