New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

bgutils-js

Package Overview
Dependencies
Maintainers
0
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bgutils-js - npm Package Compare versions

Comparing version 3.1.2 to 3.1.3

11

dist/core/webPoClient.d.ts

@@ -8,13 +8,16 @@ import type { PoTokenArgs, PoTokenResult } from '../utils/index.js';

/**
* Creates a placeholder PoToken. This can be used while `sps` (StreamProtectionStatus) is 2, but will not work once it changes to 3.
* Creates a cold start token. This can be used while `sps` (StreamProtectionStatus) is 2, but will not work once it changes to 3.
* @param identifier - Visitor ID or Data Sync ID.
* @param clientState - The client state.
*/
export declare function generateColdStartToken(identifier: string, clientState?: number): string;
/**
* @deprecated Use `generateColdStartToken` instead.
*/
export declare function generatePlaceholder(identifier: string, clientState?: number): string;
/**
* Decodes a placeholder potoken string into its components.
* @param placeholder - The placeholder potoken to decode.
* Decodes a cold start webpo token.
* @throws Error if the packet length is invalid.
*/
export declare function decodePlaceholder(placeholder: string): {
export declare function decodeColdStartToken(token: string): {
identifier: string;

@@ -21,0 +24,0 @@ timestamp: number;

@@ -33,7 +33,7 @@ import BotGuardClient from './botGuardClient.js';

/**
* Creates a placeholder PoToken. This can be used while `sps` (StreamProtectionStatus) is 2, but will not work once it changes to 3.
* Creates a cold start token. This can be used while `sps` (StreamProtectionStatus) is 2, but will not work once it changes to 3.
* @param identifier - Visitor ID or Data Sync ID.
* @param clientState - The client state.
*/
export function generatePlaceholder(identifier, clientState) {
export function generateColdStartToken(identifier, clientState) {
const encodedIdentifier = new TextEncoder().encode(identifier);

@@ -67,8 +67,13 @@ if (encodedIdentifier.length > 118)

/**
* Decodes a placeholder potoken string into its components.
* @param placeholder - The placeholder potoken to decode.
* @deprecated Use `generateColdStartToken` instead.
*/
export function generatePlaceholder(identifier, clientState) {
return generateColdStartToken(identifier, clientState);
}
/**
* Decodes a cold start webpo token.
* @throws Error if the packet length is invalid.
*/
export function decodePlaceholder(placeholder) {
const packet = base64ToU8(placeholder);
export function decodeColdStartToken(token) {
const packet = base64ToU8(token);
const payloadLength = packet[1];

@@ -75,0 +80,0 @@ const totalPacketLength = 2 + payloadLength;

import type { IntegrityTokenData, MintCallback, WebPoSignalOutput } from '../utils/types.js';
export default class WebPoMinter {
private mintCallback;
private readonly mintCallback;
constructor(mintCallback: MintCallback);

@@ -5,0 +5,0 @@ static create(integrityTokenResponse: IntegrityTokenData, webPoSignalOutput: WebPoSignalOutput): Promise<WebPoMinter>;

@@ -38,3 +38,3 @@ export type PoTokenArgs = {

webPoSignalOutput?: WebPoSignalOutput;
skipPrivacyBuffer?: unknown;
skipPrivacyBuffer?: boolean;
};

@@ -65,3 +65,3 @@ export type IntegrityTokenData = {

/**
* The program.
* The challenge program.
*/

@@ -81,6 +81,18 @@ program: string;

fetch: FetchFunction;
/**
* Global object in which the VM is loaded.
*/
globalObj: Record<string, any>;
/**
* Content binding.
*/
identifier: string;
/**
* A lookup key which maps to a program descriptor in the server config.
*/
requestKey: string;
/**
* Whether to use the YouTube API.
*/
useYouTubeAPI?: boolean;
};
{
"name": "bgutils-js",
"version": "3.1.2",
"version": "3.1.3",
"description": "A JavaScript library for interfacing with Botguard.",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -1,6 +0,5 @@

# What Is This?
This library facilitates the creation of PoTokens by directly interacting with BotGuard's API.
# Introduction
This library provides tools for generating PO Tokens and executing attestation challenges, reverse-engineering how YouTube’s web player interacts with BotGuard and the Web Anti-Abuse API.
- [What Is This?](#what-is-this)
- [Introduction](#introduction)
- [A Few Notes](#a-few-notes)

@@ -11,4 +10,4 @@ - [Usage](#usage)

- [Retrieving Integrity Token](#retrieving-integrity-token)
- [Minting WebPo Tokens](#minting-webpo-tokens)
- [When to Use a PoToken](#when-to-use-a-potoken)
- [Minting WebPO Tokens](#minting-webpo-tokens)
- [When to Use a PO Token](#when-to-use-a-po-token)
- [License](#license)

@@ -18,5 +17,5 @@

1. 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.
1. BotGuard is a security mechanism used by Google to protect its services from abuse and verify that requests originate from legitimate clients. This library provides a reverse-engineered implementation of the process used by YouTube's web player to generate PO Tokens and run attestation challenges. However, **it does not bypass BotGuard**; you still need a compliant environment that meets its checks to use this library.
2. 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.
2. This library is intended for educational purposes and is not affiliated with Google or YouTube. I am not responsible for any misuse of this library.

@@ -29,39 +28,115 @@ ## Usage

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.
Here’s a brief overview of the process for generating a PO Token, for those curious about the library’s inner workings. This information is based on my own research and may become outdated as Google updates its security mechanisms.
### Initialization Process
First, retrieve the VM's script and program:
```shell
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" ]'
The VM's script and respective bytecode program can be fetched in three different ways:
1. **Directly from the page's source code**:
- The (InnerTube) challenge response is usually embedded in the initial page's source code.
2. **InnerTube API**:
- InnerTube has an endpoint that can be used to retrieve challenge data. It is usually the easiest way to do so, as the response is in a readable format.
3. **Web Anti-Abuse Private API**:
- An internal Google API for BotGuard, also used by services like Google Drive. Responses may be obfuscated depending on the `requestKey`.
WAA challenge fetcher example:
```ts
type TrustedResource = {
privateDoNotAccessOrElseSafeScriptWrappedValue: string | null;
privateDoNotAccessOrElseTrustedResourceUrlWrappedValue: string | null;
}
type DescrambledChallenge = {
/**
* The ID of the JSPB message.
*/
messageId?: string;
/**
* The script associated with the challenge.
*/
interpreterJavascript: TrustedResource;
/**
* The hash of the script. Useful if you want to fetch the challenge script again at a later time.
*/
interpreterHash: string;
/**
* The challenge program.
*/
program: string;
/**
* The name of the VM in the global scope.
*/
globalName: string;
/**
* The client experiments state blob.
*/
clientExperimentsStateBlob?: string;
};
async function fetchWaaChallenge(requestKey: string, interpreterHash?: string): Promise<DescrambledChallenge | undefined> {
const payload = [ requestKey ];
if (interpreterHash)
payload.push(interpreterHash);
const response = await fetch('https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/Create', {
method: 'POST',
headers: {
'Content-Type': 'application/json+protobuf',
'x-goog-api-key': 'AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw',
'x-user-agent': 'grpc-web-javascript/0.1',
},
body: JSON.stringify(payload)
});
const rawData = await response.json() as unknown[];
// The response may be obfuscated. For an example implementation, see src/core/challengeFetcher.ts
return parseChallengeData(rawData);
};
const challengeResponse = await fetchWaaChallenge('requestKeyHere');
// ...
```
Once the response data is available, it must be descrambled and parsed:
```js
InnerTube challenge fetcher example (for the sake of simplicity, I'll use YouTube.js in this example):
```ts
import { Innertube, UniversalCache } from 'youtubei.js';
const innertube = await Innertube.create({ cache: new UniversalCache(true) });
const challengeResponse = await innertube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
if (!challengeResponse.bg_challenge)
throw new Error('Could not get challenge');
const interpreterUrl = challengeResponse.bg_challenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const bgScriptResponse = await fetch(`https:${interpreterUrl}`);
const interpreterJavascript = await bgScriptResponse.text();
// ...
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:
To make the VM available, you need to execute the script in some way:
```js
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (interpreterJavascript) {
new Function(interpreterJavascript)();
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
// If you're in a browser-like environment, you can also use the following:
if (!document.getElementById(interpreterHash)) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.id = interpreterHash;
script.textContent = interpreterJavascript;
document.head.appendChild(script);
}
```
If everything goes well, you should be able to access it like so:
If everything goes well, you should be able to access it like this:
```js
const globalObject = window || globalThis;
console.log(globalObject[challengeData.globalName]);
const vm = globalObject[globalName];
console.info(vm);
```

@@ -71,11 +146,11 @@

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.
This is an important step, the integrity token is retrieved from an attestation server and relies on the BotGuard response, likely to assess the integrity of the runtime environment. To solve this challenge, you need to invoke BotGuard and load the bytecode program.
```js
// ...
// Assuming you have the VM and the program available in some way.
if (!this.vm)
throw new Error('[BotGuardClient]: VM not found in the global object');
throw new Error('[BotGuardClient] VM not found in the global object');
if (!this.vm.a)
throw new Error('[BotGuardClient]: Could not load program');
throw new Error('[BotGuardClient] Cannot load program');

@@ -94,7 +169,7 @@ const vmFunctionsCallback = (

} catch (error) {
throw new Error(`[BotGuardClient]: Failed to load program (${(error as Error).message})`);
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`.
Here, BotGuard will return several functions, but we are mainly interested in `asyncSnapshotFunction`.

@@ -106,5 +181,6 @@ Once `asyncSnapshotFunction` is available, call it with the following arguments:

- 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).
- 3rd: `webPoSignalOutput` (Optional but required for our case, BotGuard will fill this array with a function to get a WebPO minter).
- 4th: `skipPrivacyBuffer` (Optional).
Here's a simplified example:
```js

@@ -132,31 +208,52 @@ async function snapshot(args) {

If everything was done correctly, you should have a token and an array with one or more functions.
If everything was done correctly so far, 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.
```shell
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" ]'
```
Example:
```ts
type PoIntegrityTokenResponse = {
integrityToken?: string;
estimatedTtlSecs: number;
mintRefreshThreshold: number;
websafeFallbackToken?: string;
};
If the API call is successful, you will receive a JSPB (json+protobuf) response that looks like this:
```json
[
"azXvdvYQKz8ff4h9PjIlQI7JUOTtYnBdXEGs4bmQb8FvmFB+oosILg6flcoDfzFpwas/hitYcUzx3Qm+DFtQ9slN",
43200,
100,
]
/**
* Creates an integrity token for use in PO Tokens (Proof of Origin).
* @param requestKey - The request key.
* @param botguardResponse - A valid BotGuard response.
*/
async function getPoIntegrityToken(requestKey: string, botguardResponse: string): Promise<PoIntegrityTokenResponse> {
const payload = [ requestKey, botguardResponse ];
const integrityTokenResponse = await fetch('https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/GenerateIT', {
method: 'POST',
headers: {
'Content-Type': 'application/json+protobuf',
'x-goog-api-key': 'AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw',
'x-user-agent': 'grpc-web-javascript/0.1',
},
body: JSON.stringify(payload)
});
const integrityTokenJson = await integrityTokenResponse.json() as [string, number, number, string];
const [ integrityToken, estimatedTtlSecs, mintRefreshThreshold, websafeFallbackToken ] = integrityTokenJson;
return {
integrityToken,
estimatedTtlSecs,
mintRefreshThreshold,
websafeFallbackToken
};
}
const integrityTokenResponse = await getPoIntegrityToken('requestKeyHere', botguardResponse);
```
The first item is the Integrity Token, the second is the TTL (Time to Live), and the third is the refresh threshold.
Store the integrity token response and the array we obtained earlier. We'll use them to construct our WebPO Token.
Store the token and the array we obtained earlier. We'll use them to construct the PoToken.
### Minting WebPO Tokens
### Minting WebPo Tokens
Call the first function in the `webPoSignalOutput` array with the Integrity Token (in bytes) as an argument:

@@ -175,4 +272,3 @@

```
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:
If successful, you'll get a function that can be used to mint WebPO tokens. Call it with the value you want to use as content binding, such as a Visitor ID, Data Sync ID (if you're signed in), or a Video ID.
```js

@@ -188,15 +284,17 @@ const result = await mintCallback(new TextEncoder().encode(identifier));

const poToken = u8ToBase64(result, true);
console.log(poToken);
console.info(poToken);
```
The result will be a sequence of bytes, about 110–128 bytes in length. Base64 encode it, and you'll have a PoToken!
The result will be a sequence of bytes, about 110–128 bytes in length. Base64 encode it, and you'll have a PO Token!
### When to Use a PoToken
### When to Use a PO Token
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.
On web, YouTube tries to mint a session bound PO Token as soon as the user interacts with the player, a cold start token is also minted to ensure playback starts without delays. Once minted, the PO Token is then reused for the rest of the session. If the user refreshes the page, the cached token is used (if available, otherwise a cold start token is used) until a new one finishes minting, and if that fails for some reason, the player will continue using the cached token as long as its respective integrity token is still valid.
- **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.
The player also checks a value called "sps" (`StreamProtectionStatus`), included in each media segment response (only if using `UMP` or `SABR`; our browser example uses `UMP`) to determine if the stream needs a PO Token.
- **Status 1**: The stream is already using a valid PO Token, the user has a YouTube Premium subscription, or the stream does not require PO Tokens.
- **Status 2**: A PO Token is required, but the client can request up to 1-2 MB of data before playback is interrupted.
- **Status 3**: At this stage, the player can no longer request data without a PO Token.
## License

@@ -203,0 +301,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc