From 2215b9e5d8267a39930c24569a13491790a5abb3 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 26 Jan 2024 16:57:44 -0500 Subject: [PATCH] add/use test fixtures from Node.js --- example/src/components/Indentator.tsx | 30 ----- .../testing/Tests/pbkdf2Tests/pbkdf2Tests.ts | 125 +++++++++++++----- .../Tests/webcryptoTests/webcryptoTests.ts | 84 ++++++++---- src/Hashnames.ts | 15 +++ src/keys.ts | 2 +- src/pbkdf2.ts | 6 +- 6 files changed, 173 insertions(+), 89 deletions(-) delete mode 100644 example/src/components/Indentator.tsx diff --git a/example/src/components/Indentator.tsx b/example/src/components/Indentator.tsx deleted file mode 100644 index 0cb5292d..00000000 --- a/example/src/components/Indentator.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { View, StyleSheet } from 'react-native'; - -type IndentatorProps = { - indentation: number; - children: React.ReactChild; -}; - -export const Indentator: React.FC = ({ - indentation, - children, -}: IndentatorProps) => { - return ( - - - {children} - - ); -}; - -const styles = StyleSheet.create({ - container: { - width: '100%', - flexDirection: 'row', - alignContent: 'center', - }, - result: { - flex: 1, - }, -}); diff --git a/example/src/testing/Tests/pbkdf2Tests/pbkdf2Tests.ts b/example/src/testing/Tests/pbkdf2Tests/pbkdf2Tests.ts index c336a768..2f2f044e 100644 --- a/example/src/testing/Tests/pbkdf2Tests/pbkdf2Tests.ts +++ b/example/src/testing/Tests/pbkdf2Tests/pbkdf2Tests.ts @@ -5,6 +5,8 @@ import { Buffer } from '@craftzdog/react-native-buffer'; import type { Done } from 'mocha'; import { fixtures } from './fixtures'; +type TestFixture = [string, string, number, number, string]; + function ab2str(buf: ArrayBuffer) { return Buffer.from(buf).toString('hex'); } @@ -16,16 +18,79 @@ function ab2str(buf: ArrayBuffer) { // https://stackoverflow.com/questions/15593184/pbkdf2-hmac-sha-512-test-vectors describe('pbkdf2', () => { + // RFC 6070 tests from Node.js + { + const test = ( + pass: string, + salt: string, + iterations: number, + hash: string, + length: number, + expected: string, + done: Done + ) => { + QuickCrypto.pbkdf2( + pass, + salt, + iterations, + length, + hash, + function (err, result) { + try { + expect(err).to.eql(null); + expect(result).to.not.eql(null); + expect(ab2str(result as ArrayBuffer)).to.equal(expected); + done(); + } catch (e) { + done(e); + } + } + ); + }; + + const kTests: TestFixture[] = [ + ['password', 'salt', 1, 20, '120fb6cffcf8b32c43e7225256c4f837a86548c9'], + ['password', 'salt', 2, 20, 'ae4d0c95af6b46d32d0adff928f06dd02a303f8e'], + [ + 'password', + 'salt', + 4096, + 20, + 'c5e478d59288c841aa530db6845c4c8d962893a0', + ], + [ + 'passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 25, + '348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c', + ], + ['pass\0word', 'sa\0lt', 4096, 16, '89b69d0516f829893c696226650a8687'], + [ + 'password', + 'salt', + 32, + 32, + '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956', + ], + ]; + + kTests.forEach(([pass, salt, iterations, length, expected]) => { + const hash = 'sha256'; + it(`RFC 6070 - ${pass} ${salt} ${iterations} ${hash} ${length}`, (done: Done) => { + test(pass, salt, iterations, hash, length, expected, done); + }); + }); + } + // eslint-disable-next-line @typescript-eslint/no-shadow var Buffer = require('safe-buffer').Buffer; it(' defaults to sha1 and handles buffers', (done: Done) => { var resultSync = QuickCrypto.pbkdf2Sync('password', 'salt', 1, 32); - chai - .expect(ab2str(resultSync)) - .to.eql( - '0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164' - ); + expect(ab2str(resultSync)).to.eql( + '0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164' + ); QuickCrypto.pbkdf2( Buffer.from('password'), @@ -34,12 +99,10 @@ describe('pbkdf2', () => { 32, function (_, result) { - chai - // @ts-expect-error - .expect(ab2str(result)) - .to.eql( - '0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164' - ); + // @ts-expect-error + expect(ab2str(result)).to.eql( + '0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164' + ); done(); } ); @@ -155,32 +218,30 @@ describe('pbkdf2', () => { it(' async w/ ' + description, function () { function noop() {} - chai - .expect( - QuickCrypto.pbkdf2( - f.key, - f.salt, - f.iterations, - f.dkLen, - f.algo, - noop - ) + expect( + QuickCrypto.pbkdf2( + f.key, + f.salt, + f.iterations, + f.dkLen, + f.algo, + noop ) - .to.throw(new RegExp(f.exception)); + ) + .to.throw(new RegExp(f.exception)); }); it(' sync w/' + description, function () { - chai - .expect( - QuickCrypto.pbkdf2Sync( - f.key, - f.salt, - f.iterations, - f.dkLen, - f.algo - ) + expect( + QuickCrypto.pbkdf2Sync( + f.key, + f.salt, + f.iterations, + f.dkLen, + f.algo ) - .to.throw(new RegExp(f.exception)); + ) + .to.throw(new RegExp(f.exception)); }); }); */ }); diff --git a/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts b/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts index 4da15270..67cd09b9 100644 --- a/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts +++ b/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { atob, btoa } from 'react-native-quick-base64'; import crypto from 'react-native-quick-crypto'; import { describe, it } from '../../MochaRNAdapter'; +import type { HashAlgorithm } from '../../../../../src/keys'; // Tests that a key pair can be used for encryption / decryption. // function testEncryptDecrypt(publicKey: any, privateKey: any) { @@ -39,6 +40,15 @@ import { describe, it } from '../../MochaRNAdapter'; // } // } +type TestFixture = [ + string, + string, + number, + HashAlgorithm | string, + number, + string +]; + function base64ToArrayBuffer(val: string): ArrayBuffer { var binaryString = atob(val); var bytes = new Uint8Array(binaryString.length); @@ -79,30 +89,56 @@ describe('webcrypto', () => { ); }); - it('PBKDF2 importKey raw/deriveBits', async () => { - const key = await crypto.subtle.importKey( - 'raw', - 'password', - { name: 'PBKDF2' }, - false, - ['deriveBits'] - ); + // PBKDF2 deriveBits() + { + const test = async ( + pass: string, + salt: string, + iterations: number, + hash: HashAlgorithm | string, + length: number, + expected: string + ) => { + const key = await crypto.subtle.importKey( + 'raw', + pass, + { name: 'PBKDF2', hash }, + false, + ['deriveBits'] + ); - const bits = await crypto.subtle.deriveBits( - { - name: 'PBKDF2', - salt: 'salt', - iterations: 1, - hash: { - name: 'SHA-512', + const bits = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt, + iterations, + hash, }, - }, - key, - 512 - ); - const pbkdf2Key = arrayBufferToBase64(bits); - expect(pbkdf2Key).to.equal( - 'hn9wzxreAs/zdSWZo6U9xK80x6ZpgVrl1RNVThyM8lLALUcKKFoFAbrZmb/pQ8CPBQI119aLHaVeY/c7YKV/zg==' - ); - }); + key, + length + ); + const pbkdf2Key = arrayBufferToBase64(bits); + expect(pbkdf2Key).to.equal(expected); + }; + + const kTests: TestFixture[] = [ + [ + 'hello', + 'there', + 10, + 'SHA-256', + 512, + 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7' + + 'ce7678b4cb16fad88098110a83e71f4483ce73203f7a64' + + '719d293280f780f9fafdcf46925c5c0588b3', + ], + ['hello', 'there', 5, 'SHA-384', 128, '201509b012c9cd2fbe7ea938f0c509b3'], + ]; + + kTests.forEach(async ([pass, salt, iterations, hash, length, expected]) => { + it(`PBKDF2 importKey raw/deriveBits - ${pass} ${salt} ${iterations} ${hash} ${length}`, async () => { + await test(pass, salt, iterations, hash, length, expected); + }); + }); + } }); diff --git a/src/Hashnames.ts b/src/Hashnames.ts index 5ffdb284..8f92c1c4 100644 --- a/src/Hashnames.ts +++ b/src/Hashnames.ts @@ -1,3 +1,5 @@ +import type { HashAlgorithm } from './keys'; + export enum HashContext { Node, WebCrypto, @@ -76,3 +78,16 @@ export function normalizeHashName( } catch (_e) {} return name; } + +// TODO: https://github.com/nodejs/node/blob/main/lib/internal/crypto/util.js#L303-L379 maybe? +export const normalizeHash = ( + hash: HashAlgorithm | string | undefined +): HashAlgorithm => { + if (typeof hash === 'string') { + return { name: hash }; + } else if (typeof hash === 'undefined') { + return { name: 'unknown' }; + } else { + return hash; + } +}; diff --git a/src/keys.ts b/src/keys.ts index dacda640..4992a475 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -22,7 +22,7 @@ export type SubtleAlgorithm = { name: 'ECDSA' | 'ECDH' | 'PBKDF2'; salt?: string; iterations?: number; - hash?: HashAlgorithm; + hash?: HashAlgorithm | string; namedCurve?: NamedCurve; length?: number; }; diff --git a/src/pbkdf2.ts b/src/pbkdf2.ts index b03f9542..9a791f59 100644 --- a/src/pbkdf2.ts +++ b/src/pbkdf2.ts @@ -7,6 +7,7 @@ import { bufferLikeToArrayBuffer, normalizeHashName, HashContext, + normalizeHash, } from './Utils'; import type { CryptoKey, SubtleAlgorithm } from './keys'; import { promisify } from 'util'; @@ -130,7 +131,8 @@ export async function pbkdf2DeriveBits( length: number ): Promise { const { iterations, hash, salt } = algorithm; - if (!hash || !hash.name) { + const normalizedHash = normalizeHash(hash); + if (!normalizedHash || !normalizedHash.name) { throw lazyDOMException('hash cannot be blank', 'OperationError'); } if (!iterations || iterations === 0) { @@ -156,7 +158,7 @@ export async function pbkdf2DeriveBits( sanitizedSalt, iterations, length / 8, - hash.name + normalizedHash.name ); if (!result) { throw lazyDOMException(