跳转至

通过横幅自动登录

要通过点击推广横幅自动登录已应用 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 响应规范必须按照相关 参考文档 进行设计。