diff --git a/README.md b/README.md index 19345180..a3d1c5d1 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ - [Adding the scripts](#adding-the-scripts) - [Browser bundle size](#browser-bundle-size) - [Available Packages](#available-packages) - - [Base](#base) + - [Core](#core) - [Plugins](#plugins) - [Crypto](#crypto) - - [Base 32](#base-32) - - [Packages](#packages) + - [Base32](#base32) + - [Presets](#presets) - [Available Options](#available-options) - [Google Authenticator](#google-authenticator) - [Difference between Authenticator and TOTP](#difference-between-authenticator-and-totp) @@ -169,42 +169,53 @@ Paired with the gzipped browser `buffer.js` module, it would be about `7.65KB + ## Available Packages -This library has been split into 3 categories: `base`, `plugins` and `packages`. +This library has been split into 3 categories: `core`, `plugin` and `preset`. -### Base +### Core These provides the main functionality of the library. However parts of the logic has been separated out in order to provide flexibility to the library. -| file | description | -| -------------------- | ---------------------------------- | -| otplib/core | HOTP and TOTP functionality | -| otplib/authenticator | Google Authenticator functionality | +| file | description | +| -------------------- | ---------------------------------------------------- | +| otplib/hotp | HOTP functions + class | +| otplib/hotp | TOTP functions + class | +| otplib/authenticator | Google Authenticator functions + class | +| otplib/core | Aggregates hotp/totp/authenticator functions + class | ### Plugins #### Crypto -| file | description | -| ---------------------- | ------------------------- | -| otplib/crypto/node | node crypto based methods | -| otplib/crypto/cryptojs | crypto-js based methods | +These plugins provide the core digest generation which is used in +token generation. -#### Base 32 +| plugin | npm | +| ---------------------- | ----------------------- | +| otplib/plugin-crypto | crypto module from node | +| otplib/plugin-cryptojs | `npm install crypto-js` | -| file | description | -| -------------------------- | ----------------------------------------------------- | -| otplib/base32/thirty-two | Encoder/Decoder using thirty-two | -| otplib/base32/base32-endec | Encoder/Decoder using base32-encode and base32-decode | +#### Base32 -### Packages +The Base32 functionalities are used when encoding and decoding keys +in the Google Authenticator implementation. -| file | description | -| --------------- | ----------------------------------------------------------- | -| otplib/node | Uses node crypto + thirty-two | -| otplib/cryptojs | Uses crypto-js + thirty-two | -| otplib/browser | Webpack bundle. Uses base32-endec + crypto-js | -| otplib/legacy | Wrapper to adapt the APIs to otplib@v11.x compatible format | +| plugin | npm | +| ---------------------------- | ----------------------------------------- | +| otplib/plugin-thirty-two | `npm install thirty-two` | +| otplib/plugin-base32-enc-dec | `npm install base32-encode base32-decode` | + +### Presets + +Presets are preconfigured HOTP, TOTP, Authenticator instances to allow for a +faster implementations. They would need the corresponding npm module +to be installed (except `preset-browser`). + +| file | description | +| --------------------- | ----------------------------------------------------------- | +| otplib/preset-default | Uses node crypto + thirty-two | +| otplib/preset-browser | Webpack bundle. Uses base32-endec + crypto-js | +| otplib/preset-legacy | Wrapper to adapt the APIs to otplib@v11.x compatible format | ## Available Options diff --git a/configs/builds.js b/configs/builds.js index 6cb6d552..fcfcfc08 100644 --- a/configs/builds.js +++ b/configs/builds.js @@ -2,25 +2,45 @@ const defaultPresetEnv = { targets: 'node 8' }; +const standard = alias => ({ + alias, + external: [], + bundler: 'rollup', + files: ['index.ts'], + format: 'cjs', + presetEnv: defaultPresetEnv +}); + module.exports = { - 'otplib-authenticator': { - alias: 'authenticator', - bundler: 'rollup', - external: [], - files: ['index.ts'], - format: 'cjs', - presetEnv: defaultPresetEnv + // core + 'otplib-authenticator': standard('authenticator'), + 'otplib-core': standard('core'), + 'otplib-hotp': standard('hotp'), + 'otplib-totp': standard('totp'), + + // base32 + 'otplib-plugin-base32-enc-dec': { + ...standard('plugin-base32-enc-dec'), + external: ['base32-encode', 'base32-decode'] }, - 'otplib-base32': { - alias: 'base32', - bundler: 'rollup', - external: ['thirty-two', 'base32-encode', 'base32-decode'], - files: ['base32-endec.ts', 'thirty-two.ts'], - format: 'cjs', - presetEnv: defaultPresetEnv + 'otplib-plugin-thirty-two': { + ...standard('plugin-thirty-two'), + external: ['thirty-two'] }, - 'otplib-browser': { - alias: 'browser', + + // crypto + 'otplib-plugin-crypto': { + ...standard('plugin-crypto'), + external: ['crypto'] + }, + 'otplib-plugin-crypto-js': { + ...standard('plugin-crypto-js'), + external: ['crypto'] + }, + + // presets + 'otplib-preset-browser': { + alias: 'preset-browser', bundler: 'webpack', files: ['index.ts'], format: 'umd', @@ -28,36 +48,10 @@ module.exports = { targets: 'cover 99.5%' } }, - 'otplib-core': { - alias: 'core', - bundler: 'rollup', - external: [], - files: ['index.ts'], - format: 'cjs', - presetEnv: defaultPresetEnv - }, - 'otplib-cryptojs': { - alias: 'cryptojs', - bundler: 'rollup', - external: ['crypto-js'], - files: ['index.ts'], - format: 'cjs', - presetEnv: defaultPresetEnv - }, - 'otplib-legacy': { - alias: 'legacy', - bundler: 'rollup', - external: [], - files: ['index.js'], - format: 'cjs', - presetEnv: defaultPresetEnv - }, - 'otplib-node': { - alias: 'node', - bundler: 'rollup', - external: ['crypto'], - files: ['index.ts'], - format: 'cjs', - presetEnv: defaultPresetEnv + + 'otplib-preset-default': standard('preset-default'), + 'otplib-preset-legacy': { + ...standard('preset-legacy'), + files: ['index.js'] } }; diff --git a/configs/rollup.config.js b/configs/rollup.config.js index 62d49ae8..03c1aa09 100644 --- a/configs/rollup.config.js +++ b/configs/rollup.config.js @@ -36,7 +36,8 @@ function rollupConfig(config, helpers) { ] }), nodeResolve({ - extensions: helpers.EXTENSIONS + extensions: helpers.EXTENSIONS, + preferBuiltins: true }), commonjs({ include: 'node_modules/**' diff --git a/configs/webpack.config.js b/configs/webpack.config.js index 624353ab..0f0e97a1 100644 --- a/configs/webpack.config.js +++ b/configs/webpack.config.js @@ -16,11 +16,6 @@ function webpackConfig(config, helpers) { path: config.buildFolderPath, filename: config.buildFileName }, - // externals: { - // Buffer: { - // root: 'buffer.Buffer' - // } - // }, node: { Buffer: false }, diff --git a/package.json b/package.json index 958d5b33..231f6c08 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "otplib", "version": "11.1.0-0", "description": "HMAC-based (HOTP) and Time-based (TOTP) One-Time Password library", - "main": "./node/index.js", - "typings": "./node/index.d.ts", + "main": "./core/index.js", + "typings": "./core/index.d.ts", "scripts": { "build": "./scripts/build.sh", "build:site": "./scripts/build-site.sh", diff --git a/packages/otplib-authenticator/authenticator.test.ts b/packages/otplib-authenticator/authenticator.test.ts index 36ead0f9..c04a6ca9 100644 --- a/packages/otplib-authenticator/authenticator.test.ts +++ b/packages/otplib-authenticator/authenticator.test.ts @@ -1,12 +1,43 @@ -import { AUTHENTICATOR_DATASET } from 'tests-suites'; -import * as totp from 'otplib-core/totp'; -import { HashAlgorithms } from 'otplib-core'; +import * as totp from 'otplib-totp/totp'; +import { HashAlgorithms } from 'otplib-hotp'; import { AuthenticatorOptions, authenticatorOptionValidator, Authenticator } from './authenticator'; +interface AuthenticatorTestCase { + decoded: string; + digest: string; + secret: string; + epoch: number; + token: string; +} + +export const AUTHENTICATOR_DATASET: AuthenticatorTestCase[] = [ + { + decoded: '68442f372b67474e2f47617679706f6e30756f51', + digest: '422eb1a849cf0650fef4dbdd8b0ee0fe57a87eb9', + epoch: 1565103854545, + secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', + token: '566155' + }, + { + decoded: '68442f372b67474e2f47617679706f6e30756f51', + digest: 'c305b82dbf2a8d2d8a22e9d3992e4e666222d0e2', + secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', + epoch: 1565103878581, + token: '522154' + }, + { + decoded: '636c6c4e506479436f314f6b4852623167564f76', + digest: '64a959e511420af1a406424f87b4412977b3cbd4', + secret: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', + epoch: 1565103903110, + token: '540849' + } +]; + const runOptionValidator = ( opt: Partial ): { error: boolean; message?: string } => { diff --git a/packages/otplib-authenticator/authenticator.ts b/packages/otplib-authenticator/authenticator.ts index b0ee721f..9c56b1a8 100644 --- a/packages/otplib-authenticator/authenticator.ts +++ b/packages/otplib-authenticator/authenticator.ts @@ -2,14 +2,16 @@ import { HashAlgorithms, KeyEncodings, SecretKey, + createInstance +} from 'otplib-hotp'; +import { TOTP, TOTPOptions, totpCheckWithWindow, totpCreateHmacKey, totpOptionsValidator, - totpToken, - createInstance -} from 'otplib-core'; + totpToken +} from 'otplib-totp'; /** * RFC4648 / RFC3548 Base32 String. diff --git a/packages/otplib-base32/base32-endec.test.ts b/packages/otplib-base32/base32-endec.test.ts deleted file mode 100644 index 32e5058f..00000000 --- a/packages/otplib-base32/base32-endec.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { base32TestSuite } from 'tests-suites'; -import * as pkg from './base32-endec'; - -base32TestSuite('base32-endec', pkg); diff --git a/packages/otplib-base32/thirty-two.test.ts b/packages/otplib-base32/thirty-two.test.ts deleted file mode 100644 index 3774f5cc..00000000 --- a/packages/otplib-base32/thirty-two.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { base32TestSuite } from 'tests-suites'; -import * as pkg from './thirty-two'; - -base32TestSuite('thirty-two', pkg); diff --git a/packages/otplib-browser/index.ts b/packages/otplib-browser/index.ts deleted file mode 100644 index b0eb6b7e..00000000 --- a/packages/otplib-browser/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* globals buffer */ -import { - hotp as hotpBase, - totp as totpBase, - authenticator as authenticatorBase -} from 'otplib-cryptojs'; -import { keyDecoder, keyEncoder } from 'otplib-base32/base32-endec'; - -// @ts-ignore -if (typeof window === 'object' && typeof window.Buffer === 'undefined') { - // @ts-ignore - window.Buffer = buffer.Buffer; -} - -export const hotp = hotpBase; -export const totp = totpBase; -export const authenticator = authenticatorBase.clone({ - keyEncoder, - keyDecoder -}); diff --git a/packages/otplib-core/index.ts b/packages/otplib-core/index.ts index 8c40b11c..7afab168 100644 --- a/packages/otplib-core/index.ts +++ b/packages/otplib-core/index.ts @@ -1,3 +1,3 @@ -export * from './hotp'; -export * from './totp'; -export * from './utils'; +export * from 'otplib-hotp'; +export * from 'otplib-totp'; +export * from 'otplib-authenticator'; diff --git a/packages/otplib-cryptojs/index.test.ts b/packages/otplib-cryptojs/index.test.ts deleted file mode 100644 index 661c6fe3..00000000 --- a/packages/otplib-cryptojs/index.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { pkgTestSuite } from 'tests-suites'; -import { keyDecoder, keyEncoder } from 'otplib-base32/base32-endec'; -import * as pkg from './index'; - -const createDigest = pkg.hotp.options.createDigest; - -pkgTestSuite('otplib-cryptojs', { - ...pkg, - authenticator: pkg.authenticator.clone({ keyEncoder, keyDecoder }) -}); - -test('createDigest fails on unsupported encoding format', (): void => { - const fn = (): void => { - // @ts-ignore - createDigest('unknown', 'test', '0'); - }; - expect(fn).toThrow(); -}); diff --git a/packages/otplib-core/hotp.test.ts b/packages/otplib-hotp/hotp.test.ts similarity index 100% rename from packages/otplib-core/hotp.test.ts rename to packages/otplib-hotp/hotp.test.ts index c2ead263..a382d7ed 100644 --- a/packages/otplib-core/hotp.test.ts +++ b/packages/otplib-hotp/hotp.test.ts @@ -1,6 +1,6 @@ import { secret } from 'tests-data/rfc4226'; -import { HOTPOptions, hotpOptionsValidator, HOTP } from './hotp'; import { HashAlgorithms } from './utils'; +import { HOTPOptions, hotpOptionsValidator, HOTP } from './hotp'; interface HOTPCheckTestCase { token: string; diff --git a/packages/otplib-core/hotp.ts b/packages/otplib-hotp/hotp.ts similarity index 100% rename from packages/otplib-core/hotp.ts rename to packages/otplib-hotp/hotp.ts diff --git a/packages/otplib-hotp/index.ts b/packages/otplib-hotp/index.ts new file mode 100644 index 00000000..f411978c --- /dev/null +++ b/packages/otplib-hotp/index.ts @@ -0,0 +1,2 @@ +export * from './hotp'; +export * from './utils'; diff --git a/packages/otplib-core/utils.test.ts b/packages/otplib-hotp/utils.test.ts similarity index 100% rename from packages/otplib-core/utils.test.ts rename to packages/otplib-hotp/utils.test.ts diff --git a/packages/otplib-core/utils.ts b/packages/otplib-hotp/utils.ts similarity index 100% rename from packages/otplib-core/utils.ts rename to packages/otplib-hotp/utils.ts diff --git a/packages/otplib-legacy/index.js b/packages/otplib-legacy/index.js deleted file mode 100644 index 870dc7f1..00000000 --- a/packages/otplib-legacy/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import { authenticator as base } from 'otplib-node'; -import { keyEncoder, keyDecoder } from 'otplib-base32/thirty-two'; -import { HOTP, TOTP, Authenticator } from './legacy'; - -const { createDigest, createRandomBytes } = base.options; - -export const hotp = new HOTP({ - createDigest -}); - -export const totp = new TOTP({ - createDigest -}); - -export const authenticator = new Authenticator({ - createDigest, - createRandomBytes, - keyEncoder, - keyDecoder -}); diff --git a/packages/otplib-node/index.test.ts b/packages/otplib-node/index.test.ts deleted file mode 100644 index cc637d33..00000000 --- a/packages/otplib-node/index.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { pkgTestSuite } from 'tests-suites'; -import { keyDecoder, keyEncoder } from 'otplib-base32/base32-endec'; -import * as pkg from './index'; - -pkgTestSuite('otplib-node', { - ...pkg, - authenticator: pkg.authenticator.clone({ keyEncoder, keyDecoder }) -}); diff --git a/packages/otplib-node/index.ts b/packages/otplib-node/index.ts deleted file mode 100644 index a8fa8225..00000000 --- a/packages/otplib-node/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import crypto from 'crypto'; -import { - HashAlgorithms, - CreateDigest, - HOTP, - HexString, - KeyEncodings, - TOTP -} from 'otplib-core'; -import { Authenticator, CreateRandomBytes } from 'otplib-authenticator'; - -const createDigest: CreateDigest = ( - algorithm: HashAlgorithms, - hmacKey: HexString, - counter: HexString -): HexString => { - const hmac = crypto.createHmac(algorithm, Buffer.from(hmacKey, 'hex')); - const digest = hmac.update(Buffer.from(counter, 'hex')).digest(); - return digest.toString('hex'); -}; - -const createRandomBytes: CreateRandomBytes = ( - size: number, - encoding: KeyEncodings -): string => { - return crypto.randomBytes(size).toString(encoding); -}; - -/** - * A HOTP instance. - * - * Initialised with package-specific implementation of: - * - * - createDigest - */ -export const hotp = new HOTP({ - createDigest -}); - -/** - * A TOTP instance. - * - * Initialised with package-specific implementation of: - * - * - createDigest - */ -export const totp = new TOTP({ - createDigest -}); - -/** - * An Authenticator instance. - * - * Initialised with package-specific implementation of: - * - * - createDigest - * - createRandomBytes - */ -export const authenticator = new Authenticator({ - createDigest, - createRandomBytes -}); diff --git a/packages/otplib-plugin-base32-enc-dec/index.test.ts b/packages/otplib-plugin-base32-enc-dec/index.test.ts new file mode 100644 index 00000000..eecf8ed2 --- /dev/null +++ b/packages/otplib-plugin-base32-enc-dec/index.test.ts @@ -0,0 +1,4 @@ +import { base32PluginTestSuite } from 'tests-suites'; +import * as plugin from './index'; + +base32PluginTestSuite('plugin-base32-enc-dec', plugin); diff --git a/packages/otplib-base32/base32-endec.ts b/packages/otplib-plugin-base32-enc-dec/index.ts similarity index 100% rename from packages/otplib-base32/base32-endec.ts rename to packages/otplib-plugin-base32-enc-dec/index.ts diff --git a/packages/otplib-plugin-crypto-js/index.test.ts b/packages/otplib-plugin-crypto-js/index.test.ts new file mode 100644 index 00000000..6fe0d4d5 --- /dev/null +++ b/packages/otplib-plugin-crypto-js/index.test.ts @@ -0,0 +1,4 @@ +import { cryptoPluginTestSuite } from 'tests-suites/plugin-crypto'; +import * as plugin from './index'; + +cryptoPluginTestSuite('plugin-crypto-js', plugin); diff --git a/packages/otplib-cryptojs/index.ts b/packages/otplib-plugin-crypto-js/index.ts similarity index 79% rename from packages/otplib-cryptojs/index.ts rename to packages/otplib-plugin-crypto-js/index.ts index b1ea2438..61fea0cf 100644 --- a/packages/otplib-cryptojs/index.ts +++ b/packages/otplib-plugin-crypto-js/index.ts @@ -6,14 +6,12 @@ import SHA512 from 'crypto-js/hmac-sha512'; import Hex from 'crypto-js/enc-hex'; import { CreateDigest, - HOTP, HashAlgorithms, HexString, KeyEncodings, - TOTP, objectValues } from 'otplib-core'; -import { Authenticator, CreateRandomBytes } from 'otplib-authenticator'; +import { CreateRandomBytes } from 'otplib-authenticator'; const HASH_ALGORITHMS = objectValues(HashAlgorithms); const { WordArray } = cryptoJsCore.lib; @@ -37,7 +35,7 @@ function cryptoEncoder( } } -const createDigest: CreateDigest = ( +export const createDigest: CreateDigest = ( algorithm: HashAlgorithms, hmacKey: HexString, counter: HexString @@ -48,23 +46,10 @@ const createDigest: CreateDigest = ( return String(encoder(message, secret)); }; -const createRandomBytes: CreateRandomBytes = ( +export const createRandomBytes: CreateRandomBytes = ( numberOfBytes: number, encoding: KeyEncodings ): string => { const words = WordArray.random(numberOfBytes); return Buffer.from(words.toString(), 'hex').toString(encoding); }; - -export const hotp = new HOTP({ - createDigest -}); - -export const totp = new TOTP({ - createDigest -}); - -export const authenticator = new Authenticator({ - createDigest, - createRandomBytes -}); diff --git a/packages/otplib-plugin-crypto/index.test.ts b/packages/otplib-plugin-crypto/index.test.ts new file mode 100644 index 00000000..bc62b88f --- /dev/null +++ b/packages/otplib-plugin-crypto/index.test.ts @@ -0,0 +1,4 @@ +import { cryptoPluginTestSuite } from 'tests-suites/plugin-crypto'; +import * as plugin from './index'; + +cryptoPluginTestSuite('plugin-crypto', plugin); diff --git a/packages/otplib-plugin-crypto/index.ts b/packages/otplib-plugin-crypto/index.ts new file mode 100644 index 00000000..b1a97789 --- /dev/null +++ b/packages/otplib-plugin-crypto/index.ts @@ -0,0 +1,25 @@ +import crypto from 'crypto'; +import { + KeyEncodings, + HashAlgorithms, + HexString, + CreateDigest +} from 'otplib-hotp'; +import { CreateRandomBytes } from 'otplib-authenticator'; + +export const createDigest: CreateDigest = ( + algorithm: HashAlgorithms, + hmacKey: HexString, + counter: HexString +): HexString => { + const hmac = crypto.createHmac(algorithm, Buffer.from(hmacKey, 'hex')); + const digest = hmac.update(Buffer.from(counter, 'hex')).digest(); + return digest.toString('hex'); +}; + +export const createRandomBytes: CreateRandomBytes = ( + size: number, + encoding: KeyEncodings +): string => { + return crypto.randomBytes(size).toString(encoding); +}; diff --git a/packages/otplib-plugin-thirty-two/index.test.ts b/packages/otplib-plugin-thirty-two/index.test.ts new file mode 100644 index 00000000..ead989aa --- /dev/null +++ b/packages/otplib-plugin-thirty-two/index.test.ts @@ -0,0 +1,4 @@ +import { base32PluginTestSuite } from 'tests-suites'; +import * as plugin from './index'; + +base32PluginTestSuite('plugin-thirty-two', plugin); diff --git a/packages/otplib-base32/thirty-two.ts b/packages/otplib-plugin-thirty-two/index.ts similarity index 100% rename from packages/otplib-base32/thirty-two.ts rename to packages/otplib-plugin-thirty-two/index.ts diff --git a/packages/otplib-preset-browser/index.test.ts b/packages/otplib-preset-browser/index.test.ts new file mode 100644 index 00000000..35f9ffcc --- /dev/null +++ b/packages/otplib-preset-browser/index.test.ts @@ -0,0 +1,4 @@ +import { presetTestSuite } from 'tests-suites'; +import * as preset from './index'; + +presetTestSuite('preset-browser', preset); diff --git a/packages/otplib-preset-browser/index.ts b/packages/otplib-preset-browser/index.ts new file mode 100644 index 00000000..fc625201 --- /dev/null +++ b/packages/otplib-preset-browser/index.ts @@ -0,0 +1,25 @@ +/* globals buffer */ +import { createDigest, createRandomBytes } from 'otplib-plugin-crypto-js'; +import { keyDecoder, keyEncoder } from 'otplib-plugin-base32-enc-dec'; +import { HOTP, TOTP, Authenticator } from 'otplib-core'; + +// @ts-ignore +if (typeof window === 'object' && typeof window.Buffer === 'undefined') { + // @ts-ignore + window.Buffer = buffer.Buffer; +} + +export const hotp = new HOTP({ + createDigest +}); + +export const totp = new TOTP({ + createDigest +}); + +export const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyDecoder, + keyEncoder +}); diff --git a/packages/otplib-preset-default/index.test.ts b/packages/otplib-preset-default/index.test.ts new file mode 100644 index 00000000..543840a6 --- /dev/null +++ b/packages/otplib-preset-default/index.test.ts @@ -0,0 +1,4 @@ +import { presetTestSuite } from 'tests-suites'; +import * as preset from './index'; + +presetTestSuite('otplib-node', preset); diff --git a/packages/otplib-preset-default/index.ts b/packages/otplib-preset-default/index.ts new file mode 100644 index 00000000..82cc9f7e --- /dev/null +++ b/packages/otplib-preset-default/index.ts @@ -0,0 +1,18 @@ +import { createDigest, createRandomBytes } from 'otplib-plugin-crypto'; +import { keyDecoder, keyEncoder } from 'otplib-plugin-thirty-two'; +import { HOTP, TOTP, Authenticator } from 'otplib-core'; + +export const hotp = new HOTP({ + createDigest +}); + +export const totp = new TOTP({ + createDigest +}); + +export const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyDecoder, + keyEncoder +}); diff --git a/packages/otplib-preset-legacy/index.js b/packages/otplib-preset-legacy/index.js new file mode 100644 index 00000000..a8fc4a87 --- /dev/null +++ b/packages/otplib-preset-legacy/index.js @@ -0,0 +1,18 @@ +import { createDigest, createRandomBytes } from 'otplib-plugin-crypto'; +import { keyEncoder, keyDecoder } from 'otplib-plugin-thirty-two'; +import { HOTP, TOTP, Authenticator } from './v11'; + +export const hotp = new HOTP({ + createDigest +}); + +export const totp = new TOTP({ + createDigest +}); + +export const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyEncoder, + keyDecoder +}); diff --git a/packages/otplib-legacy/index.test.js b/packages/otplib-preset-legacy/index.test.js similarity index 92% rename from packages/otplib-legacy/index.test.js rename to packages/otplib-preset-legacy/index.test.js index c45734f6..2409e14c 100644 --- a/packages/otplib-legacy/index.test.js +++ b/packages/otplib-preset-legacy/index.test.js @@ -1,8 +1,7 @@ import * as rfc4226 from 'tests-data/rfc4226'; import * as rfc6238 from 'tests-data/rfc6238'; -import { AUTHENTICATOR_DATASET } from 'tests-suites'; import { hotp, totp, authenticator } from './index'; -import { HOTP, TOTP, Authenticator } from './legacy'; +import { HOTP, TOTP, Authenticator } from './v11'; let originalConsoleWarn; @@ -190,7 +189,23 @@ describe('TOTP - RFC 6238', () => { }); describe('Authenticator', () => { - AUTHENTICATOR_DATASET.forEach(entry => { + [ + { + epoch: 1565103854545, + secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', + token: '566155' + }, + { + secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', + epoch: 1565103878581, + token: '522154' + }, + { + secret: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', + epoch: 1565103903110, + token: '540849' + } + ].forEach(entry => { test(`should return expected token - ${entry.token}`, () => { const instance = authenticator.clone(); instance.options = { epoch: entry.epoch }; diff --git a/packages/otplib-legacy/legacy.js b/packages/otplib-preset-legacy/v11.js similarity index 100% rename from packages/otplib-legacy/legacy.js rename to packages/otplib-preset-legacy/v11.js diff --git a/packages/otplib-legacy/legacy.test.js b/packages/otplib-preset-legacy/v11.test.js similarity index 93% rename from packages/otplib-legacy/legacy.test.js rename to packages/otplib-preset-legacy/v11.test.js index b93e4ee5..3ffead8f 100644 --- a/packages/otplib-legacy/legacy.test.js +++ b/packages/otplib-preset-legacy/v11.test.js @@ -1,4 +1,4 @@ -import { epochUnixToJS, epochJSToUnix } from './legacy'; +import { epochUnixToJS, epochJSToUnix } from './v11'; test('should return empty object if argument is non-object', () => { expect(epochUnixToJS()).toEqual({}); diff --git a/packages/otplib-totp/index.ts b/packages/otplib-totp/index.ts new file mode 100644 index 00000000..e94d5b05 --- /dev/null +++ b/packages/otplib-totp/index.ts @@ -0,0 +1 @@ +export * from './totp'; diff --git a/packages/otplib-core/totp.test.ts b/packages/otplib-totp/totp.test.ts similarity index 98% rename from packages/otplib-core/totp.test.ts rename to packages/otplib-totp/totp.test.ts index 69392493..59b34b28 100644 --- a/packages/otplib-core/totp.test.ts +++ b/packages/otplib-totp/totp.test.ts @@ -1,4 +1,5 @@ -import * as hotp from './hotp'; +import { KeyEncodings, HashAlgorithms } from 'otplib-hotp'; +import * as hotp from 'otplib-hotp/hotp'; import { TOTPOptions, totpOptionsValidator, @@ -6,7 +7,6 @@ import { totpCreateHmacKey, totpCheckWithWindow } from './totp'; -import { KeyEncodings, HashAlgorithms } from './utils'; interface TOTPCheckTestCase { delta: number; diff --git a/packages/otplib-core/totp.ts b/packages/otplib-totp/totp.ts similarity index 99% rename from packages/otplib-core/totp.ts rename to packages/otplib-totp/totp.ts index baf31e20..980ab92b 100644 --- a/packages/otplib-core/totp.ts +++ b/packages/otplib-totp/totp.ts @@ -2,20 +2,18 @@ import { CreateHmacKey, HOTP, HOTPOptions, - hotpOptionsValidator, - hotpToken, - createInstance -} from './hotp'; -import { HashAlgorithms, HexString, KeyEncodings, SecretKey, Strategy, + createInstance, + hotpOptionsValidator, + hotpToken, isTokenValid, keyuri, objectValues -} from './utils'; +} from 'otplib-hotp'; const HASH_ALGORITHMS = objectValues(HashAlgorithms); diff --git a/packages/tests-builds/browser.test.js b/packages/tests-builds/browser.test.js index 39399137..ef151046 100644 --- a/packages/tests-builds/browser.test.js +++ b/packages/tests-builds/browser.test.js @@ -1,24 +1,4 @@ -import { AUTHENTICATOR_DATASET } from 'tests-suites'; -import * as otplibImport from '../../builds/otplib/browser'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const otplibRequire = require('../../builds/otplib/browser'); +import { presetTestSuite } from 'tests-suites'; +import * as otplib from '../../builds/otplib/preset-browser'; -describe('builds - browser (require)', () => { - AUTHENTICATOR_DATASET.forEach(entry => { - test(`should return expected token - ${entry.token}`, () => { - const instance = otplibRequire.authenticator.clone(); - instance.options = { epoch: entry.epoch }; - expect(instance.generate(entry.secret)).toBe(entry.token); - }); - }); -}); - -describe('builds - browser (import)', () => { - AUTHENTICATOR_DATASET.forEach(entry => { - test(`should return expected token - ${entry.token}`, () => { - const instance = otplibImport.authenticator.clone(); - instance.options = { epoch: entry.epoch }; - expect(instance.generate(entry.secret)).toBe(entry.token); - }); - }); -}); +presetTestSuite('[builds] preset-browser', otplib); diff --git a/packages/tests-builds/node.test.js b/packages/tests-builds/node.test.js deleted file mode 100644 index 7ca11612..00000000 --- a/packages/tests-builds/node.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { AUTHENTICATOR_DATASET } from 'tests-suites'; -import { authenticator } from '../../builds/otplib/node'; -import * as endec from '../../builds/otplib/base32/base32-endec'; -import * as thirtyTwo from '../../builds/otplib/base32/thirty-two'; - -describe('builds - node', () => { - AUTHENTICATOR_DATASET.forEach(entry => { - test(`(thirtyTwo) should return expected token - ${entry.token}`, () => { - const instance = authenticator.clone(thirtyTwo); - instance.options = { epoch: entry.epoch }; - expect(instance.generate(entry.secret)).toBe(entry.token); - }); - }); - - AUTHENTICATOR_DATASET.forEach(entry => { - test(`(base32-endec) should return expected token - ${entry.token}`, () => { - const instance = authenticator.clone(endec); - instance.options = { epoch: entry.epoch }; - expect(instance.generate(entry.secret)).toBe(entry.token); - }); - }); -}); diff --git a/packages/tests-suites/endec.ts b/packages/tests-suites/endec.ts deleted file mode 100644 index 9e26bf97..00000000 --- a/packages/tests-suites/endec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { KeyDecoder, KeyEncoder } from 'otplib-authenticator'; -import { KeyEncodings } from 'otplib-core'; -import { authenticator } from 'otplib-node'; - -interface TestKeys { - encoded: string; - decoded: string; -} - -interface TestPkg { - keyDecoder: KeyDecoder; - keyEncoder: KeyEncoder; -} - -const testKeys: TestKeys[] = [ - { - encoded: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', - decoded: '68442f372b67474e2f47617679706f6e30756f51' - }, - { - encoded: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', - decoded: '636c6c4e506479436f314f6b4852623167564f76' - }, - { - encoded: 'IFCWS4SRN5FEOUJTOJRXAUKBKRVTA4SB', - decoded: '41456972516f4a4751337263705141546b307241' - }, - { - encoded: 'JFYFCSJSJNMXCOJTGJGVISDMNY3VEV2M', - decoded: '49705149324b59713933324d54486c6e3752574c' - }, - { - encoded: 'JJQXGMCDOI2HS6KTF44E66KQPBRHQOLO', - decoded: '4a6173304372347979532f384f7950786278396e' - } -]; - -interface AuthenticatorSuiteTestCase { - epoch: number; - secret: string; - token: string; -} - -const tokenSets: AuthenticatorSuiteTestCase[] = [ - { - epoch: 1565103854545, - secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', - token: '566155' - }, - { - epoch: 1565103903110, - secret: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', - token: '540849' - }, - { - epoch: 1565106003151, - secret: 'IM4G6QTIONHS63SRKRBEU4LEIRSTIQTM', - token: '668733' - }, - { - epoch: 1565106018408, - secret: 'PIZGURTBIZ2EU4SNGFKHE5LXKVEFA6CM', - token: '767234' - }, - { - epoch: 1565106407848, - secret: 'LA2XU3LZO53SWWJVKFVFU3TGIQZEU3SF', - token: '942732' - }, - { - epoch: 1565106431089, - secret: 'PE4U4RBVMJFE6V2VOBEGINLKMJ3G4LZZ', - token: '235413' - }, - { - epoch: 1565106483557, - secret: 'I5JUGURXO5TTGVRUGBBUGU2TNZMVSVTP', - token: '508543' - } -]; - -export function base32TestSuite(name: string, opt: TestPkg): void { - describe(`${name}`, (): void => { - testKeys.forEach((entry, idx): void => { - test(`key #${idx} - decode`, (): void => { - expect(opt.keyDecoder(entry.encoded, KeyEncodings.HEX)).toBe( - entry.decoded - ); - }); - - test(`key #${idx} - encode`, (): void => { - expect(opt.keyEncoder(entry.decoded, KeyEncodings.HEX)).toBe( - entry.encoded - ); - }); - }); - }); - - describe(`otplib-node + ${name}`, (): void => { - tokenSets.forEach((entry): void => { - test(`epoch ${entry.epoch}`, (): void => { - authenticator.options = { - epoch: entry.epoch, - keyDecoder: opt.keyDecoder - }; - - expect(authenticator.generate(entry.secret)).toBe(entry.token); - }); - }); - }); -} diff --git a/packages/tests-suites/index.ts b/packages/tests-suites/index.ts index 3eb47d4d..a815539a 100644 --- a/packages/tests-suites/index.ts +++ b/packages/tests-suites/index.ts @@ -1,71 +1,2 @@ -import { HOTP, TOTP, KeyEncodings } from 'otplib-core'; -import { Authenticator } from 'otplib-authenticator'; -import { issuesTestSuite } from './issues'; -import { hotpTestSuite, totpTestSuite } from './rfcs'; - -export { base32TestSuite } from './endec'; - -interface TestPkg { - hotp: HOTP; - totp: TOTP; - authenticator: Authenticator; -} - -export function pkgTestSuite(name: string, pkg: TestPkg): void { - hotpTestSuite(name, { - hotp: pkg.hotp - }); - - totpTestSuite(name, { - totp: pkg.totp - }); - - issuesTestSuite(name, { - authenticator: pkg.authenticator - }); - - describe('createRandomBytes', (): void => { - const sizes: number[] = [20, 30, 60]; - const { createRandomBytes } = pkg.authenticator.allOptions(); - - sizes.forEach((size): void => { - const hexSize = (size * 8) / 4; - test(`byte ${size}, hex size: ${hexSize}`, (): void => { - const result = createRandomBytes(size, KeyEncodings.HEX); - expect(result.length).toBe(hexSize); - }); - }); - }); -} - -interface AuthenticatorTestCase { - decoded: string; - digest: string; - secret: string; - epoch: number; - token: string; -} - -export const AUTHENTICATOR_DATASET: AuthenticatorTestCase[] = [ - { - decoded: '68442f372b67474e2f47617679706f6e30756f51', - digest: '422eb1a849cf0650fef4dbdd8b0ee0fe57a87eb9', - epoch: 1565103854545, - secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', - token: '566155' - }, - { - decoded: '68442f372b67474e2f47617679706f6e30756f51', - digest: 'c305b82dbf2a8d2d8a22e9d3992e4e666222d0e2', - secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', - epoch: 1565103878581, - token: '522154' - }, - { - decoded: '636c6c4e506479436f314f6b4852623167564f76', - digest: '64a959e511420af1a406424f87b4412977b3cbd4', - secret: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', - epoch: 1565103903110, - token: '540849' - } -]; +export * from './preset'; +export * from './plugin-base32'; diff --git a/packages/tests-suites/issues.ts b/packages/tests-suites/issues.ts index 419a0c81..b49ee7a4 100644 --- a/packages/tests-suites/issues.ts +++ b/packages/tests-suites/issues.ts @@ -1,5 +1,5 @@ import { Authenticator } from 'otplib-authenticator'; -import { keyDecoder, keyEncoder } from 'otplib-base32/thirty-two'; +import { keyDecoder, keyEncoder } from 'otplib-plugin-thirty-two'; export interface IssuesTestSuiteOptions { authenticator: Authenticator; diff --git a/packages/tests-suites/plugin-base32.ts b/packages/tests-suites/plugin-base32.ts new file mode 100644 index 00000000..09851e05 --- /dev/null +++ b/packages/tests-suites/plugin-base32.ts @@ -0,0 +1,55 @@ +import { KeyEncodings, KeyDecoder, KeyEncoder } from 'otplib-core'; + +interface TestKeys { + encoded: string; + decoded: string; +} + +interface Base32Plugin { + keyDecoder: KeyDecoder; + keyEncoder: KeyEncoder; +} + +const testKeys: TestKeys[] = [ + { + encoded: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', + decoded: '68442f372b67474e2f47617679706f6e30756f51' + }, + { + encoded: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', + decoded: '636c6c4e506479436f314f6b4852623167564f76' + }, + { + encoded: 'IFCWS4SRN5FEOUJTOJRXAUKBKRVTA4SB', + decoded: '41456972516f4a4751337263705141546b307241' + }, + { + encoded: 'JFYFCSJSJNMXCOJTGJGVISDMNY3VEV2M', + decoded: '49705149324b59713933324d54486c6e3752574c' + }, + { + encoded: 'JJQXGMCDOI2HS6KTF44E66KQPBRHQOLO', + decoded: '4a6173304372347979532f384f7950786278396e' + } +]; + +export function base32PluginTestSuite( + name: string, + plugin: Base32Plugin +): void { + describe(`${name}`, (): void => { + testKeys.forEach((entry): void => { + test(`given encoded key ${entry.encoded}, should receive decoded key ${entry.decoded}`, (): void => { + expect(plugin.keyDecoder(entry.encoded, KeyEncodings.HEX)).toBe( + entry.decoded + ); + }); + + test(`given decoded key ${entry.decoded}, should receive encoded key ${entry.encoded}`, (): void => { + expect(plugin.keyEncoder(entry.decoded, KeyEncodings.HEX)).toBe( + entry.encoded + ); + }); + }); + }); +} diff --git a/packages/tests-suites/plugin-crypto.ts b/packages/tests-suites/plugin-crypto.ts new file mode 100644 index 00000000..0244aba2 --- /dev/null +++ b/packages/tests-suites/plugin-crypto.ts @@ -0,0 +1,42 @@ +import * as rfc4226 from 'tests-data/rfc4226'; +import { + KeyEncodings, + CreateRandomBytes, + CreateDigest, + hotpCreateHmacKey, + hotpCounter, + HashAlgorithms +} from 'otplib-core'; + +const { secret, digests } = rfc4226; + +interface CryptoPlugin { + createDigest: CreateDigest; + createRandomBytes: CreateRandomBytes; +} + +export function cryptoPluginTestSuite( + name: string, + plugin: CryptoPlugin +): void { + describe(`${name}`, (): void => { + digests.forEach((digest: string, counter: number): void => { + test(`given counter (${counter}), should recieve expected digest`, (): void => { + const result = plugin.createDigest( + HashAlgorithms.SHA1, + hotpCreateHmacKey(HashAlgorithms.SHA1, secret, KeyEncodings.ASCII), + hotpCounter(counter) + ); + + expect(result).toBe(digest); + }); + }); + + test('should create random bytes of expected length', (): void => { + // 10 bytes * 8 = 80 bits + // 80 / 4 = 20 for hex encoded; + const result = plugin.createRandomBytes(10, KeyEncodings.HEX); + expect(result.length).toBe(20); + }); + }); +} diff --git a/packages/tests-suites/preset.ts b/packages/tests-suites/preset.ts new file mode 100644 index 00000000..25f32217 --- /dev/null +++ b/packages/tests-suites/preset.ts @@ -0,0 +1,94 @@ +import { Authenticator, TOTP, HOTP, KeyEncodings } from 'otplib-core'; +import { hotpTestSuite, totpTestSuite } from './rfcs'; +import { issuesTestSuite } from './issues'; + +interface Presets { + hotp: HOTP; + totp: TOTP; + authenticator: Authenticator; +} + +interface AuthenticatorSuiteTestCase { + epoch: number; + secret: string; + token: string; +} + +const tokenSets: AuthenticatorSuiteTestCase[] = [ + { + epoch: 1565103854545, + secret: 'NBCC6NZLM5DU4L2HMF3HS4DPNYYHK32R', + token: '566155' + }, + { + epoch: 1565103903110, + secret: 'MNWGYTSQMR4UG3ZRJ5VUQUTCGFTVMT3W', + token: '540849' + }, + { + epoch: 1565106003151, + secret: 'IM4G6QTIONHS63SRKRBEU4LEIRSTIQTM', + token: '668733' + }, + { + epoch: 1565106018408, + secret: 'PIZGURTBIZ2EU4SNGFKHE5LXKVEFA6CM', + token: '767234' + }, + { + epoch: 1565106407848, + secret: 'LA2XU3LZO53SWWJVKFVFU3TGIQZEU3SF', + token: '942732' + }, + { + epoch: 1565106431089, + secret: 'PE4U4RBVMJFE6V2VOBEGINLKMJ3G4LZZ', + token: '235413' + }, + { + epoch: 1565106483557, + secret: 'I5JUGURXO5TTGVRUGBBUGU2TNZMVSVTP', + token: '508543' + } +]; + +export function presetTestSuite(name: string, pkg: Presets): void { + hotpTestSuite(name, { + hotp: pkg.hotp + }); + + totpTestSuite(name, { + totp: pkg.totp + }); + + issuesTestSuite(name, { + authenticator: pkg.authenticator + }); + + describe(`${name} - Authenticator`, (): void => { + const { authenticator } = pkg; + + tokenSets.forEach((entry): void => { + test(`given epoch (${entry.epoch}) and secret, should receive expected token ${entry.token}`, (): void => { + authenticator.options = { + epoch: entry.epoch + }; + + expect(authenticator.generate(entry.secret)).toBe(entry.token); + }); + }); + }); + + describe('createRandomBytes', (): void => { + const sizes: number[] = [20, 30, 60]; + const { createRandomBytes } = pkg.authenticator.allOptions(); + + sizes.forEach((size): void => { + const hexSize = (size * 8) / 4; + test(`byte ${size}, hex size: ${hexSize}`, (): void => { + const result = createRandomBytes(size, KeyEncodings.HEX); + expect(result.length).toBe(hexSize); + }); + }); + }); +} diff --git a/packages/tests-suites/rfcs.ts b/packages/tests-suites/rfcs.ts index 629a94b0..a02cdaf5 100644 --- a/packages/tests-suites/rfcs.ts +++ b/packages/tests-suites/rfcs.ts @@ -22,13 +22,13 @@ export function hotpTestSuite(name: string, opt: HOTPTestSuiteOptions): void { describe(`(${name}) RFC4226`, (): void => { tokens.forEach((token: string, counter: number): void => { - test(`token verification - ${counter}`, (): void => { + test(`given counter (${counter}) and secret, expect token to be (${token}) `, (): void => { expect(hotp.check(token, secret, counter)).toBe(true); }); }); digests.forEach((digest: string, counter: number): void => { - test(`expected intermediate HMAC value - ${counter}`, (): void => { + test(`given counter (${counter}), should recieve expected digest`, (): void => { const result = createDigest( HashAlgorithms.SHA1, createHmacKey(HashAlgorithms.SHA1, secret, KeyEncodings.ASCII), @@ -51,21 +51,21 @@ export function totpTestSuite(name: string, opt: TOTPTestSuiteOptions): void { function runTable(fn: (id: string, row: rfc6238.RowData) => void): void { table.forEach((row: rfc6238.RowData): void => { - const id = `${row.algorithm} / ${row.epoch}`; + const id = `algorithm (${row.algorithm}) and epoch (${row.epoch})`; fn(id, row); }); } describe(`(${name}) RFC6238`, (): void => { runTable((id, row): void => { - test(`expected counter value - ${id}`, (): void => { + test(`given ${id}, should receive expected counter`, (): void => { const counter = hotpCounter(totpCounter(row.epoch * 1000, step)); expect(counter.toUpperCase()).toBe(row.counter); }); }); runTable((id, row): void => { - test(`token verification - ${id}`, (): void => { + test(`given ${id}, should receive token (${row.token})`, (): void => { totp.options = { epoch: row.epoch * 1000, algorithm: row.algorithm, diff --git a/scripts/build.sh b/scripts/build.sh index bcb94013..14545140 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -23,7 +23,7 @@ NODE_ENV=production npx webpack \ echo "\n--- downloading buffer module ---" curl https://bundle.run/buffer@5.3.0 \ - --output ./builds/otplib/browser/buffer.js + --output ./builds/otplib/preset-browser/buffer.js echo "\n--- copying meta ---" cp ./README.md ./builds/otplib/README.md