All files / src sealedResults.ts

100% Statements 54/54
100% Branches 17/17
100% Functions 5/5
100% Lines 54/54

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 951x 1x 1x   1x 1x   1x   1x 1x               1x   4x 4x 4x         1x 4x   4x 2x 2x   2x 2x                 9x 9x   3x 3x         9x 9x 2x 2x   7x   9x 10x 10x 9x 9x 9x 6x 6x 6x   10x 1x 10x 10x   3x 3x   9x 9x 9x   9x 9x   9x   9x 9x   9x   3x 3x  
import { createDecipheriv } from 'crypto'
import { inflateRaw } from 'zlib'
import { promisify } from 'util'
import { Event } 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 Event {
  return Boolean(data && typeof data === 'object' && 'event_id' in data && 'timestamp' in data)
}
 
/**
 * @private
 * */
export function parseEventsResponse(unsealed: string): Event {
  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
 * @throws UnsealAggregateError
 * @throws Error
 */
export async function unsealEventsResponse(sealedData: Buffer, decryptionKeys: DecryptionKey[]): Promise<Event> {
  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()
}