/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This module implements client-side key stretching for use in Firefox
 * Accounts account creation and login.
 *
 * See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
 */

"use strict";

this.EXPORTED_SYMBOLS = ["Credentials"];

const {utils: Cu, interfaces: Ci} = Components;

Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://services-common/utils.js");

const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
const PBKDF2_ROUNDS = 1000;
const STRETCHED_PW_LENGTH_BYTES = 32;
const HKDF_SALT = CommonUtils.hexToBytes("00");
const HKDF_LENGTH = 32;
const HMAC_ALGORITHM = Ci.nsICryptoHMAC.SHA256;
const HMAC_LENGTH = 32;

// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
// default.
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
try {
  this.LOG_LEVEL =
    Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
    && Services.prefs.getCharPref(PREF_LOG_LEVEL);
} catch (e) {
  this.LOG_LEVEL = Log.Level.Error;
}

let log = Log.repository.getLogger("Identity.FxAccounts");
log.level = LOG_LEVEL;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));

this.Credentials = Object.freeze({
  /**
   * Make constants accessible to tests
   */
  constants: {
    PROTOCOL_VERSION: PROTOCOL_VERSION,
    PBKDF2_ROUNDS: PBKDF2_ROUNDS,
    STRETCHED_PW_LENGTH_BYTES: STRETCHED_PW_LENGTH_BYTES,
    HKDF_SALT: HKDF_SALT,
    HKDF_LENGTH: HKDF_LENGTH,
    HMAC_ALGORITHM: HMAC_ALGORITHM,
    HMAC_LENGTH: HMAC_LENGTH,
  },

  /**
   * KW function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
   *
   * keyWord derivation for use as a salt.
   *
   *
   *   @param {String} context  String for use in generating salt
   *
   *   @return {bitArray} the salt
   *
   * Note that PROTOCOL_VERSION does not refer in any way to the version of the
   * Firefox Accounts API.
   */
  keyWord: function(context) {
    return CommonUtils.stringToBytes(PROTOCOL_VERSION + context);
  },

  /**
   * KWE function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
   *
   * keyWord extended with a name and an email.
   *
   *   @param {String} name The name of the salt
   *   @param {String} email The email of the user.
   *
   *   @return {bitArray} the salt combination with the namespace
   *
   * Note that PROTOCOL_VERSION does not refer in any way to the version of the
   * Firefox Accounts API.
   */
  keyWordExtended: function(name, email) {
    return CommonUtils.stringToBytes(PROTOCOL_VERSION + name + ':' + email);
  },

  setup: function(emailInput, passwordInput, options={}) {
    let deferred = Promise.defer();
    log.debug("setup credentials for " + emailInput);

    let hkdfSalt = options.hkdfSalt || HKDF_SALT;
    let hkdfLength = options.hkdfLength || HKDF_LENGTH;
    let hmacLength = options.hmacLength || HMAC_LENGTH;
    let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM;
    let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES;
    let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS;

    let result = {
      emailUTF8: emailInput,
      passwordUTF8: passwordInput,
    };

    let password = CommonUtils.encodeUTF8(passwordInput);
    let salt = this.keyWordExtended("quickStretch", emailInput);

    let runnable = () => {
      let start = Date.now();
      let quickStretchedPW = CryptoUtils.pbkdf2Generate(
          password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);

      result.quickStretchedPW = quickStretchedPW;

      result.authPW =
        CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);

      result.unwrapBKey =
        CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);

      log.debug("Credentials set up after " + (Date.now() - start) + " ms");
      deferred.resolve(result);
    }

    Services.tm.currentThread.dispatch(runnable,
        Ci.nsIThread.DISPATCH_NORMAL);
    log.debug("Dispatched thread for credentials setup crypto work");

    return deferred.promise;
  }
});

