perryrh0dan/2fa

View on GitHub
src/secret/secret.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { randomBytes } from 'crypto';
import { create, QRCode } from 'qrcode';
import { format } from 'url';
import { base32Encode } from '../base32';
import { ImageOptions, Query, SecretOptions } from './types';

export class SecretKey {
  private _secret: string
  private _label: string
  private _issuer: string

  public constructor(secret: string, label: string, issuer: string) {
    this._secret = secret;
    this._label = label;
    this._issuer = issuer;
  }

  public get secret(): string {
    return this._secret;
  }

  public get label(): string {
    return this._label;
  }

  public get issuer(): string {
    return this._issuer;
  }

  public image(options?: ImageOptions): QRCode {
    const otpauthURL = this.otpauthURL(options);
    return create(otpauthURL, {});
  }

  /**
   * Generate a Google Authenticator-compatible otpauth:// URL for passing the
   * secret to a mobile device to install the secret.
   *
   * Authenticator considers TOTP codes valid for 30 seconds. Additionally,
   * the app presents 6 digits codes to the user. According to the
   * documentation, the period and number of digits are currently ignored by
   * the app.
   */
  public otpauthURL(options?: Partial<ImageOptions>): string {
    // unpack options
    if (!options) options = {};
    var secret = this.secret;
    var label = this.label;
    var issuer = this.issuer;
    var type = options.type || 'totp';
    var algorithm = options.algorithm || 'sha1';
    var digits = options.digits || 6;
    var period = options.period || 30;
    var encoding = options.encoding || 'ascii';

    // require counter for HOTP
    if (type === 'hotp' && typeof options.counter === 'undefined') {
      throw new Error('Missing counter value for HOTP');
    }

    var counter = options.counter;

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

    // build query while validating
    const query: Query = { secret: secret };

    if (issuer) query.issuer = issuer;
    if (type === 'hotp') {
      query.counter = counter;
    }

    query.algorithm = algorithm.toUpperCase();
    query.digits = digits;
    query.period = period;

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

/**
 * Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length
 * (default 32). Returns the secret key in ASCII, hexadecimal, and base32 format,
 * along with the URL used for the QR code for Google Authenticator (an otpauth
 * URL). Use a QR code library to generate a QR code based on the Google
 * Authenticator URL to obtain a QR code you can scan into the app.
 */
export function generateSecret(options: SecretOptions): SecretKey {
  // options
  const length = options.length || 32;
  const symbols = options.symbols || false;
  const label = options.label;
  const issuer = options.issuer;

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

  // return a SecretKey with ascii, hex, and base32
  var secretKey: SecretKey = new SecretKey(key, label, issuer);

  return secretKey;
};

/**
 * Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and
 * symbols (if requested).
 */
export function generateSecretASCII(length?: number, symbols?: boolean): string {
  var bytes = 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;
};