Skip to content

Instantly share code, notes, and snippets.

@kevincennis
Last active August 3, 2018 18:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kevincennis/b1ca2799703cbe0aeb59db02fc0840f3 to your computer and use it in GitHub Desktop.
Save kevincennis/b1ca2799703cbe0aeb59db02fc0840f3 to your computer and use it in GitHub Desktop.
totp/hotp
{
"plugins": [
"transform-flow-strip-types"
]
}
[options]
unsafe.enable_getters_and_setters=true

HOTP/TOTP

  1. npm i
  2. make
  3. Download the Google Authenticator app
  4. node demo.js
  5. The script will open Chrome and display a QR code
  6. Use Google Authenticator to scan the QR code
  7. Enter the number from Google Authenticator into your terminal to validate it

Note: Each time you run the script, it will generate a new secret key, so any previous QR codes you scanned into Google Authenticator will basically be orphaned.

const lib = require('./dist/index.js');
const cp = require('child_process');
const key = lib.genKey();
const url = lib.genAuthenticatorURL( key, 'Test' );
const enc = encodeURI( url );
const link = `https://api.qrserver.com/v1/create-qr-code/?data=${ enc }&margin=0`;
cp.exec( `open -a "Google Chrome" ${ link }` );
prompt();
function prompt() {
console.log('\n\nEnter the code from Google Authenticator to verify it');
process.stdin.resume();
process.stdin.once( 'data', input => {
const token = input.toString().trim();
const valid = lib.totp.verify( token, key );
console.log( 'The supplied token is %s', valid ? 'valid' : 'invalid' );
prompt();
});
}
/* @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 };
all:
mkdir -p dist && babel index.js --out-file dist/index.js
{
"name": "hotp-totp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "flow && mocha --compilers js:babel-register",
"build": "make"
},
"author": "",
"license": "MIT",
"devDependencies": {
"babel-plugin-transform-flow-strip-types": "^6.18.0",
"babel-register": "^6.18.0",
"flow-bin": "^0.37.0",
"mocha": "^3.2.0"
},
"dependencies": {
"babel-cli": "^6.18.0",
"thirty-two": "^1.0.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment