跳轉至

通過横幅自動登入

要通過點擊推廣横幅自動登入已應用 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 安全 Base64 編碼方式提供
  • 密文採用 IV || CIPHERTEXT 格式(IV 在前)
  • 密鑰是 64 字元的十六進位字串(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 玩家 ID(點擊横幅的使用者識別符) String Y
player_token 安全驗證令牌(用於請求有效性確認) String Y
appid 遊戲應用程式的包/Bundle ID String Y
did 裝置 ID String Y
server_id 用於連接(或推薦)的遊戲伺服器識別符 String Y
game_language 遊戲中使用的語言代碼(例如 ko、en) String Y
device_type 裝置類型/型號資訊。可能不提供,可以為 null String 或 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 回應規範必須按照相關 參考文件 進行設計。