blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
160 lines (141 loc) • 4.72 kB
text/typescript
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> {
if (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) => {
if (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
if (digest === 'sha256') {
algo = 'SHA-256'
} else if (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> {
if (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)
}
}