|
/* @flow */ |
|
|
|
const crypto = require('crypto'); |
|
const base32 = require('thirty-two'); |
|
const querystring = require('querystring'); |
|
|
|
/** |
|
* Convert an hmac result buffer to a 31-bit integer |
|
*/ |
|
|
|
function b2n( b: Buffer ): number { |
|
let i: number = b[ 19 ] &0xf; |
|
return ( b[ i++ ] & 0x7f ) << 24 | b[ i++ ] << 16 | b[ i++ ] << 8 | b[ i ]; |
|
} |
|
|
|
/** |
|
* Convert a number to a buffer of ints |
|
*/ |
|
|
|
function n2b( n: number ): Buffer { |
|
const buffer: Buffer = Buffer.alloc( 8 ); |
|
|
|
for ( let i: number = 7; i >= 0; --i ) { |
|
buffer[ i ] = n & 255; |
|
n = n >> 8; |
|
} |
|
|
|
return buffer; |
|
} |
|
|
|
/** |
|
* Generate a random 160-bit key |
|
*/ |
|
|
|
function genKey(): string { |
|
return crypto.randomBytes( 20 ).toString('ascii'); |
|
} |
|
|
|
/** |
|
* Generate a Google Authenticator URL |
|
*/ |
|
|
|
function genAuthenticatorURL( key: string, label: string, issuer: ?string ): string { |
|
if ( Buffer.byteLength( key ) < 16 ) { |
|
throw new Error('Secret key must be at least 16 bytes in length'); |
|
} |
|
|
|
const secret: string = base32.encode( key ).toString().replace( /=/g, '' ); |
|
const params: Object = { secret }; |
|
|
|
if ( issuer ) { |
|
params.issuer = issuer; |
|
} |
|
|
|
const query: string = querystring.stringify( params ); |
|
|
|
return `otpauth://totp/${ label }?${ query }`; |
|
}; |
|
|
|
/** |
|
* HOTP methods |
|
*/ |
|
|
|
const hotp = { |
|
|
|
/** |
|
* Generate an HOTP value |
|
* @method generate |
|
* @param {String} k – secret key |
|
* @param {Number} c – counter |
|
* @return {String} – HOTP value |
|
*/ |
|
|
|
generate( k: string, c: number ): string { |
|
const cbuf: Buffer = n2b( c ); |
|
const hash: Buffer = crypto.createHmac( 'sha1', k ).update( cbuf ).digest(); |
|
const trunc: number = b2n( hash ); |
|
const token: string = String( trunc % 1e6 ); |
|
return `${ Array( 7 - token.length ).join('0') }${ token }`; |
|
}, |
|
|
|
/** |
|
* Verify a token against a secret key and a counter |
|
* @method verify |
|
* @param {String} t – token |
|
* @param {String} k – secret key |
|
* @param {Number} c – counter |
|
* @param {Number} s – slop (tokens to accept on either side of counter) |
|
* @return {Boolean} – if the token is valid, true – otherwise false |
|
*/ |
|
|
|
verify( t: string, k: string, c: number, s: number = 1 ): boolean { |
|
for ( let i = c - s; i < c + s; ++i ) { |
|
if ( this.generate( k, i ) === t ) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
}; |
|
|
|
/** |
|
* TOTP methods |
|
*/ |
|
|
|
const totp = { |
|
|
|
/** |
|
* Generate an TOTP value |
|
* @method generate |
|
* @param {String} k – secret key |
|
* @return {String} – TOTP value |
|
*/ |
|
|
|
generate( k: string ): string { |
|
const c: number = Math.floor( Date.now() / 3e4 ); |
|
return hotp.generate( k, c ); |
|
}, |
|
|
|
/** |
|
* Verify a token against a secret key |
|
* @method verify |
|
* @param {String} t – token |
|
* @param {String} k – secret key |
|
* @param {Number} s – slop (tokens to accept on either side of counter) |
|
* @return {Boolean} – if the token is valid, true – otherwise false |
|
*/ |
|
|
|
verify( t: string, k: string, s: number = 1 ): boolean { |
|
const c: number = Math.floor( Date.now() / 3e4 ); |
|
return hotp.verify( t, k, c, s ); |
|
} |
|
|
|
}; |
|
|
|
module.exports = { genKey, genAuthenticatorURL, hotp, totp }; |