通過横幅自動登入¶
要通過點擊推廣横幅自動登入已應用 Hive Web Login 的網站,遊戲伺服器必須解密傳送給網站 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 演算法解密解碼後的資料。 -
零填充移除 從解密結果中移除零填充。
-
Gzip 解壓縮
以 Gzip 格式解壓縮移除填充後的資料。 -
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_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)
// 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
回應規範必須按照相關 參考文件 進行設計。