/* 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/. */

"use strict";

this.EXPORTED_SYMBOLS = [
  "BulkKeyBundle",
  "SyncKeyBundle"
];

var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.import("resource://services-sync/constants.js");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/util.js");

/**
 * Represents a pair of keys.
 *
 * Each key stored in a key bundle is 256 bits. One key is used for symmetric
 * encryption. The other is used for HMAC.
 *
 * A KeyBundle by itself is just an anonymous pair of keys. Other types
 * deriving from this one add semantics, such as associated collections or
 * generating a key bundle via HKDF from another key.
 */
function KeyBundle() {
  this._encrypt = null;
  this._encryptB64 = null;
  this._hmac = null;
  this._hmacB64 = null;
  this._hmacObj = null;
  this._sha256HMACHasher = null;
}
KeyBundle.prototype = {
  _encrypt: null,
  _encryptB64: null,
  _hmac: null,
  _hmacB64: null,
  _hmacObj: null,
  _sha256HMACHasher: null,

  equals: function equals(bundle) {
    return bundle &&
           (bundle.hmacKey == this.hmacKey) &&
           (bundle.encryptionKey == this.encryptionKey);
  },

  /*
   * Accessors for the two keys.
   */
  get encryptionKey() {
    return this._encrypt;
  },

  set encryptionKey(value) {
    if (!value || typeof value != "string") {
      throw new Error("Encryption key can only be set to string values.");
    }

    if (value.length < 16) {
      throw new Error("Encryption key must be at least 128 bits long.");
    }

    this._encrypt = value;
    this._encryptB64 = btoa(value);
  },

  get encryptionKeyB64() {
    return this._encryptB64;
  },

  get hmacKey() {
    return this._hmac;
  },

  set hmacKey(value) {
    if (!value || typeof value != "string") {
      throw new Error("HMAC key can only be set to string values.");
    }

    if (value.length < 16) {
      throw new Error("HMAC key must be at least 128 bits long.");
    }

    this._hmac = value;
    this._hmacB64 = btoa(value);
    this._hmacObj = value ? Utils.makeHMACKey(value) : null;
    this._sha256HMACHasher = value ? Utils.makeHMACHasher(
      Ci.nsICryptoHMAC.SHA256, this._hmacObj) : null;
  },

  get hmacKeyB64() {
    return this._hmacB64;
  },

  get hmacKeyObject() {
    return this._hmacObj;
  },

  get sha256HMACHasher() {
    return this._sha256HMACHasher;
  },

  /**
   * Populate this key pair with 2 new, randomly generated keys.
   */
  generateRandom: function generateRandom() {
    let generatedHMAC = Svc.Crypto.generateRandomKey();
    let generatedEncr = Svc.Crypto.generateRandomKey();
    this.keyPairB64 = [generatedEncr, generatedHMAC];
  },

};

/**
 * Represents a KeyBundle associated with a collection.
 *
 * This is just a KeyBundle with a collection attached.
 */
this.BulkKeyBundle = function BulkKeyBundle(collection) {
  let log = Log.repository.getLogger("Sync.BulkKeyBundle");
  log.info("BulkKeyBundle being created for " + collection);
  KeyBundle.call(this);

  this._collection = collection;
}

BulkKeyBundle.prototype = {
  __proto__: KeyBundle.prototype,

  get collection() {
    return this._collection;
  },

  /**
   * Obtain the key pair in this key bundle.
   *
   * The returned keys are represented as raw byte strings.
   */
  get keyPair() {
    return [this.encryptionKey, this.hmacKey];
  },

  set keyPair(value) {
    if (!Array.isArray(value) || value.length != 2) {
      throw new Error("BulkKeyBundle.keyPair value must be array of 2 keys.");
    }

    this.encryptionKey = value[0];
    this.hmacKey       = value[1];
  },

  get keyPairB64() {
    return [this.encryptionKeyB64, this.hmacKeyB64];
  },

  set keyPairB64(value) {
    if (!Array.isArray(value) || value.length != 2) {
      throw new Error("BulkKeyBundle.keyPairB64 value must be an array of 2 " +
                      "keys.");
    }

    this.encryptionKey  = Utils.safeAtoB(value[0]);
    this.hmacKey        = Utils.safeAtoB(value[1]);
  },
};

/**
 * Represents a key pair derived from a Sync Key via HKDF.
 *
 * Instances of this type should be considered immutable. You create an
 * instance by specifying the username and 26 character "friendly" Base32
 * encoded Sync Key. The Sync Key is derived at instance creation time.
 *
 * If the username or Sync Key is invalid, an Error will be thrown.
 */
this.SyncKeyBundle = function SyncKeyBundle(username, syncKey) {
  let log = Log.repository.getLogger("Sync.SyncKeyBundle");
  log.info("SyncKeyBundle being created.");
  KeyBundle.call(this);

  this.generateFromKey(username, syncKey);
}
SyncKeyBundle.prototype = {
  __proto__: KeyBundle.prototype,

  /*
   * If we've got a string, hash it into keys and store them.
   */
  generateFromKey: function generateFromKey(username, syncKey) {
    if (!username || (typeof username != "string")) {
      throw new Error("Sync Key cannot be generated from non-string username.");
    }

    if (!syncKey || (typeof syncKey != "string")) {
      throw new Error("Sync Key cannot be generated from non-string key.");
    }

    if (!Utils.isPassphrase(syncKey)) {
      throw new Error("Provided key is not a passphrase, cannot derive Sync " +
                      "Key Bundle.");
    }

    // Expand the base32 Sync Key to an AES 256 and 256 bit HMAC key.
    let prk = Utils.decodeKeyBase32(syncKey);
    let info = HMAC_INPUT + username;
    let okm = Utils.hkdfExpand(prk, info, 32 * 2);
    this.encryptionKey = okm.slice(0, 32);
    this.hmacKey = okm.slice(32, 64);
  },
};