1+ package com.github.libretube.api.local
2+
3+ import okio.ByteString.Companion.decodeBase64
4+ import okio.ByteString.Companion.toByteString
5+
6+ import kotlinx.serialization.json.Json
7+ import kotlinx.serialization.json.JsonNull
8+ import kotlinx.serialization.json.JsonObject
9+ import kotlinx.serialization.json.JsonPrimitive
10+ import kotlinx.serialization.json.jsonArray
11+ import kotlinx.serialization.json.jsonPrimitive
12+ import kotlinx.serialization.json.long
13+
14+ /* *
15+ * Parses the raw challenge data obtained from the Create endpoint and returns an object that can be
16+ * embedded in a JavaScript snippet.
17+ */
18+ fun parseChallengeData (rawChallengeData : String ): String {
19+ val scrambled = Json .parseToJsonElement(rawChallengeData).jsonArray
20+
21+ val challengeData = if (scrambled.size > 1 && scrambled[1 ].jsonPrimitive.isString) {
22+ val descrambled = descramble(scrambled[1 ].jsonPrimitive.content)
23+ Json .parseToJsonElement(descrambled).jsonArray
24+ } else {
25+ scrambled[1 ].jsonArray
26+ }
27+
28+ val messageId = challengeData[0 ].jsonPrimitive.content
29+ val interpreterHash = challengeData[3 ].jsonPrimitive.content
30+ val program = challengeData[4 ].jsonPrimitive.content
31+ val globalName = challengeData[5 ].jsonPrimitive.content
32+ val clientExperimentsStateBlob = challengeData[7 ].jsonPrimitive.content
33+
34+
35+ val privateDoNotAccessOrElseSafeScriptWrappedValue = challengeData[1 ]
36+ .takeIf { it !is JsonNull }
37+ ?.jsonArray
38+ ?.find { it.jsonPrimitive.isString }
39+
40+ val privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = challengeData[2 ]
41+ .takeIf { it !is JsonNull }
42+ ?.jsonArray
43+ ?.find { it.jsonPrimitive.isString }
44+
45+
46+ return Json .encodeToString(
47+ JsonObject .serializer(), JsonObject (
48+ mapOf (
49+ " messageId" to JsonPrimitive (messageId),
50+ " interpreterJavascript" to JsonObject (
51+ mapOf (
52+ " privateDoNotAccessOrElseSafeScriptWrappedValue" to (privateDoNotAccessOrElseSafeScriptWrappedValue
53+ ? : JsonPrimitive (" " )),
54+ " privateDoNotAccessOrElseTrustedResourceUrlWrappedValue" to (privateDoNotAccessOrElseTrustedResourceUrlWrappedValue
55+ ? : JsonPrimitive (" " ))
56+ )
57+ ),
58+ " interpreterHash" to JsonPrimitive (interpreterHash),
59+ " program" to JsonPrimitive (program),
60+ " globalName" to JsonPrimitive (globalName),
61+ " clientExperimentsStateBlob" to JsonPrimitive (clientExperimentsStateBlob)
62+ )
63+ )
64+ )
65+ }
66+
67+ /* *
68+ * Parses the raw integrity token data obtained from the GenerateIT endpoint to a JavaScript
69+ * `Uint8Array` that can be embedded directly in JavaScript code, and an [Int] representing the
70+ * duration of this token in seconds.
71+ */
72+ fun parseIntegrityTokenData (rawIntegrityTokenData : String ): Pair <String , Long > {
73+ val integrityTokenData = Json .parseToJsonElement(rawIntegrityTokenData).jsonArray
74+ return base64ToU8(integrityTokenData[0 ].jsonPrimitive.content) to integrityTokenData[1 ].jsonPrimitive.long
75+ }
76+
77+ /* *
78+ * Converts a string (usually the identifier used as input to `obtainPoToken`) to a JavaScript
79+ * `Uint8Array` that can be embedded directly in JavaScript code.
80+ */
81+ fun stringToU8 (identifier : String ): String {
82+ return newUint8Array(identifier.toByteArray())
83+ }
84+
85+ /* *
86+ * Takes a poToken encoded as a sequence of bytes represented as integers separated by commas
87+ * (e.g. "97,98,99" would be "abc"), which is the output of `Uint8Array::toString()` in JavaScript,
88+ * and converts it to the specific base64 representation for poTokens.
89+ */
90+ fun u8ToBase64 (poToken : String ): String {
91+ return poToken.split(" ," )
92+ .map { it.toUByte().toByte() }
93+ .toByteArray()
94+ .toByteString()
95+ .base64()
96+ .replace(" +" , " -" )
97+ .replace(" /" , " _" )
98+ }
99+
100+ /* *
101+ * Takes the scrambled challenge, decodes it from base64, adds 97 to each byte.
102+ */
103+ private fun descramble (scrambledChallenge : String ): String {
104+ return base64ToByteString(scrambledChallenge)
105+ .map { (it + 97 ).toByte() }
106+ .toByteArray()
107+ .decodeToString()
108+ }
109+
110+ /* *
111+ * Decodes a base64 string encoded in the specific base64 representation used by YouTube, and
112+ * returns a JavaScript `Uint8Array` that can be embedded directly in JavaScript code.
113+ */
114+ private fun base64ToU8 (base64 : String ): String {
115+ return newUint8Array(base64ToByteString(base64))
116+ }
117+
118+ private fun newUint8Array (contents : ByteArray ): String {
119+ return " new Uint8Array([" + contents.joinToString(separator = " ," ) { it.toUByte().toString() } + " ])"
120+ }
121+
122+ /* *
123+ * Decodes a base64 string encoded in the specific base64 representation used by YouTube.
124+ */
125+ private fun base64ToByteString (base64 : String ): ByteArray {
126+ val base64Mod = base64
127+ .replace(' -' , ' +' )
128+ .replace(' _' , ' /' )
129+ .replace(' .' , ' =' )
130+
131+ return (base64Mod.decodeBase64() ? : throw PoTokenException (" Cannot base64 decode" ))
132+ .toByteArray()
133+ }
0 commit comments