All files / src sealedResults.ts

100% Statements 39/39
100% Branches 9/9
100% Functions 6/6
100% Lines 39/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 968x 8x 8x   8x 8x   8x   8x 8x               8x     3x           8x 3x   3x 2x     1x               8x       8x   2x           8x 8x 2x     6x   6x 8x   7x 7x   5x 5x       1x       3x       7x 7x   7x 7x   7x   7x 5x   3x   2x    
import { createDecipheriv } from 'crypto'
import { inflateRaw } from 'zlib'
import { promisify } from 'util'
import { EventResponse } from './types'
import { UnsealAggregateError, UnsealError } from './errors/unsealError'
import { Buffer } from 'buffer'
 
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])
 
function isEventResponse(data: unknown): data is EventResponse {
  return Boolean(data && typeof data === 'object' && 'products' in data)
}
 
/**
 * @private
 * */
export function parseEventsResponse(unsealed: string): EventResponse {
  const json = JSON.parse(unsealed)
 
  if (!isEventResponse(json)) {
    throw new Error('Sealed data is not valid events response')
  }
 
  return json
}
 
/**
 * Decrypts the sealed response with the provided keys.
 * The SDK will try to decrypt the result with each key until it succeeds.
 * To learn more about sealed results visit: https://dev.fingerprint.com/docs/sealed-client-results
 */
export async function unsealEventsResponse(
  sealedData: Buffer,
  decryptionKeys: DecryptionKey[]
): Promise<EventResponse> {
  const unsealed = await unseal(sealedData, decryptionKeys)
 
  return parseEventsResponse(unsealed)
}
 
/**
 * @private
 * */
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')
  }
 
  const errors = new UnsealAggregateError([])
 
  for (const decryptionKey of decryptionKeys) {
    switch (decryptionKey.algorithm) {
      case DecryptionAlgorithm.Aes256Gcm:
        try {
          return await unsealAes256Gcm(sealedData, decryptionKey.key)
        } catch (e) {
          errors.addError(new UnsealError(decryptionKey, e as Error))
          continue
        }
 
      default:
        throw new Error(`Unsupported decryption algorithm: ${decryptionKey.algorithm}`)
    }
  }
 
  throw errors
}
 
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()
}