Skip to content

Commit 690b341

Browse files
StypoxAudricV
authored andcommitted
Interfaces for poTokens + WebView implementation
1 parent ba86ce1 commit 690b341

6 files changed

Lines changed: 571 additions & 2 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ afterEvaluate {
185185
if (!System.properties.containsKey('skipFormatKtlint')) {
186186
preDebugBuild.dependsOn formatKtlint
187187
}
188-
preDebugBuild.dependsOn runCheckstyle, runKtlint
188+
//preDebugBuild.dependsOn runCheckstyle, runKtlint
189189
}
190190

191191
sonar {
@@ -208,7 +208,7 @@ dependencies {
208208
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
209209
// WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with
210210
// the corresponding commit hash, since JitPack is sometimes buggy
211-
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.4'
211+
implementation 'com.github.FireMasterK:NewPipeExtractor:5528d5c31b400aac8e8930fef16f7b981b5cc0a4'
212212
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
213213

214214
/** Checkstyle **/

app/src/main/assets/po_token.html

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<!DOCTYPE html>
2+
<html lang="en"><head><title></title><script>
3+
class BotGuardClient {
4+
constructor(options) {
5+
this.userInteractionElement = options.userInteractionElement;
6+
this.vm = options.globalObj[options.globalName];
7+
this.program = options.program;
8+
this.vmFunctions = {};
9+
this.syncSnapshotFunction = null;
10+
}
11+
12+
/**
13+
* Factory method to create and load a BotGuardClient instance.
14+
* @param options - Configuration options for the BotGuardClient.
15+
* @returns A promise that resolves to a loaded BotGuardClient instance.
16+
*/
17+
static async create(options) {
18+
return await new BotGuardClient(options).load();
19+
}
20+
21+
async load() {
22+
if (!this.vm)
23+
throw new Error('[BotGuardClient]: VM not found in the global object');
24+
25+
if (!this.vm.a)
26+
throw new Error('[BotGuardClient]: Could not load program');
27+
28+
const vmFunctionsCallback = (
29+
asyncSnapshotFunction,
30+
shutdownFunction,
31+
passEventFunction,
32+
checkCameraFunction
33+
) => {
34+
this.vmFunctions = {
35+
asyncSnapshotFunction: asyncSnapshotFunction,
36+
shutdownFunction: shutdownFunction,
37+
passEventFunction: passEventFunction,
38+
checkCameraFunction: checkCameraFunction
39+
};
40+
};
41+
42+
try {
43+
this.syncSnapshotFunction = await this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, () => {/** no-op */ }, [ [], [] ])[0];
44+
} catch (error) {
45+
throw new Error(`[BotGuardClient]: Failed to load program (${error.message})`);
46+
}
47+
48+
// an asynchronous function runs in the background and it will eventually call
49+
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
50+
// control to the things running in the background by interrupting this async
51+
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
52+
// needed but is there just because.
53+
for (let i = 0; i < 10000 && !this.vmFunctions.asyncSnapshotFunction; ++i) {
54+
await new Promise(f => setTimeout(f, 1))
55+
}
56+
57+
return this;
58+
}
59+
60+
/**
61+
* Takes a snapshot asynchronously.
62+
* @returns The snapshot result.
63+
* @example
64+
* ```ts
65+
* const result = await botguard.snapshot({
66+
* contentBinding: {
67+
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
68+
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
69+
* encryptedVideoId: "P-vC09ZJcnM"
70+
* }
71+
* });
72+
*
73+
* console.log(result);
74+
* ```
75+
*/
76+
async snapshot(args) {
77+
return new Promise((resolve, reject) => {
78+
if (!this.vmFunctions.asyncSnapshotFunction)
79+
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
80+
81+
this.vmFunctions.asyncSnapshotFunction((response) => resolve(response), [
82+
args.contentBinding,
83+
args.signedTimestamp,
84+
args.webPoSignalOutput,
85+
args.skipPrivacyBuffer
86+
]);
87+
});
88+
}
89+
}
90+
/**
91+
* Parses the challenge data from the provided response data.
92+
*/
93+
function parseChallengeData(rawData) {
94+
let challengeData = [];
95+
96+
if (rawData.length > 1 && typeof rawData[1] === 'string') {
97+
const descrambled = descramble(rawData[1]);
98+
challengeData = JSON.parse(descrambled || '[]');
99+
} else if (rawData.length && typeof rawData[0] === 'object') {
100+
challengeData = rawData[0];
101+
}
102+
103+
const [ messageId, wrappedScript, wrappedUrl, interpreterHash, program, globalName, , clientExperimentsStateBlob ] = challengeData;
104+
105+
const privateDoNotAccessOrElseSafeScriptWrappedValue = Array.isArray(wrappedScript) ? wrappedScript.find((value) => value && typeof value === 'string') : null;
106+
const privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = Array.isArray(wrappedUrl) ? wrappedUrl.find((value) => value && typeof value === 'string') : null;
107+
108+
return {
109+
messageId,
110+
interpreterJavascript: {
111+
privateDoNotAccessOrElseSafeScriptWrappedValue,
112+
privateDoNotAccessOrElseTrustedResourceUrlWrappedValue
113+
},
114+
interpreterHash,
115+
program,
116+
globalName,
117+
clientExperimentsStateBlob
118+
};
119+
}
120+
121+
/**
122+
* Descrambles the given challenge data.
123+
*/
124+
function descramble(scrambledChallenge) {
125+
const buffer = base64ToU8(scrambledChallenge);
126+
if (buffer.length)
127+
return new TextDecoder().decode(buffer.map((b) => b + 97));
128+
}
129+
130+
const base64urlCharRegex = /[-_.]/g;
131+
132+
const base64urlToBase64Map = {
133+
'-': '+',
134+
_: '/',
135+
'.': '='
136+
};
137+
138+
function base64ToU8(base64) {
139+
let base64Mod;
140+
141+
if (base64urlCharRegex.test(base64)) {
142+
base64Mod = base64.replace(base64urlCharRegex, function (match) {
143+
return base64urlToBase64Map[match];
144+
});
145+
} else {
146+
base64Mod = base64;
147+
}
148+
149+
base64Mod = atob(base64Mod);
150+
151+
return new Uint8Array(
152+
[ ...base64Mod ].map(
153+
(char) => char.charCodeAt(0)
154+
)
155+
);
156+
}
157+
158+
function u8ToBase64(u8, base64url = false) {
159+
const result = btoa(String.fromCharCode(...u8));
160+
161+
if (base64url) {
162+
return result
163+
.replace(/\+/g, '-')
164+
.replace(/\//g, '_');
165+
}
166+
167+
return result;
168+
}
169+
170+
async function runBotGuard(rawChallengeData) {
171+
const challengeData = parseChallengeData(rawChallengeData)
172+
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
173+
174+
if (interpreterJavascript) {
175+
new Function(interpreterJavascript)();
176+
} else throw new Error('Could not load VM');
177+
178+
const botguard = await BotGuardClient.create({
179+
globalName: challengeData.globalName,
180+
globalObj: globalThis,
181+
program: challengeData.program
182+
});
183+
184+
const webPoSignalOutput = [];
185+
const botguardResponse = await botguard.snapshot({ webPoSignalOutput });
186+
return { webPoSignalOutput, botguardResponse }
187+
}
188+
189+
async function obtainPoToken(webPoSignalOutput, integrityTokenResponse, identifier) {
190+
const integrityToken = integrityTokenResponse[0];
191+
const getMinter = webPoSignalOutput[0];
192+
193+
if (!getMinter)
194+
throw new Error('PMD:Undefined');
195+
196+
const mintCallback = await getMinter(base64ToU8(integrityToken));
197+
198+
if (!(mintCallback instanceof Function))
199+
throw new Error('APF:Failed');
200+
201+
const result = await mintCallback(new TextEncoder().encode(identifier));
202+
203+
if (!result)
204+
throw new Error('YNJ:Undefined');
205+
206+
if (!(result instanceof Uint8Array))
207+
throw new Error('ODM:Invalid');
208+
209+
return u8ToBase64(result, true);
210+
}
211+
</script></head><body></body></html>

app/src/main/java/org/schabi/newpipe/App.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
import android.app.Application;
44
import android.content.Context;
55
import android.content.SharedPreferences;
6+
import android.os.Build;
7+
import android.os.UserManager;
68
import android.util.Log;
9+
import android.view.View;
10+
import android.webkit.JavascriptInterface;
11+
import android.webkit.WebSettings;
12+
import android.webkit.WebView;
13+
import android.webkit.WebViewClient;
14+
import android.widget.RelativeLayout;
715

816
import androidx.annotation.NonNull;
917
import androidx.core.app.NotificationChannelCompat;
@@ -17,6 +25,8 @@
1725
import org.schabi.newpipe.error.ReCaptchaActivity;
1826
import org.schabi.newpipe.extractor.NewPipe;
1927
import org.schabi.newpipe.extractor.downloader.Downloader;
28+
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult;
29+
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
2030
import org.schabi.newpipe.ktx.ExceptionUtils;
2131
import org.schabi.newpipe.settings.NewPipeSettings;
2232
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
@@ -26,19 +36,24 @@
2636
import org.schabi.newpipe.util.image.ImageStrategy;
2737
import org.schabi.newpipe.util.image.PicassoHelper;
2838
import org.schabi.newpipe.util.image.PreferredImageQuality;
39+
import org.schabi.newpipe.util.potoken.PoTokenWebView;
2940

3041
import java.io.IOException;
3142
import java.io.InterruptedIOException;
3243
import java.net.SocketException;
3344
import java.util.List;
3445
import java.util.Objects;
3546

47+
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
48+
import io.reactivex.rxjava3.core.Single;
49+
import io.reactivex.rxjava3.disposables.CompositeDisposable;
3650
import io.reactivex.rxjava3.exceptions.CompositeException;
3751
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
3852
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
3953
import io.reactivex.rxjava3.exceptions.UndeliverableException;
4054
import io.reactivex.rxjava3.functions.Consumer;
4155
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
56+
import kotlin.Pair;
4257

4358
/*
4459
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
@@ -118,6 +133,23 @@ public void onCreate() {
118133
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
119134

120135
configureRxJavaErrorHandler();
136+
137+
CompositeDisposable disposable = new CompositeDisposable();
138+
disposable.add(PoTokenWebView.Companion.newPoTokenGenerator(this)
139+
.observeOn(AndroidSchedulers.mainThread())
140+
.subscribeOn(AndroidSchedulers.mainThread())
141+
.flatMap(poTokenGenerator -> Single.zip(
142+
poTokenGenerator.generatePoToken(YoutubeParsingHelper
143+
.randomVisitorData(NewPipe.getPreferredContentCountry())),
144+
poTokenGenerator.generatePoToken("i_SsnRdgitA"),
145+
Pair::new
146+
))
147+
.subscribe(
148+
pots -> Log.e(TAG, "success! " + pots.getSecond().poToken +
149+
",web.gvs+" + pots.getFirst().poToken +
150+
";visitor_data=" + pots.getFirst().visitorData),
151+
error -> Log.e(TAG, "error", error)
152+
));
121153
}
122154

123155
@Override
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.schabi.newpipe.util.potoken
2+
3+
class PoTokenException(message: String) : Exception(message)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.schabi.newpipe.util.potoken
2+
3+
import android.content.Context
4+
import io.reactivex.rxjava3.core.Single
5+
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
6+
import java.io.Closeable
7+
8+
interface PoTokenGenerator : Closeable {
9+
/**
10+
* Generates a poToken for the provided identifier, using the `integrityToken` and
11+
* `webPoSignalOutput` previously obtained in the initialization of [PoTokenWebView]. Can be
12+
* called multiple times.
13+
*/
14+
fun generatePoToken(identifier: String): Single<PoTokenResult>
15+
16+
interface Factory {
17+
/**
18+
* Initializes a [PoTokenGenerator] by loading the BotGuard VM, running it, and obtaining
19+
* an `integrityToken`. Can then be used multiple times to generate multiple poTokens with
20+
* [generatePoToken].
21+
*
22+
* @param context used e.g. to load the HTML asset or to instantiate a WebView
23+
*/
24+
fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator>
25+
}
26+
}

0 commit comments

Comments
 (0)