speakeasyjs / speakeasy

**NOT MAINTAINED** Two-factor authentication for Node.js. One-time passcode generator (HOTP/TOTP) with support for Google Authenticator.
MIT License
2.71k stars 229 forks source link

[hotp] Was not giving right code...(So I Maintained for 2021) #138

Open suprim12 opened 3 years ago

suprim12 commented 3 years ago
'use strict';
var base32 = require('base32.js');
var crypto = require('crypto');
var url = require('url');
var util = require('util');

exports.digest = function digest (options) {
  var i;

  // unpack options
  var secret = options.secret;
  var counter = options.counter;
  var encoding = options.encoding || 'ascii';
  var algorithm = (options.algorithm || 'sha1').toLowerCase();

  // Backwards compatibility - deprecated
  if (options.key != null) {
    console.warn('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.');
    secret = options.key;
  }

  // convert secret to buffer
  if (!Buffer.isBuffer(secret)) {
    if (encoding === 'base32') { secret = base32.decode(secret); }
    secret =  Buffer.from(secret, encoding);
  }

  var secret_buffer_size;
  if (algorithm === 'sha1') {
    secret_buffer_size = 20; // 20 bytes
  } else if (algorithm === 'sha256') {
    secret_buffer_size = 32; // 32 bytes
  } else if (algorithm === 'sha512') {
    secret_buffer_size = 64; // 64 bytes
  } else {
    console.warn('Speakeasy - The algorithm provided (`' + algorithm + '`) is not officially supported, results may be different than expected.');
  }

  // The secret for sha1, sha256 and sha512 needs to be a fixed number of bytes for the one-time-password to be calculated correctly
  // Pad the buffer to the correct size be repeating the secret to the desired length
  if (secret_buffer_size && secret.length !== secret_buffer_size) {
    secret =  Buffer.from(Array(Math.ceil(secret_buffer_size / secret.length) + 1).join(secret.toString('hex')), 'hex').slice(0, secret_buffer_size);
  }

  // create an buffer from the counter
  var buf =  Buffer.alloc(8);
  var tmp = counter;
  for (i = 0; i < 8; i++) {
    // mask 0xff over number to get last 8
    buf[7 - i] = tmp & 0xff;

    // shift 8 and get ready to loop over the next batch of 8
    tmp = tmp >> 8;
  }

  // init hmac with the key
  var hmac = crypto.createHmac(algorithm, secret);

  // update hmac with the counter
  hmac.update(buf);

  // return the digest
  return hmac.digest();
};

exports.hotp = function hotpGenerate (options) {

  // verify secret and counter exists
  var secret = options.secret;
  var key = options.key;
  var counter = options.counter || 0;

  if (key === null || typeof key === 'undefined') {
    if (secret === null || typeof secret === 'undefined') {
      throw new Error('Speakeasy - hotp - Missing secret');
    }
  }

  if (counter === null || typeof counter === 'undefined') {
    throw new Error('Speakeasy - hotp - Missing counter');
  }

  // unpack digits
  // backward compatibility: `length` is also accepted here, but deprecated
  var digits = (options.digits != null ? options.digits : options.length) || 6;
  if (options.length != null) 
  console.warn('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');

  // digest the options
  var digest = options.digest || exports.digest(options);

  // compute HOTP offset
  var offset = digest[digest.length - 1] & 0xf;

  // calculate binary code (RFC4226 5.4)
  var code = (digest[offset] & 0x7f) << 24 |
    (digest[offset + 1] & 0xff) << 16 |
    (digest[offset + 2] & 0xff) << 8 |
    (digest[offset + 3] & 0xff);

  // left-pad code
  code = new Array(digits + 1).join('0') + code.toString(10);

  console.log(code.substr(-digits));
  // return length number off digits
  return code.substr(-digits);
};

function intToBytes(num) {
    var bytes = [];

    for(var i=7 ; i>=0 ; --i) {
        bytes[i] = num & (255);
        num = num >> 8;
    }

    return bytes;
}

function hexToBytes(hex) {
    var bytes = [];
    for(var c = 0, C = hex.length; c < C; c += 2) {
        bytes.push(parseInt(hex.substr(c, 2), 16));
    }
    return bytes;
}

exports.customhotp = function customgen(options){
    var key = options.secret || '';
    var counter = options.counter || 0;
    var p = 6;

    // Create the byte array
    var b =  Buffer.from(intToBytes(counter));

    var hmac = crypto.createHmac('sha1',  Buffer.from(key));

    // Update the HMAC with the byte array
    var digest = hmac.update(b).digest('hex');

    // Get byte array
    var h = hexToBytes(digest);

    // Truncate
    var offset = h[19] & 0xf;
    var v = (h[offset] & 0x7f) << 24 |
        (h[offset + 1] & 0xff) << 16 |
        (h[offset + 2] & 0xff) << 8  |
        (h[offset + 3] & 0xff);

    v = (v % 1000000) + '';
    console.log(Array(7-v.length).join('0') + v);
    return Array(7-v.length).join('0') + v;
}

// Alias counter() for hotp()
exports.counter = exports.hotp;

exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
  var i;

  // shadow options
  options = Object.create(options);

  // verify secret and token exist
  var secret = options.secret;
  var token = options.token;
  if (secret === null || typeof secret === 'undefined') throw new Error('Speakeasy - hotp.verifyDelta - Missing secret');
  if (token === null || typeof token === 'undefined') throw new Error('Speakeasy - hotp.verifyDelta - Missing token');

  // unpack options
  var token = String(options.token);
  var digits = parseInt(options.digits, 10) || 6;
  var window = parseInt(options.window, 10) || 0;
  var counter = parseInt(options.counter, 10) || 0;

  // fail if token is not of correct length
  if (token.length !== digits) {
    return;
  }

  // parse token to integer
  token = parseInt(token, 10);

  // fail if token is NA
  if (isNaN(token)) {
    return;
  }

  // loop from C to C + W inclusive
  for (i = counter - window; i <= counter + window; ++i) {
    options.counter = i;
    // domain-specific constant-time comparison for integer codes
    console.log(token);

    // WAS NOT GIVING RIGHT CODE -- FIXED

    // if (parseInt(exports.hotp(options), 10) === token) {
    //   // found a matching code, return delta
    //   console.log(token);
    //   return {delta: i - counter};
    // }

    if (parseInt(exports.customhotp(options)) === token) {
        // found a matching code, return delta
        console.log(token);
        return {delta: i - counter};
      }
  }

  // no codes have matched
  return null;
};

exports.hotp.verify = function hotpVerify (options) {
  return exports.hotp.verifyDelta(options) != null;
};

exports._counter = function _counter (options) {
  var step = options.step || 30;
  var time = options.time != null ? (options.time * 1000) : Date.now();

  // also accepts 'initial_time', but deprecated
  var epoch = (options.epoch != null ? (options.epoch * 1000) : (options.initial_time * 1000)) || 0;
  if (options.initial_time != null) console.warn('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.');

  return Math.floor((time - epoch) / step / 1000);
};

exports.totp = function totpGenerate (options) {
  // shadow options
  options = Object.create(options);

  // verify secret exists if key is not specified
  var key = options.key;
  var secret = options.secret;
  if (key === null || typeof key === 'undefined') {
    if (secret === null || typeof secret === 'undefined') {
      throw new Error('Speakeasy - totp - Missing secret');
    }
  }

  // calculate default counter value
  if (options.counter == null) options.counter = exports._counter(options);

  // pass to hotp
  return this.hotp(options);
};

// Alias time() for totp()
exports.time = exports.totp;

exports.totp.verifyDelta = function totpVerifyDelta (options) {
  // shadow options
  options = Object.create(options);
  // verify secret and token exist
  var secret = options.secret;
  var token = options.token;
  if (secret === null || typeof secret === 'undefined') throw new Error('Speakeasy - totp.verifyDelta - Missing secret');
  if (token === null || typeof token === 'undefined') throw new Error('Speakeasy - totp.verifyDelta - Missing token');

  // unpack options
  var window = parseInt(options.window, 10) || 0;

  // calculate default counter value
  if (options.counter == null) options.counter = exports._counter(options);

  // adjust for two-sided window
  options.counter -= window;
  options.window += window;

  // pass to hotp.verifyDelta
  var delta = exports.hotp.verifyDelta(options);

  console.log(delta);
  // adjust for two-sided window
  if (delta) {
    delta.delta -= window;
  }

  return delta;
};

exports.totp.verify = function totpVerify (options) {
  return exports.totp.verifyDelta(options) != null;
};

exports.generateSecret = function generateSecret (options) {
  // options
  if (!options) options = {};
  var length = options.length || 32;
  var name = options.name || 'SecretKey';
  var qr_codes = options.qr_codes || false;
  var google_auth_qr = options.google_auth_qr || false;
  var otpauth_url = options.otpauth_url != null ? options.otpauth_url : true;
  var symbols = true;
  var issuer = options.issuer;

  // turn off symbols only when explicity told to
  if (options.symbols !== undefined && options.symbols === false) {
    symbols = false;
  }

  // generate an ascii key
  var key = this.generateSecretASCII(length, symbols);

  // return a SecretKey with ascii, hex, and base32
  var SecretKey = {};
  SecretKey.ascii = key;
  SecretKey.hex = Buffer.from(key, 'ascii').toString('hex');
  SecretKey.base32 = base32.encode(Buffer.from(key)).toString().replace(/=/g, '');

  // generate some qr codes if requested
  if (qr_codes) {
    console.warn('Speakeasy - Deprecation Notice - generateSecret() QR codes are deprecated and no longer supported. Please use your own QR code implementation.');
    SecretKey.qr_code_ascii = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii);
    SecretKey.qr_code_hex = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex);
    SecretKey.qr_code_base32 = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32);
  }

  // add in the Google Authenticator-compatible otpauth URL
  if (otpauth_url) {
    SecretKey.otpauth_url = exports.otpauthURL({
      secret: SecretKey.ascii,
      label: name,
      issuer: issuer
    });
  }

  // generate a QR code for use in Google Authenticator if requested
  if (google_auth_qr) {
    console.warn('Speakeasy - Deprecation Notice - generateSecret() Google Auth QR code is deprecated and no longer supported. Please use your own QR code implementation.');
    SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.otpauthURL({ secret: SecretKey.base32, label: name }));
  }

  return SecretKey;
};

exports.generate_key = util.deprecate(function (options) {
  return exports.generateSecret(options);
}, 'Speakeasy - Deprecation Notice - `generate_key()` is depreciated, please use `generateSecret()` instead.');

exports.generateSecretASCII = function generateSecretASCII (length, symbols) {
  var bytes = crypto.randomBytes(length || 32);
  var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
  if (symbols) {
    set += '!@#$%^&*()<>?/[]{},.:;';
  }

  var output = '';
  for (var i = 0, l = bytes.length; i < l; i++) {
    output += set[Math.floor(bytes[i] / 255.0 * (set.length - 1))];
  }
  return output;
};

// Backwards compatibility - generate_key_ascii is deprecated
exports.generate_key_ascii = util.deprecate(function (length, symbols) {
  return exports.generateSecretASCII(length, symbols);
}, 'Speakeasy - Deprecation Notice - `generate_key_ascii()` is depreciated, please use `generateSecretASCII()` instead.');

exports.otpauthURL = function otpauthURL (options) {
  // unpack options
  var secret = options.secret;
  var label = options.label;
  var issuer = options.issuer;
  var type = (options.type || 'totp').toLowerCase();
  var counter = options.counter;
  var algorithm = (options.algorithm || 'sha1').toLowerCase();
  var digits = options.digits || 6;
  var period = options.period || 30;
  var encoding = options.encoding || 'ascii';

  // validate type
  switch (type) {
    case 'totp':
    case 'hotp':
      break;
    default:
      throw new Error('Speakeasy - otpauthURL - Invalid type `' + type + '`; must be `hotp` or `totp`');
  }

  // validate required options
  if (!secret) throw new Error('Speakeasy - otpauthURL - Missing secret');
  if (!label) throw new Error('Speakeasy - otpauthURL - Missing label');

  // require counter for HOTP
  if (type === 'hotp' && (counter === null || typeof counter === 'undefined')) {
    throw new Error('Speakeasy - otpauthURL - Missing counter value for HOTP');
  }

  // convert secret to base32
  if (encoding !== 'base32') secret = new Buffer.from(secret, encoding);
  if (Buffer.isBuffer(secret)) secret = base32.encode(secret);

  // build query while validating
  var query = {secret: secret};
  if (issuer) query.issuer = issuer;
  if (type === 'hotp') {
    query.counter = counter;
  }

  // validate algorithm
  if (algorithm != null) {
    switch (algorithm.toUpperCase()) {
      case 'SHA1':
      case 'SHA256':
      case 'SHA512':
        break;
      default:
        console.warn('Speakeasy - otpauthURL - Warning - Algorithm generally should be SHA1, SHA256, or SHA512');
    }
    query.algorithm = algorithm.toUpperCase();
  }

  // validate digits
  if (digits != null) {
    if (isNaN(digits)) {
      throw new Error('Speakeasy - otpauthURL - Invalid digits `' + digits + '`');
    } else {
      switch (parseInt(digits, 10)) {
        case 6:
        case 8:
          break;
        default:
          console.warn('Speakeasy - otpauthURL - Warning - Digits generally should be either 6 or 8');
      }
    }
    query.digits = digits;
  }

  // validate period
  if (period != null) {
    period = parseInt(period, 10);
    if (~~period !== period) {
      throw new Error('Speakeasy - otpauthURL - Invalid period `' + period + '`');
    }
    query.period = period;
  }

  // return url

  return url.format({
    protocol: 'otpauth',
    slashes: true,
    hostname: type,
    pathname: encodeURIComponent(label),
    query: query
  });

};

Basic Usage

const twofa = require('./utils/2fa');
const qrcode = require('qrcode');

const secret =  twofa.generateSecret({
    name:'Mero'
})
qrcode.toDataURL(secret.otpauth_url,function(err,data){
    if(err) throw err;
    console.log(data);
})
console.log(secret);
const twofa = require('./utils/2fa');

// To Verify
const verified = twofa.totp.verify({
    secret:'>Lu%Z:$W&s2Zf0Qqsm!P%g(R!@i[T5gk',
    token:'878287',
})

console.log(verified);
DAmrGharieb commented 3 years ago

first thank you for your attention and spent time ==> the same behaviour ... is there is any contradiction with any windows files permissions i think the problem there i tested the old code and the new one by you give true on various machines and false on one machine even it generate the code successfully but when verifying is the problem

HamzaAitOumghar commented 3 years ago

I tried your solution but the problem still occurs , my code :

import speakeasy from "./speakeasy";

const secret = "AZAD22113ADASDJQF";

const token = speakeasy.totp({
      secret: secret,
      encoding: "ascii",
      step: 600,
    });

 const isValid = speakeasy.totp.verify({
        secret: secret,
        token: code,
        encoding: "ascii",
        window: 2,
      });