Skip to content

Commit

Permalink
Merge pull request #590 from Telegram-Mini-Apps/feature/3rd-party-ini…
Browse files Browse the repository at this point in the history
…t-data-validation

3rd party init data validation utilities
  • Loading branch information
heyqbnk authored Dec 7, 2024
2 parents 025d2f3 + a91a0cb commit c2f694f
Show file tree
Hide file tree
Showing 30 changed files with 576 additions and 415 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-badgers-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/init-data-node": minor
---

Add utilities related to 3-rd party validation. Git rid of Node.js `Buffer`.
5 changes: 5 additions & 0 deletions .changeset/mean-tomatoes-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/types": minor
---

Define InitData.signature.
5 changes: 5 additions & 0 deletions .changeset/perfect-icons-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/transformers": minor
---

Add InitData.signature transformer.
60 changes: 59 additions & 1 deletion apps/docs/packages/telegram-apps-init-data-node.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
outline: [2, 3]
---

# @telegram-apps/init-data-node

<p style="display: flex; gap: 8px; min-height: 20px">
Expand Down Expand Up @@ -133,6 +137,45 @@ duration is set to 1 day (86,400 seconds). It is recommended to always check the
initialization data, as it could be stolen but still remain valid. To disable this feature,
pass `{ expiresIn: 0 }` as the third argument.

### `validate3rd`

The `validate3rd` function is used to check if the passed init data was signed by Telegram. As
well as the `validate` function, this one accepts the init data in the same format.

As the second argument, it accepts the Telegram Bot identifier that was used to sign this
init data.

The third argument is an object with the following properties:

- `expiresIn` is responsible for init data expiration validation
- `test: boolean`: should be equal `true` if the init data was received in the test Telegram
environment

Here is the usage example:

```ts
const initData =
'user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%20%2B%20-%20%3F%20%5C%2F%22%2C%22last_name%22%3A%22Kibenko%22%2C%22username%22%3A%22vdkfrost%22%2C%22language_code%22%3A%22ru%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%2C%22photo_url%22%3A%22https%3A%5C%2F%5C%2Ft.me%5C%2Fi%5C%2Fuserpic%5C%2F320%5C%2F4FPEE4tmP3ATHa57u6MqTDih13LTOiMoKoLDRG4PnSA.svg%22%7D' +
'&chat_instance=8134722200314281151' +
'&chat_type=private' +
'&auth_date=1733584787' +
'&signature=zL-ucjNyREiHDE8aihFwpfR9aggP2xiAo3NSpfe-p7IbCisNlDKlo7Kb6G4D0Ao2mBrSgEk4maLSdv6MLIlADQ' +
'&hash=2174df5b000556d044f3f020384e879c8efcab55ddea2ced4eb752e93e7080d6';
const botId = 7342037359;

await validate3rd(initData, botId);
```

Function will throw an error in one of these cases:

- `ERR_AUTH_DATE_INVALID`: `auth_date` is empty or not found
- `ERR_SIGNATURE_MISSING`: `signature` is empty or not found
- `ERR_SIGN_INVALID`: signature is invalid
- `ERR_EXPIRED`: init data expired

> [!WARNING]
> This function uses **Web Crypto API** instead of Node.js modules.
### `isValid`

Alternatively, to check the init data validity, a developer could use the `isValid` function.
Expand All @@ -141,11 +184,26 @@ It doesn't throw an error, but returns a boolean value indicating the init data
```ts
import { isValid } from '@telegram-apps/init-data-node';

if (isValid('init-data')) {
if (isValid('init-data', 'my-bot-token')) {
console.log('Init data is fine');
}
```

### `isValid3rd`

Does the same as the `isValid` function, but checks if the init data was signed by Telegram:

```ts
import { isValid3rd } from '@telegram-apps/init-data-node';

if (await isValid3rd('init-data')) {
console.log('Init data is fine');
}
```

> [!WARNING]
> This function uses **Web Crypto API** instead of Node.js modules.
## Signing

There could be some cases when a developer needs to create their own init data. For instance,
Expand Down
5 changes: 3 additions & 2 deletions packages/bridge/src/env/mockTelegramEnv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@ const lp: LaunchParams = {
lastName: 'Rogue',
username: 'rogue',
},
signature: 'abc',
},
initDataRaw: 'user=%7B%22id%22%3A99281932%2C%22first_name%22%3A%22Andrew%22%2C%22last_name%22%3A%22Rogue%22%2C%22username%22%3A%22rogue%22%2C%22language_code%22%3A%22en%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%7D&chat_instance=8428209589180549439&chat_type=sender&start_param=debug&auth_date=1716922846&hash=89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31',
initDataRaw: 'user=%7B%22id%22%3A99281932%2C%22first_name%22%3A%22Andrew%22%2C%22last_name%22%3A%22Rogue%22%2C%22username%22%3A%22rogue%22%2C%22language_code%22%3A%22en%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%7D&chat_instance=8428209589180549439&chat_type=sender&start_param=debug&auth_date=1716922846&hash=89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31&signature=abc',
version: '7.2',
platform: 'tdesktop',
botInline: false,
showSettings: false
};

const lpString = 'tgWebAppPlatform=tdesktop&tgWebAppThemeParams=%7B%22accent_text_color%22%3A%22%236ab2f2%22%2C%22bg_color%22%3A%22%2317212b%22%2C%22button_color%22%3A%22%235288c1%22%2C%22button_text_color%22%3A%22%23ffffff%22%2C%22destructive_text_color%22%3A%22%23ec3942%22%2C%22header_bg_color%22%3A%22%2317212b%22%2C%22hint_color%22%3A%22%23708499%22%2C%22link_color%22%3A%22%236ab3f3%22%2C%22secondary_bg_color%22%3A%22%23232e3c%22%2C%22section_bg_color%22%3A%22%2317212b%22%2C%22section_header_text_color%22%3A%22%236ab3f3%22%2C%22subtitle_text_color%22%3A%22%23708499%22%2C%22text_color%22%3A%22%23f5f5f5%22%7D&tgWebAppVersion=7.2&tgWebAppData=user%3D%257B%2522id%2522%253A99281932%252C%2522first_name%2522%253A%2522Andrew%2522%252C%2522last_name%2522%253A%2522Rogue%2522%252C%2522username%2522%253A%2522rogue%2522%252C%2522language_code%2522%253A%2522en%2522%252C%2522is_premium%2522%253Atrue%252C%2522allows_write_to_pm%2522%253Atrue%257D%26chat_instance%3D8428209589180549439%26chat_type%3Dsender%26start_param%3Ddebug%26auth_date%3D1716922846%26hash%3D89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31&tgWebAppShowSettings=0&tgWebAppBotInline=0';
const lpString = 'tgWebAppPlatform=tdesktop&tgWebAppThemeParams=%7B%22accent_text_color%22%3A%22%236ab2f2%22%2C%22bg_color%22%3A%22%2317212b%22%2C%22button_color%22%3A%22%235288c1%22%2C%22button_text_color%22%3A%22%23ffffff%22%2C%22destructive_text_color%22%3A%22%23ec3942%22%2C%22header_bg_color%22%3A%22%2317212b%22%2C%22hint_color%22%3A%22%23708499%22%2C%22link_color%22%3A%22%236ab3f3%22%2C%22secondary_bg_color%22%3A%22%23232e3c%22%2C%22section_bg_color%22%3A%22%2317212b%22%2C%22section_header_text_color%22%3A%22%236ab3f3%22%2C%22subtitle_text_color%22%3A%22%23708499%22%2C%22text_color%22%3A%22%23f5f5f5%22%7D&tgWebAppVersion=7.2&tgWebAppData=user%3D%257B%2522id%2522%253A99281932%252C%2522first_name%2522%253A%2522Andrew%2522%252C%2522last_name%2522%253A%2522Rogue%2522%252C%2522username%2522%253A%2522rogue%2522%252C%2522language_code%2522%253A%2522en%2522%252C%2522is_premium%2522%253Atrue%252C%2522allows_write_to_pm%2522%253Atrue%257D%26chat_instance%3D8428209589180549439%26chat_type%3Dsender%26start_param%3Ddebug%26auth_date%3D1716922846%26hash%3D89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31%26signature%3Dabc&tgWebAppShowSettings=0&tgWebAppBotInline=0';

it('should save passed launch parameters in session storage', () => {
const setItem = mockSessionStorageSetItem();
Expand Down
85 changes: 0 additions & 85 deletions packages/bridge/src/launch-params/parseLaunchParams.test.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/bridge/src/utils/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,18 @@ describe('options', () => {
expect(globalPostEvent).toHaveBeenCalledWith('web_app_request_phone', undefined);
});

it('should reject promise if postEvent threw an error', () => {
it('should reject promise if postEvent threw an error', async () => {
const promise = request('web_app_request_phone', 'phone_requested', {
postEvent() {
throw new Error('Nope!');
},
});
void expect(promise).rejects.toStrictEqual(new Error('Nope!'));
await expect(promise).rejects.toStrictEqual(new Error('Nope!'));
});
});

describe('capture', () => {
it('should capture an event in case, capture method returned true', () => {
it('should capture an event in case, capture method returned true', async () => {
const promise = request('web_app_request_phone', 'phone_requested', {
timeout: 1000,
capture: ({ status }) => status === 'allowed',
Expand Down
10 changes: 10 additions & 0 deletions packages/init-data-node/src/converters/arrayBufferToHex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Converts array buffer to hex.
* @param buffer - buffer to convert
*/
export function arrayBufferToHex(buffer: ArrayBuffer): string {
return new Uint8Array(buffer).reduce((acc, byte) => {
// Convert byte to hex and pad with zero if needed (e.g., "0a" instead of "a")
return acc + byte.toString(16).padStart(2, '0');
}, '');
}
17 changes: 17 additions & 0 deletions packages/init-data-node/src/converters/hexToArrayBuffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Converts a hex string to ArrayBuffer.
* @param hexString - value to convert.
*/
export function hexToArrayBuffer(hexString: string): ArrayBuffer {
if (hexString.length % 2 !== 0) {
throw new Error('Hex string must have an even number of characters');
}

const buffer = new ArrayBuffer(hexString.length / 2);
const uint8Array = new Uint8Array(buffer);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substring(i, i + 2), 16);
}

return buffer;
}
Loading

0 comments on commit c2f694f

Please sign in to comment.