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

/* Copyright © 2015, Deutsche Telekom, Inc. */

"use strict";

const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/systemlibs.js");

XPCOMUtils.defineLazyServiceGetter(this, "UiccConnector",
                                   "@mozilla.org/secureelement/connector/uicc;1",
                                   "nsISecureElementConnector");

XPCOMUtils.defineLazyModuleGetter(this, "SEUtils",
                                  "resource://gre/modules/SEUtils.jsm");

XPCOMUtils.defineLazyGetter(this, "SE", function() {
  let obj = {};
  Cu.import("resource://gre/modules/se_consts.js", obj);
  return obj;
});

XPCOMUtils.defineLazyGetter(this, "GP", function() {
  let obj = {};
  Cu.import("resource://gre/modules/gp_consts.js", obj);
  return obj;
});

var DEBUG = SE.DEBUG_ACE;
function debug(msg) {
  if (DEBUG) {
    dump("-*- GPAccessRulesManager " + msg);
  }
}

/**
 * Based on [1] - "GlobalPlatform Device Technology
 * Secure Element Access Control Version 1.0".
 * GPAccessRulesManager reads and parses access rules from SE file system
 * as defined in section #7 of [1]: "Structure of Access Rule Files (ARF)".
 * Rules retrieval from ARA-M applet is not implmented due to lack of
 * commercial implemenations of ARA-M.
 * @todo Bug 1137537: Implement ARA-M support according to section #4 of [1]
 */
function GPAccessRulesManager() {}

GPAccessRulesManager.prototype = {
  // source [1] section 7.1.3 PKCS#15 Selection
  PKCS_AID: "a000000063504b43532d3135",

  // APDUs (ISO 7816-4) for accessing rules on SE file system
  // see for more details: http://www.cardwerk.com/smartcards/
  // smartcard_standard_ISO7816-4_6_basic_interindustry_commands.aspx
  READ_BINARY:   [GP.CLA_SM, GP.INS_RB, GP.P1_RB, GP.P2_RB],
  GET_RESPONSE:  [GP.CLA_SM, GP.INS_GR, GP.P1_GR, GP.P2_GR],
  SELECT_BY_DF:  [GP.CLA_SM, GP.INS_SF, GP.P1_SF_DF, GP.P2_SF_FCP],

  // Non-null if there is a channel open
  channel: null,

  // Refresh tag path in the acMain file as described in GPD spec,
  // sections 7.1.5 and C.1.
  REFRESH_TAG_PATH: [GP.TAG_SEQUENCE, GP.TAG_OCTETSTRING],
  refreshTag: null,

  // Contains rules as read from the SE
  rules: [],

  // Returns the latest rules. Results are cached.
  getAccessRules: function getAccessRules() {
    debug("getAccessRules");

    return new Promise((resolve, reject) => {
      this._readAccessRules(() => resolve(this.rules));
    });
  },

  _readAccessRules: Task.async(function*(done) {
    try {
      yield this._openChannel(this.PKCS_AID);

      let odf = yield this._readODF();
      let dodf = yield this._readDODF(odf);

      let acmf = yield this._readACMF(dodf);
      let refreshTag = acmf[this.REFRESH_TAG_PATH[0]]
                           [this.REFRESH_TAG_PATH[1]];

      // Update cached rules based on refreshTag.
      if (SEUtils.arraysEqual(this.refreshTag, refreshTag)) {
        debug("_readAccessRules: refresh tag equals to the one saved.");
        yield this._closeChannel();
        return done();
      }

      this.refreshTag = refreshTag;
      debug("_readAccessRules: refresh tag saved: " + this.refreshTag);

      let acrf = yield this._readACRules(acmf);
      let accf = yield this._readACConditions(acrf);
      this.rules = yield this._parseRules(acrf, accf);

      DEBUG && debug("_readAccessRules: " + JSON.stringify(this.rules, 0, 2));

      yield this._closeChannel();
      done();
    } catch (error) {
      debug("_readAccessRules: " + error);
      this.rules = [];
      yield this._closeChannel();
      done();
    }
  }),

  _openChannel: function _openChannel(aid) {
    if (this.channel !== null) {
      debug("_openChannel: Channel already opened, rejecting.");
      return Promise.reject();
    }

    return new Promise((resolve, reject) => {
      UiccConnector.openChannel(aid, {
        notifyOpenChannelSuccess: (channel, openResponse) => {
          debug("_openChannel/notifyOpenChannelSuccess: Channel " + channel +
                " opened, open response: " + openResponse);
          this.channel = channel;
          resolve();
        },
        notifyError: (error) => {
          debug("_openChannel/notifyError: failed to open channel, error: " +
                error);
          reject(error);
        }
      });
    });
  },

  _closeChannel: function _closeChannel() {
    if (this.channel === null) {
      debug("_closeChannel: Channel not opened, rejecting.");
      return Promise.reject();
    }

    return new Promise((resolve, reject) => {
      UiccConnector.closeChannel(this.channel, {
        notifyCloseChannelSuccess: () => {
          debug("_closeChannel/notifyCloseChannelSuccess: chanel " +
                this.channel + " closed");
          this.channel = null;
          resolve();
        },
        notifyError: (error) => {
          debug("_closeChannel/notifyError: error closing channel, error" +
                error);
          reject(error);
        }
      });
    });
  },

  _exchangeAPDU: function _exchangeAPDU(bytes) {
    DEBUG && debug("apdu " + JSON.stringify(bytes));

    let apdu = this._bytesToAPDU(bytes);
    return new Promise((resolve, reject) => {
      UiccConnector.exchangeAPDU(this.channel, apdu.cla,
        apdu.ins, apdu.p1, apdu.p2, apdu.data, apdu.le,
        {
          notifyExchangeAPDUResponse: (sw1, sw2, data) => {
            debug("APDU response is " + sw1.toString(16) + sw2.toString(16) +
                  " data: " + data);

            // 90 00 is "success"
            if (sw1 !== 0x90 && sw2 !== 0x00) {
              debug("rejecting APDU response");
              reject(new Error("Response " + sw1 + "," + sw2));
              return;
            }

            resolve(this._parseTLV(data));
          },

          notifyError: (error) => {
            debug("_exchangeAPDU/notifyError " + error);
            reject(error);
          }
        }
      );
    });
  },

  _readBinaryFile: function _readBinaryFile(selectResponse) {
    DEBUG && debug("Select response: " + JSON.stringify(selectResponse));
    // 0x80 tag parameter - get the elementary file (EF) length
    // without structural information.
    let fileLength = selectResponse[GP.TAG_FCP][0x80];

    // If file is empty, no need to attempt to read it.
    if (fileLength[0] === 0 && fileLength[1] === 0) {
      return Promise.resolve(null);
    }

    // TODO READ BINARY with filelength not supported
    // let readApdu = this.READ_BINARY.concat(fileLength);
    return this._exchangeAPDU(this.READ_BINARY);
  },

  _selectAndRead: function _selectAndRead(df) {
    return this._exchangeAPDU(this.SELECT_BY_DF.concat(df.length & 0xFF, df))
           .then((resp) => this._readBinaryFile(resp));
  },

  _readODF: function _readODF() {
    debug("_readODF");
    return this._selectAndRead(GP.ODF_DF);
  },

  _readDODF: function _readDODF(odfFile) {
    debug("_readDODF, ODF file: " + odfFile);

    // Data Object Directory File (DODF) is used as an entry point to the
    // Access Control data. It is specified in PKCS#15 section 6.7.6.
    // DODF is referenced by the ODF file, which looks as follows:
    //   A7 06
    //      30 04
    //         04 02 XY WZ
    // where [0xXY, 0xWZ] is a DF of DODF file.
    let DODF_DF = odfFile[GP.TAG_EF_ODF][GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING];
    return this._selectAndRead(DODF_DF);
  },

  _readACMF: function _readACMF(dodfFile) {
    debug("_readACMF, DODF file: " + dodfFile);

    // ACMF file DF is referenced in DODF file, which looks like this:
    //
    //  A1 29
    //     30 00
    //     30 0F
    //        0C 0D 47 50 20 53 45 20 41 63 63 20 43 74 6C
    //     A1 14
    //        30 12
    //           06 0A 2A 86 48 86 FC 6B 81 48 01 01  <-- GPD registered OID
    //           30 04
    //              04 02 AB CD  <-- ACMF DF
    //  A1 2B
    //     30 00
    //     30 0F
    //        0C 0D 53 41 54 53 41 20 47 54 4F 20 31 2E 31
    //     A1 16
    //        30 14
    //           06 0C 2B 06 01 04 01 2A 02 6E 03 01 01 01  <-- some other OID
    //           30 04
    //              04 02 XY WZ  <-- some other file's DF
    //
    // DODF file consists of DataTypes with oidDO entries. Entry with OID
    // equal to "1.2.840.114283.200.1.1" ("2A 86 48 86 FC 6B 81 48 01 01")
    // contains DF of the ACMF. In the file above, it means that ACMF DF
    // equals to [0xAB, 0xCD], and not [0xXY, 0xWZ].
    //
    // Algorithm used to encode OID to an byte array:
    //   http://www.snmpsharpnet.com/?p=153

    let gpdOid = [0x2A,                 // 1.2
                  0x86, 0x48,           // 840
                  0x86, 0xFC, 0x6B,     // 114283
                  0x81, 0x48,           // 129
                  0x01,                 // 1
                  0x01];                // 1

    let records = SEUtils.ensureIsArray(dodfFile[GP.TAG_EXTERNALDO]);

    // Look for the OID registered for GPD SE.
    let gpdRecords = records.filter((record) => {
      let oid = record[GP.TAG_EXTERNALDO][GP.TAG_SEQUENCE][GP.TAG_OID];
      return SEUtils.arraysEqual(oid, gpdOid);
    });

    // [1] 7.1.5: "There shall be only one ACMF file per Secure Element.
    // If a Secure Element contains several ACMF files, then the security shall
    // be considered compromised and the Access Control enforcer shall forbid
    // access to all (...) apps."
    if (gpdRecords.length !== 1) {
      return Promise.reject(new Error(gpdRecords.length + " ACMF files found"));
    }

    let ACMain_DF = gpdRecords[0][GP.TAG_EXTERNALDO][GP.TAG_SEQUENCE]
                                 [GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING];
    return this._selectAndRead(ACMain_DF);
  },

  _readACRules: function _readACRules(acMainFile) {
    debug("_readACRules, ACMain file: " + acMainFile);

    // ACMF looks like this:
    //
    //  30 10
    //    04 08 XX XX XX XX XX XX XX XX
    //    30 04
    //       04 02 XY WZ
    //
    // where [XY, WZ] is a DF of ACRF, and XX XX XX XX XX XX XX XX is a refresh
    // tag.

    let ACRules_DF = acMainFile[GP.TAG_SEQUENCE][GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING];
    return this._selectAndRead(ACRules_DF);
  },

  _readACConditions: function _readACConditions(acRulesFile) {
    debug("_readACCondition, ACRules file: " + acRulesFile);

    let acRules = SEUtils.ensureIsArray(acRulesFile[GP.TAG_SEQUENCE]);
    if (acRules.length === 0) {
      debug("No rules found in ACRules file.");
      return Promise.reject(new Error("No rules found in ACRules file"));
    }

    // We first read all the condition files referenced in the ACRules file,
    // because ACRules file might reference one ACCondition file more than
    // once. Since reading it isn't exactly fast, we optimize here.
    let acReadQueue = Promise.resolve({});

    acRules.forEach((ruleEntry) => {
      let df = ruleEntry[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING];

      // Promise chain read condition entries:
      let readAcCondition = (acConditionFiles) => {
        if (acConditionFiles[df] !== undefined) {
          debug("Skipping previously read acCondition df: " + df);
          return acConditionFiles;
        }

        return this._selectAndRead(df)
               .then((acConditionFileContents) => {
                 acConditionFiles[df] = acConditionFileContents;
                 return acConditionFiles;
               });
      }

      acReadQueue = acReadQueue.then(readAcCondition);
    });

    return acReadQueue;
  },

  _parseRules: function _parseRules(acRulesFile, acConditionFiles) {
    DEBUG && debug("_parseRules: acConditionFiles " + JSON.stringify(acConditionFiles));
    let rules = [];

    let acRules = SEUtils.ensureIsArray(acRulesFile[GP.TAG_SEQUENCE]);
    acRules.forEach((ruleEntry) => {
      DEBUG && debug("Parsing one rule: " + JSON.stringify(ruleEntry));
      let rule = {};

      // 0xA0 and 0x82 tags as per GPD spec sections C.1 - C.3. 0xA0 means
      // that rule describes access to one SE applet only (and its AID is
      // given). 0x82 means that rule describes acccess to all SE applets.
      let oneApplet = ruleEntry[GP.TAG_GPD_AID];
      let allApplets = ruleEntry[GP.TAG_GPD_ALL];

      if (oneApplet) {
        rule.applet = oneApplet[GP.TAG_OCTETSTRING];
      } else if (allApplets) {
        rule.applet = Ci.nsIAccessRulesManager.ALL_APPLET;
      } else {
        throw Error("Unknown applet definition");
      }

      let df = ruleEntry[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING];
      let condition = acConditionFiles[df];
      if (condition === null) {
        rule.application = Ci.nsIAccessRulesManager.DENY_ALL;
      } else if (condition[GP.TAG_SEQUENCE]) {
        if (!Array.isArray(condition[GP.TAG_SEQUENCE]) &&
            !condition[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]) {
          rule.application = Ci.nsIAccessRulesManager.ALLOW_ALL;
        } else {
          rule.application = SEUtils.ensureIsArray(condition[GP.TAG_SEQUENCE])
                             .map((conditionEntry) => {
            return conditionEntry[GP.TAG_OCTETSTRING];
          });
        }
      } else {
        throw Error("Unknown application definition");
      }

      DEBUG && debug("Rule parsed, adding to the list: " + JSON.stringify(rule));
      rules.push(rule);
    });

    DEBUG && debug("All rules parsed, we have those in total: " + JSON.stringify(rules));
    return rules;
  },

  _parseTLV: function _parseTLV(bytes) {
    let containerTags = [
      GP.TAG_SEQUENCE,
      GP.TAG_FCP,
      GP.TAG_GPD_AID,
      GP.TAG_EXTERNALDO,
      GP.TAG_INDIRECT,
      GP.TAG_EF_ODF
    ];
    return SEUtils.parseTLV(bytes, containerTags);
  },

  // TODO consider removing if better format for storing
  // APDU consts will be introduced
  _bytesToAPDU: function _bytesToAPDU(arr) {
    let apdu = {
      cla: arr[0] & 0xFF,
      ins: arr[1] & 0xFF,
      p1: arr[2] & 0xFF,
      p2: arr[3] & 0xFF,
      p3: arr[4] & 0xFF,
      le: 0
    };

    let data = (apdu.p3 > 0) ? (arr.slice(5)) : [];
    apdu.data = (data.length) ? SEUtils.byteArrayToHexString(data) : null;
    return apdu;
  },

  classID: Components.ID("{3e046b4b-9e66-439a-97e0-98a69f39f55f}"),
  contractID: "@mozilla.org/secureelement/access-control/rules-manager;1",
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessRulesManager])
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([GPAccessRulesManager]);