bda.ts
1 import { base64 } from "@scure/base"; 2 import { cbc } from "@noble/ciphers/aes.js"; 3 import { md5 } from "@noble/hashes/legacy.js"; 4 import { 5 hexToBytes, 6 utf8ToBytes, 7 concatBytes, 8 bytesToUtf8, 9 } from "@noble/ciphers/utils.js"; 10 11 export interface EncryptedBDA { 12 iv: string; 13 ct: string; 14 s: string; 15 } 16 17 type BDAEntryValue = string | number | number[] | string[]; 18 19 export interface BDAEntry { 20 key: string; 21 value: BDAEntryValue | Array<BDAEntry>; 22 } 23 24 export class ExpiredBDAError extends Error { 25 public constructor() { 26 super("The BDA is expired, please try with a new one."); 27 this.name = "ExpiredBDAError"; 28 } 29 } 30 31 export class ArkoseBDA { 32 public timestamp = Math.floor(Date.now() / 1_000); 33 public key: Uint8Array; 34 35 public constructor(public readonly userAgent: string) { 36 this.timestamp = Math.floor(this.timestamp - (this.timestamp % 21_600)); 37 this.key = utf8ToBytes(`${this.userAgent}${this.timestamp}`); 38 } 39 40 public decrypt({ 41 iv, 42 ct: ciphertext, 43 s: salt, 44 }: EncryptedBDA): Array<BDAEntry> { 45 try { 46 // NOTE: we're preallocating the keychain to avoid resizing 47 const keychain: Array<Uint8Array> = new Array(4); 48 49 { 50 const saltedKey = concatBytes(this.key, hexToBytes(salt)); 51 keychain[0] = md5(saltedKey); 52 53 for (let i = 0; i < 3; i++) { 54 keychain[i + 1] = md5(concatBytes(keychain[i], saltedKey)); 55 } 56 } 57 58 const key = concatBytes(...keychain).slice(0, 32); 59 const decrypted = cbc(key, hexToBytes(iv)).decrypt( 60 base64.decode(ciphertext) 61 ); 62 63 return JSON.parse(bytesToUtf8(decrypted)); 64 } catch { 65 throw new ExpiredBDAError(); 66 } 67 } 68 69 public static decode(data: string): EncryptedBDA { 70 return JSON.parse(bytesToUtf8(base64.decode(data.trim()))); 71 } 72 73 public static retrieve(payload: string): URLSearchParams { 74 // we're reading the raw message of an http request. 75 if (payload.startsWith("POST /fc/gt2/public_key/")) { 76 // body of the request 77 payload = payload.split("\n\n")[1]; 78 } 79 80 return new URLSearchParams(payload); 81 } 82 83 public static getEnhancedFingerprint(bda: Array<BDAEntry>) { 84 const { value } = bda.find((entry) => entry.key === "enhanced_fp")!; 85 return value as Array<BDAEntry>; 86 } 87 88 public static toObjectProperties(value: Array<BDAEntry>) { 89 type EntryValueProperty = BDAEntryValue | Record<string, BDAEntryValue>; 90 91 if (Array.isArray(value)) { 92 // we can't check if the value is an array of BDAEntry 93 // when the array is empty... 94 if (value.length === 0) { 95 return value; 96 } 97 98 // we should check if the value is an array of BDAEntry 99 if ( 100 !value.every( 101 (item) => typeof item === "object" && "key" in item && "value" in item 102 ) 103 ) { 104 return value; 105 } 106 } 107 108 return value.reduce((acc, entry) => { 109 if (Array.isArray(entry.value)) { 110 acc[entry.key] = this.toObjectProperties( 111 entry.value as Array<BDAEntry> 112 ) as Record<string, BDAEntryValue>; 113 return acc; 114 } 115 116 acc[entry.key] = entry.value; 117 return acc; 118 }, {} as Record<string, EntryValueProperty>); 119 } 120 }