-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add method for decoding sealed results
- Loading branch information
1 parent
f75d317
commit 4220fcd
Showing
6 changed files
with
239 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
|
||
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 GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)🧾 Statement is not covered
|
||
} | ||
} | ||
|
||
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |