This library facilitates the creation of PoTokens by directly interacting with BotGuard's API.
-
This library does not bypass BotGuard; you still need an environment that passes its checks to use it. It is simply a reverse-engineered implementation of the same process that YouTube’s web player uses.
-
The library is not affiliated with Google or YouTube in any way. It is an independent project created for educational purposes. I am not responsible for any misuse of this library.
Please refer to the provided examples here.
Below is a brief overview of the process to generate a PoToken for those interested in the inner workings of the library. This information is based on my own research and may become outdated as Google updates its systems.
First, retrieve the VM's script and program:
curl --request POST \
--url 'https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/Create' \
--header 'Content-Type: application/json+protobuf' \
--header 'x-goog-api-key: AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw' \
--header 'x-user-agent: grpc-web-javascript/0.1' \
--data '[ "requestKeyHere" ]'
Once the response data is available, it must be descrambled and parsed:
// ...
const buffer = base64ToU8(scrambledChallenge);
const descrambled = new TextDecoder().decode(buffer.map((b) => b + 97));
const challengeData = JSON.parse(descrambled);
The descrambled data should consist of a message ID, the interpreter JavaScript, the interpreter hash, a program, and the script's global name.
To make the VM available, evaluate the script:
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
If everything goes well, you should be able to access it like so:
const globalObject = window || globalThis;
console.log(globalObject[challengeData.globalName]);
This is an important step. The integrity token is retrieved from an attestation server and relies on the BotGuard result, likely to assess the integrity of the runtime environment. To "solve" this challenge, you need to invoke BotGuard and pass the retrieved program as its first argument.
// ...
if (!this.vm)
throw new Error('[BotGuardClient]: VM not found in the global object');
if (!this.vm.a)
throw new Error('[BotGuardClient]: Could not load program');
const vmFunctionsCallback = (
asyncSnapshotFunction,
shutdownFunction,
passEventFunction,
checkCameraFunction
) => {
Object.assign(this.vmFunctions, { asyncSnapshotFunction, shutdownFunction, passEventFunction, checkCameraFunction });
};
try {
this.syncSnapshotFunction = await this.vm.a(this.program, vmFunctionsCallback, true, undefined, () => { /** no-op */ }, [ [], [] ])[0];
} catch (error) {
throw new Error(`[BotGuardClient]: Failed to load program (${(error as Error).message})`);
}
The second parameter should be a callback function, where BotGuard will return several functions. In our case, we are mainly interested in asyncSnapshotFunction
.
Once asyncSnapshotFunction
is available, call it with the following arguments:
- A callback function that takes a single argument. This function will return the token for the attestation request.
- An array with four elements:
- 1st:
contentBinding
(Optional). - 2nd:
signedTimestamp
(Optional). - 3rd:
webPoSignalOutput
(Optional but required for our case, BotGuard will fill this array with a function to get a PoToken minter). - 4th:
skipPrivacyBuffer
(Optional, not sure what this one is/does).
- 1st:
async function snapshot(args) {
return new Promise((resolve, reject) => {
if (!this.vmFunctions.asyncSnapshotFunction)
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
this.vmFunctions.asyncSnapshotFunction((response) => resolve(response), [
args.contentBinding,
args.signedTimestamp,
args.webPoSignalOutput,
args.skipPrivacyBuffer
]);
});
}
Then:
const webPoSignalOutput = [];
const botguardResponse = await snapshot({ webPoSignalOutput });
If everything was done correctly, you should have a token and an array with one or more functions.
Now we can create the payload for the integrity token request. It should be an array of two items: the request key and the token.
curl --request POST \
--url 'https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/GenerateIT' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json+protobuf' \
--header 'x-goog-api-key: AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw' \
--header 'x-user-agent: grpc-web-javascript/0.1' \
--data '[ "requestKeyHere", "$abcdeyourbotguardtokenhere" ]'
If the API call is successful, you will receive a JSPB (json+protobuf) response that looks like this:
[
"azXvdvYQKz8ff4h9PjIlQI7JUOTtYnBdXEGs4bmQb8FvmFB+oosILg6flcoDfzFpwas/hitYcUzx3Qm+DFtQ9slN",
43200,
100,
]
The first item is the Integrity Token, the second is the TTL (Time to Live), and the third is the refresh threshold.
Store the token and the array we obtained earlier. We'll use them to construct the PoToken.
Call the first function in the webPoSignalOutput
array with the Integrity Token (in bytes) as an argument:
const getMinter = webPoSignalOutput[0];
if (!getMinter)
throw new Error('PMD:Undefined');
const mintCallback = await getMinter(base64ToU8(integrityTokenResponse.integrityToken ?? ''));
if (!(mintCallback instanceof Function))
throw new Error('APF:Failed');
If successful, you'll receive a function to mint PoTokens. Call it with your Visitor ID (or Data Sync ID if you're signed in) as an argument:
const result = await mintCallback(new TextEncoder().encode(identifier));
if (!result)
throw new Error('YNJ:Undefined');
if (!(result instanceof Uint8Array))
throw new Error('ODM:Invalid');
const poToken = u8ToBase64(result, true);
console.log(poToken);
The result will be a sequence of bytes, about 110–128 bytes in length. Base64 encode it, and you'll have a PoToken!
YouTube's web player checks the "sps" (StreamProtectionStatus
) of each media segment request (only if using UMP
or SABR
; our browser example uses UMP
) to determine if the stream needs a PoToken.
- Status 1: The stream is either already using a PoToken or does not need one.
- Status 2: The stream requires a PoToken but will allow the client to request up to 1-2 MB of data before interrupting playback.
- Status 3: The stream requires a PoToken and will interrupt playback immediately.
Distributed under the MIT License.