Skip to content

Commit

Permalink
feat: support hmac (#50)
Browse files Browse the repository at this point in the history
* feat: support hmac

* fix: updated doc

* fix: typo

* fix: add comment

Co-authored-by: Manuel Spigolon <[email protected]>

* fix: improved doc

---------

Co-authored-by: Manuel Spigolon <[email protected]>
  • Loading branch information
marco-ippolito and Eomm authored Mar 9, 2023
1 parent 41e86f8 commit ba9dde4
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 13 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ bytes, not the length of the base-64 string. Defaults to `18` bytes.
Require user-specific information in `tokens.create()` and
`tokens.verify()`.

##### hmacKey

When set, the `hmacKey` is used to generate the cryptographic HMAC hash instead of the default hash function.

##### validity

The maximum validity of the token to generate, in milliseconds. Note that the epoch is
Expand Down
43 changes: 30 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,24 @@ function Tokens (options) {
throw new TypeError('option userInfo must be a boolean')
}

const hmacKey = opts.hmacKey

if (hmacKey) {
try {
// validate if the hmacKey is a valid format
hashingStrategy(algorithm, hmacKey)
} catch (err) {
throw new TypeError('option hmacKey must be a supported hmac key')
}
}

this.algorithm = algorithm
this.saltLength = saltLength
this.saltGenerator = saltGenerator(saltLength)
this.secretLength = secretLength
this.validity = validity
this.userInfo = userInfo
this.hmacKey = hmacKey
}

/**
Expand Down Expand Up @@ -206,19 +218,19 @@ Tokens.prototype._tokenize = Buffer.isEncoding('base64url')
}

if (typeof userInfo === 'string') {
toHash += crypto
.createHash(algorithm)
.update(userInfo)
.digest('base64url')
.replace(MINUS_GLOBAL_REGEXP, '_') + '-'
toHash +=
hashingStrategy(algorithm, this.hmacKey)
.update(userInfo)
.digest('base64url')
.replace(MINUS_GLOBAL_REGEXP, '_') + '-'
}

toHash += salt

return toHash + '-' + crypto
.createHash(algorithm)
.update(toHash + '-' + secret, 'ascii')
.digest('base64url')
return toHash + '-' +
hashingStrategy(algorithm, this.hmacKey)
.update(toHash + '-' + secret, 'ascii')
.digest('base64url')
}
: function _tokenize (secret, salt, date, userInfo, algorithm) {
let toHash = ''
Expand All @@ -228,8 +240,7 @@ Tokens.prototype._tokenize = Buffer.isEncoding('base64url')
}

if (typeof userInfo === 'string') {
toHash += crypto
.createHash(algorithm)
toHash += hashingStrategy(algorithm, this.hmacKey)
.update(userInfo)
.digest('base64')
.replace(PLUS_SLASH_GLOBAL_REGEXP, '_')
Expand All @@ -238,8 +249,7 @@ Tokens.prototype._tokenize = Buffer.isEncoding('base64url')

toHash += salt

return toHash + '-' + crypto
.createHash(algorithm)
return toHash + '-' + hashingStrategy(algorithm, this.hmacKey)
.update(toHash + '-' + secret, 'ascii')
.digest('base64')
.replace(PLUS_GLOBAL_REGEXP, '-')
Expand Down Expand Up @@ -337,6 +347,13 @@ function saltGenerator (saltLength) {
return new Function(fnBody.join(''))() // eslint-disable-line no-new-func
}

function hashingStrategy (algorithm, key) {
if (key) {
return crypto.createHmac(algorithm, key)
}
return crypto.createHash(algorithm)
}

module.exports = Tokens
module.exports.default = Tokens
module.exports.Tokens = Tokens
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"test:typescript": "tsd"
},
"devDependencies": {
"@types/node": "^18.14.6",
"beautify-benchmark": "^0.2.4",
"benchmark": "^2.1.4",
"standard": "^17.0.0",
Expand Down
63 changes: 63 additions & 0 deletions test/hmac.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict'

const test = require('tap').test
const Tokens = require('..')

test('Tokens.constructor: instantiating Tokens with a non string hmacKey should throw', t => {
t.plan(1)
t.throws(() => new Tokens({ hmacKey: 123 }), new TypeError('option hmacKey must be a supported hmac key'))
})

test('Tokens.secret: should create a secret', t => {
t.plan(3)

new Tokens({ hmacKey: 'foo' }).secret(function (err, secret) {
t.error(err)
t.type(secret, 'string')
t.equal(secret.length, 24)
})
})

test('Tokens.verify: should return `true` with valid tokens', t => {
t.plan(1)

const secret = new Tokens({ hmacKey: 'foo' }).secretSync()
const token = new Tokens({ hmacKey: 'foo' }).create(secret)

t.equal(new Tokens({ hmacKey: 'foo' }).verify(secret, token), true)
})

test('Tokens.verify: should return `false` with invalid secret', t => {
t.plan(5)

const secret = new Tokens({ hmacKey: 'foo' }).secretSync()
const token = new Tokens({ hmacKey: 'foo' }).create(secret)

t.equal(new Tokens({ hmacKey: 'foo' }).verify(new Tokens().secretSync(), token), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify('invalid', token), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify(), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify([]), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify('invalid'), false)
})

test('Tokens.verify: should return `false` with invalid tokens', t => {
t.plan(4)

const secret = new Tokens({ hmacKey: 'foo' }).secretSync()
const token = new Tokens({ hmacKey: 'foo' }).create(secret)

t.equal(new Tokens({ hmacKey: 'foo' }).verify('invalid', token), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify(secret, undefined), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify(secret, []), false)
t.equal(new Tokens({ hmacKey: 'foo' }).verify(secret, 'hi'), false)
})

test('Tokens.verify: should return `false` with different hmac key', t => {
t.plan(2)

const secret = new Tokens({ hmacKey: 'foo' }).secretSync()
const token = new Tokens({ hmacKey: 'foo' }).create(secret)

t.equal(new Tokens({ hmacKey: 'foo' }).verify(secret, token), true)
t.equal(new Tokens({ hmacKey: 'bar' }).verify(secret, token), false)
})
17 changes: 17 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,28 @@ declare namespace Tokens {
* @default false
*/
userInfo?: boolean;

/**
* The HMAC key used to generate the cryptographic HMAC hash
*
*/
hmacKey?: string | ArrayBuffer | Buffer | TypedArray | DataView | CryptoKey;
}

export const Tokens: TokensConstructor
export { Tokens as default }
}

type TypedArray =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array;

declare function Tokens(...params: Parameters<TokensConstructor>): ReturnType<TokensConstructor>
export = Tokens
3 changes: 3 additions & 0 deletions types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Tokens({ saltLength: 10 });
Tokens({ secretLength: 10 });
Tokens({ userInfo: true });
Tokens({ validity: 10000 });
Tokens({ hmacKey: 'foo' });
new Tokens({ saltLength: 10 });
new Tokens({ secretLength: 10 });
new Tokens({ userInfo: true });
Expand All @@ -27,6 +28,8 @@ expectError(new Tokens({}).verify('secret', 'token', 'userinfo'));
expectError(new Tokens({ userInfo: false}).verify('secret', 'token', 'userInfo'));
expectError(new Tokens({ userInfo: true }).verify('secret', 'token'));

expectError(new Tokens({ hmacKey: 123 }));

expectType<Promise<string>>(Tokens().secret());
expectType<Promise<string>>(new Tokens().secret());

Expand Down

0 comments on commit ba9dde4

Please sign in to comment.