Skip to content

Commit

Permalink
feat: add method for decoding sealed results
Browse files Browse the repository at this point in the history
  • Loading branch information
TheUnderScorer committed Jan 23, 2024
1 parent f75d317 commit 4220fcd
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 1 deletion.
26 changes: 26 additions & 0 deletions example/sealedResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { unsealEventsResponse } = require('@fingerprintjs/fingerprintjs-pro-server-api');

async function main() {
const sealedData = process.env.BASE64_SEALED_RESULT;
const decryptionKey = process.env.BASE64_KEY;

if (!sealedData || !decryptionKey) {
console.error('Please set BASE64_KEY and BASE64_SEALED_RESULT environment variables');
process.exit(1);
}

try {
const unsealedData = await unsealEventsResponse(Buffer.from(sealedData, 'base64'), [
{
key: Buffer.from(decryptionKey, 'base64'),
algorithm: 'aes-256-gcm',
},
]);
console.log(JSON.stringify(unsealedData, null, 2));
} catch (e) {
console.error(e);
process.exit(1);
}
}

main();
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"test:coverage": "jest --coverage",
"test:dts": "tsc --noEmit --isolatedModules dist/index.d.ts",
"generateTypes": "yarn openapi-typescript resources/fingerprint-server-api.yaml --output ./src/generatedApiTypes.ts -c .prettierrc.json",
"release": "semantic-release"
"release": "semantic-release",
"docs": "typedoc --out docs src"
},
"keywords": [],
"author": "FingerprintJS, Inc (https://fingerprint.com)",
Expand Down
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ client
});
```

## Sealed results

This SDK provides utility methods for decoding sealed results.
To learn more, refer to example located in [example/sealedResults.js](./example/sealedResults.js).

## API Reference

### `constructor({region: Region, apiKey: string})`
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './urlUtils';
export * from './serverApiClient';
export * from './types';
export * from './sealedResults';
69 changes: 69 additions & 0 deletions src/sealedResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createDecipheriv } from 'crypto';
import { inflateRaw } from 'zlib';
import { promisify } from 'util';
import { EventResponse } from './types';

const asyncInflateRaw = promisify(inflateRaw);

export enum DecryptionAlgorithm {
Aes256Gcm = 'aes-256-gcm',
}

export interface DecryptionKey {
key: Buffer;
algorithm: DecryptionAlgorithm;
}

const SEALED_HEADER = Buffer.from([0x9e, 0x85, 0xdc, 0xed]);

export async function unsealEventsResponse(sealedData: Buffer, decryptionKeys: DecryptionKey[]) {
const unsealed = await unseal(sealedData, decryptionKeys);
const json = JSON.parse(unsealed) as EventResponse;

if (!json.products) {
throw new Error('Sealed data is not valid events response');

Check warning on line 24 in src/sealedResults.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 25 in src/sealedResults.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

return json;
}

export async function unseal(sealedData: Buffer, decryptionKeys: DecryptionKey[]) {
if (
sealedData.subarray(0, SEALED_HEADER.length).toString('hex') !== SEALED_HEADER.toString('hex')
) {
throw new Error('Invalid sealed data header');
}

for (const decryptionKey of decryptionKeys) {
switch (decryptionKey.algorithm) {
case DecryptionAlgorithm.Aes256Gcm:
try {
return await unsealAes256Gcm(sealedData, decryptionKey.key);
} catch {
continue;
}

default:
throw new Error(`Unsupported decryption algorithm: ${decryptionKey.algorithm}`);

Check warning on line 47 in src/sealedResults.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 47 in src/sealedResults.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
}

throw new Error('Unable to decrypt sealed data');
}

async function unsealAes256Gcm(sealedData: Buffer, decryptionKey: Buffer) {
const nonceLength = 12;
const nonce = sealedData.subarray(SEALED_HEADER.length, SEALED_HEADER.length + nonceLength);

const authTagLength = 16;
const authTag = sealedData.subarray(-authTagLength);

const ciphertext = sealedData.subarray(SEALED_HEADER.length + nonceLength, -authTagLength);

const decipher = createDecipheriv('aes-256-gcm', decryptionKey, nonce).setAuthTag(authTag);
const compressed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

const payload = await asyncInflateRaw(compressed);

return payload.toString();
}
136 changes: 136 additions & 0 deletions tests/unit-tests/sealedResults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { DecryptionAlgorithm, unsealEventsResponse } from '../../src';

describe('Unseal event response', () => {
const sealedData = Buffer.from(
'noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==',
'base64'
);
const validKey = Buffer.from('p2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53=', 'base64');
const invalidKey = Buffer.from('a2PA7MGy5tx56cnyJaFZMr96BCFwZeHjZV2EqMvTq53=', 'base64');

it('unseals sealed data using aes256gcm', async () => {
const result = await unsealEventsResponse(sealedData, [
{
key: invalidKey,
algorithm: DecryptionAlgorithm.Aes256Gcm,
},
{
key: validKey,
algorithm: DecryptionAlgorithm.Aes256Gcm,
},
]);

expect(result).toBeTruthy();
expect(result).toMatchInlineSnapshot(`
Object {
"products": Object {
"botd": Object {
"data": Object {
"bot": Object {
"result": "notDetected",
},
"ip": "::1",
"meta": Object {
"foo": "bar",
},
"requestId": "1703067132750.Z5hutJ",
"time": "2023-12-20T10:12:13.894Z",
"url": "http://localhost:8080/",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15",
},
},
"identification": Object {
"data": Object {
"browserDetails": Object {
"browserFullVersion": "17.3",
"browserMajorVersion": "17",
"browserName": "Safari",
"device": "Other",
"os": "Mac OS X",
"osVersion": "10.15.7",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15",
},
"confidence": Object {
"score": 1,
},
"firstSeenAt": Object {
"global": "2023-12-15T12:13:55.103Z",
"subscription": "2023-12-15T12:13:55.103Z",
},
"incognito": false,
"ip": "::1",
"ipLocation": Object {
"accuracyRadius": 1000,
"city": Object {
"name": "Stockholm",
},
"continent": Object {
"code": "EU",
"name": "Europe",
},
"country": Object {
"code": "SE",
"name": "Sweden",
},
"latitude": 59.3241,
"longitude": 18.0517,
"postalCode": "100 05",
"subdivisions": Array [
Object {
"isoCode": "AB",
"name": "Stockholm County",
},
],
"timezone": "Europe/Stockholm",
},
"lastSeenAt": Object {
"global": "2023-12-19T11:39:51.52Z",
"subscription": "2023-12-19T11:39:51.52Z",
},
"requestId": "1703067132750.Z5hutJ",
"tag": Object {
"foo": "bar",
},
"time": "2023-12-20T10:12:16Z",
"timestamp": 1703067136286,
"url": "http://localhost:8080/",
"visitorFound": true,
"visitorId": "2ZEDCZEfOfXjEmMuE3tq",
},
},
},
}
`);
});

it('throws error if header is not correct', async () => {
const invalidData = Buffer.from(
'xzXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw==',
'base64'
);

await expect(
unsealEventsResponse(invalidData, [
{
key: invalidKey,
algorithm: DecryptionAlgorithm.Aes256Gcm,
},
{
key: validKey,
algorithm: DecryptionAlgorithm.Aes256Gcm,
},
])
).rejects.toThrowError('Invalid sealed data header');
});

it('throws error if all decryption keys are invalid', async () => {
await expect(
unsealEventsResponse(sealedData, [
{
key: invalidKey,
algorithm: DecryptionAlgorithm.Aes256Gcm,
},
])
).rejects.toThrowError('Unable to decrypt sealed data');
});
});

0 comments on commit 4220fcd

Please sign in to comment.