All files / src/encryption pbkdf2.ts

89.71% Statements 61/68
55% Branches 11/20
100% Functions 13/13
89.06% Lines 57/64

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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 1601x                             1x       7x                   7x     7x 7x 7x     7x           1x       1x                     1x   1x 1x         1x 1x 1x     1x               1x       1x                 1x                   1x     1x 1x 1x 100000x   100000x 100000x   1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x     1x 1x   1x 1x 1x   99999x 99999x 6399936x     1x 1x   1x       1x 8x 8x 1x   7x        
import { getCryptoLib } from './cryptoUtils'
 
export type Pbkdf2Digests = 'sha512' | 'sha256'
 
export interface Pbkdf2 {
  derive(
    password: string, 
    salt: Buffer,
    iterations: number, 
    keyLength: number, 
    digest: Pbkdf2Digests): Promise<Buffer>;
}
 
type NodePbkdf2Fn = typeof import('crypto').pbkdf2
 
export class NodeCryptoPbkdf2 implements Pbkdf2 {
  nodePbkdf2: NodePbkdf2Fn
 
  constructor(nodePbkdf2: NodePbkdf2Fn) {
    this.nodePbkdf2 = nodePbkdf2
  }
 
  async derive(
    password: string, 
    salt: Buffer,
    iterations: number, 
    keyLength: number, 
    digest: Pbkdf2Digests): 
    Promise<Buffer> {
    Iif (digest !== 'sha512' && digest !== 'sha256') {
      throw new Error(`Unsupported digest "${digest}" for Pbkdf2`)
    }
    return new Promise((resolve, reject) => {
      this.nodePbkdf2(password, salt, iterations, keyLength, digest, (error, result) => {
        Iif (error) {
          reject(error)
        }
        resolve(result)
      })
    })
  }
}
 
export class WebCryptoPbkdf2 implements Pbkdf2 {
  subtleCrypto: SubtleCrypto
 
  constructor(subtleCrypto: SubtleCrypto) {
    this.subtleCrypto = subtleCrypto
  }
 
  async derive(
    password: string, 
    salt: Buffer,
    iterations: number, 
    keyLength: number, 
    digest: Pbkdf2Digests): 
    Promise<Buffer> {
    let algo: string
    Iif (digest === 'sha256') {
      algo = 'SHA-256'
    } else Eif (digest === 'sha512') {
      algo = 'SHA-512'
    } else {
      throw new Error(`Unsupported Pbkdf2 digest algorithm "${digest}"`)
    }
    let result: ArrayBuffer
    const passwordBytes = Buffer.from(password, 'utf8')
    try {
      const key = await this.subtleCrypto.importKey(
        'raw', passwordBytes, 'PBKDF2', false, ['deriveBits']
      )
      result = await this.subtleCrypto.deriveBits({
        name: 'PBKDF2', salt, iterations, hash: { name: algo }
      }, key, keyLength * 8)
    } catch (error) {
      // Browser appears to support WebCrypto but missing pbkdf2 support.
      const partialWebCrypto = new WebCryptoPartialPbkdf2(this.subtleCrypto)
      return partialWebCrypto.derive(password, salt, iterations, keyLength, digest)
    }
    return Buffer.from(result)
  }
}
 
export class WebCryptoPartialPbkdf2 implements Pbkdf2 {
  // An async implementation for browsers that support WebCrypto hmac 
  // but not pbkdf2. Extracted from crypto-browserify/pbkdf2 and modified to 
  // use WebCrypto for hmac operations.
  // Original: https://github.com/crypto-browserify/pbkdf2/tree/v3.0.17/lib
 
  subtleCrypto: SubtleCrypto
 
  constructor(subtleCrypto: SubtleCrypto) {
    this.subtleCrypto = subtleCrypto
  }
 
  async derive(
    password: string, 
    salt: Buffer,
    iterations: number, 
    keyLength: number, 
    digest: Pbkdf2Digests): 
    Promise<Buffer> {
    Iif (digest !== 'sha512' && digest !== 'sha256') {
      throw new Error(`Unsupported digest "${digest}" for Pbkdf2`)
    }
    const key = Buffer.from(password, 'utf8')
    const algo = digest === 'sha512' ? 'SHA-512' : 'SHA-256'
    const algoOpts = { name: 'HMAC', hash: algo }
    const hmacDigest = (key: ArrayBuffer, data: ArrayBuffer) => this.subtleCrypto
      .importKey('raw', key, algoOpts, true, ['sign'])
      .then(cryptoKey => this.subtleCrypto.sign(algoOpts, cryptoKey, data))
      .then(result => new Uint8Array(result))
  
    const DK = new Uint8Array(keyLength)
    const saltLength = salt.length
    const block1 = new Uint8Array(saltLength + 4)
    block1.set(salt)
    let destPos = 0
    const hLen = digest === 'sha512' ? 64 : 32
    const l = Math.ceil(keyLength / hLen)
  
    function writeUInt32BE(data: Uint8Array, value: number, offset: number) {
      value = +value
      offset >>>= 0
      data[offset] = (value >>> 24)
      data[offset + 1] = (value >>> 16)
      data[offset + 2] = (value >>> 8)
      data[offset + 3] = (value & 0xff)
      return offset + 4
    }
  
    for (let i = 1; i <= l; i++) {
      writeUInt32BE(block1, i, saltLength)
      // eslint-disable-next-line no-await-in-loop
      const T = await hmacDigest(key, block1)
      let U = T
      for (let j = 1; j < iterations; j++) {
        // eslint-disable-next-line no-await-in-loop
        U = await hmacDigest(key, U)
        for (let k = 0; k < hLen; k++) {
          T[k] ^= U[k]
        }
      }
      DK.set(T.subarray(0, DK.byteLength - destPos), destPos)
      destPos += hLen
    }
    return Buffer.from(DK.buffer)
  }
}
 
export async function createPbkdf2(): Promise<Pbkdf2> {
  const cryptoLib = await getCryptoLib()
  if (cryptoLib.name === 'subtleCrypto') {
    return new WebCryptoPbkdf2(cryptoLib.lib)
  } else {
    return new NodeCryptoPbkdf2(cryptoLib.lib.pbkdf2)
  }
}