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 969x 9x 9x   9x 9x   9x   9x 9x               9x     4x           9x 4x   4x 2x     2x               9x       9x   3x           9x 9x 2x     7x   7x 10x   9x 9x   6x 6x       1x       3x       9x 9x   9x 9x   9x   9x 7x   4x   3x    
import { createDecipheriv } from 'crypto'
import { inflateRaw } from 'zlib'
import { promisify } from 'util'
import { EventsGetResponse } 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 | `${DecryptionAlgorithm}`
}
 
const SEALED_HEADER = Buffer.from([0x9e, 0x85, 0xdc, 0xed])
 
function isEventResponse(data: unknown): data is EventsGetResponse {
  return Boolean(data && typeof data === 'object' && 'products' in data)
}
 
/**
 * @private
 * */
export function parseEventsResponse(unsealed: string): EventsGetResponse {
  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<EventsGetResponse> {
  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()
}