"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.VerificationBase = exports.SwitchStartEventError = void 0;

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

var _event = require("../../models/event");

var _events = require("events");

var _logger = require("../../logger");

var _deviceinfo = require("../deviceinfo");

var _Error = require("./Error");

var _CrossSigning = require("../CrossSigning");

var _olmlib = require("../olmlib");

/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Base class for verification methods.
 * @module crypto/verification/Base
 */
const timeoutException = new Error("Verification timed out");

class SwitchStartEventError extends Error {
  constructor(startEvent) {
    super();
    this.startEvent = startEvent;
  }

}

exports.SwitchStartEventError = SwitchStartEventError;

class VerificationBase extends _events.EventEmitter {
  /**
   * Base class for verification methods.
   *
   * <p>Once a verifier object is created, the verification can be started by
   * calling the verify() method, which will return a promise that will
   * resolve when the verification is completed, or reject if it could not
   * complete.</p>
   *
   * <p>Subclasses must have a NAME class property.</p>
   *
   * @class
   *
   * @param {module:base-apis~Channel} channel the verification channel to send verification messages over.
   *
   * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
   *
   * @param {string} userId the user ID that is being verified
   *
   * @param {string} deviceId the device ID that is being verified
   *
   * @param {object} [startEvent] the m.key.verification.start event that
   * initiated this verification, if any
   *
   * @param {object} [request] the key verification request object related to
   * this verification, if any
   */
  constructor(channel, baseApis, userId, deviceId, startEvent, request) {
    super();
    this._channel = channel;
    this._baseApis = baseApis;
    this.userId = userId;
    this.deviceId = deviceId;
    this.startEvent = startEvent;
    this.request = request;
    this.cancelled = false;
    this._done = false;
    this._promise = null;
    this._transactionTimeoutTimer = null;
  }

  get initiatedByMe() {
    // if there is no start event yet,
    // we probably want to send it,
    // which happens if we initiate
    if (!this.startEvent) {
      return true;
    }

    const sender = this.startEvent.getSender();
    const content = this.startEvent.getContent();
    return sender === this._baseApis.getUserId() && content.from_device === this._baseApis.getDeviceId();
  }

  _resetTimer() {
    _logger.logger.info("Refreshing/starting the verification transaction timeout timer");

    if (this._transactionTimeoutTimer !== null) {
      clearTimeout(this._transactionTimeoutTimer);
    }

    this._transactionTimeoutTimer = setTimeout(() => {
      if (!this._done && !this.cancelled) {
        _logger.logger.info("Triggering verification timeout");

        this.cancel(timeoutException);
      }
    }, 10 * 60 * 1000); // 10 minutes
  }

  _endTimer() {
    if (this._transactionTimeoutTimer !== null) {
      clearTimeout(this._transactionTimeoutTimer);
      this._transactionTimeoutTimer = null;
    }
  }

  _send(type, uncompletedContent) {
    return this._channel.send(type, uncompletedContent);
  }

  _waitForEvent(type) {
    if (this._done) {
      return Promise.reject(new Error("Verification is already done"));
    }

    const existingEvent = this.request.getEventFromOtherParty(type);

    if (existingEvent) {
      return Promise.resolve(existingEvent);
    }

    this._expectedEvent = type;
    return new Promise((resolve, reject) => {
      this._resolveEvent = resolve;
      this._rejectEvent = reject;
    });
  }

  canSwitchStartEvent() {
    return false;
  }

  switchStartEvent(event) {
    if (this.canSwitchStartEvent(event)) {
      _logger.logger.log("Verification Base: switching verification start event", {
        restartingFlow: !!this._rejectEvent
      });

      if (this._rejectEvent) {
        const reject = this._rejectEvent;
        this._rejectEvent = undefined;
        reject(new SwitchStartEventError(event));
      } else {
        this.startEvent = event;
      }
    }
  }

  handleEvent(e) {
    if (this._done) {
      return;
    } else if (e.getType() === this._expectedEvent) {
      // if we receive an expected m.key.verification.done, then just
      // ignore it, since we don't need to do anything about it
      if (this._expectedEvent !== "m.key.verification.done") {
        this._expectedEvent = undefined;
        this._rejectEvent = undefined;

        this._resetTimer();

        this._resolveEvent(e);
      }
    } else if (e.getType() === "m.key.verification.cancel") {
      const reject = this._reject;
      this._reject = undefined; // there is only promise to reject if verify has been called

      if (reject) {
        const content = e.getContent();
        const {
          reason,
          code
        } = content;
        reject(new Error(`Other side cancelled verification ` + `because ${reason} (${code})`));
      }
    } else if (this._expectedEvent) {
      // only cancel if there is an event expected.
      // if there is no event expected, it means verify() wasn't called
      // and we're just replaying the timeline events when syncing
      // after a refresh when the events haven't been stored in the cache yet.
      const exception = new Error("Unexpected message: expecting " + this._expectedEvent + " but got " + e.getType());
      this._expectedEvent = undefined;

      if (this._rejectEvent) {
        const reject = this._rejectEvent;
        this._rejectEvent = undefined;
        reject(exception);
      }

      this.cancel(exception);
    }
  }

  done() {
    this._endTimer(); // always kill the activity timer


    if (!this._done) {
      this.request.onVerifierFinished();

      this._resolve(); //#region Cross-signing keys request
      // If this is a self-verification, ask the other party for keys


      if (this._baseApis.getUserId() !== this.userId) {
        return;
      } // FIXME: This is a lot of logic that isn't anything to do with verification
      // and probably ought to be somewhere else.


      console.log("VerificationBase.done: Self-verification done; requesting keys");
      /* This happens asynchronously, and we're not concerned about
       * waiting for it.  We return here in order to test. */

      return new Promise((resolve, reject) => {
        const client = this._baseApis;
        const original = client._crypto._crossSigningInfo;
        /* We already have all of the infrastructure we need to validate and
         * cache cross-signing keys, so instead of replicating that, here we
         * set up callbacks that request them from the other device and call
         * CrossSigningInfo.getCrossSigningKey() to validate/cache */

        const crossSigning = new _CrossSigning.CrossSigningInfo(original.userId, {
          getCrossSigningKey: async type => {
            console.debug("VerificationBase.done: requesting secret", type, this.deviceId);
            const {
              promise
            } = client.requestSecret(`m.cross_signing.${type}`, [this.deviceId]);
            const result = await promise;
            const decoded = (0, _olmlib.decodeBase64)(result);
            return Uint8Array.from(decoded);
          }
        }, original._cacheCallbacks);
        crossSigning.keys = original.keys; // XXX: get all keys out if we get one key out
        // https://github.com/vector-im/riot-web/issues/12604
        // then change here to reject on the timeout

        /* Requests can be ignored, so don't wait around forever */

        const timeout = new Promise((resolve, reject) => {
          setTimeout(resolve, VerificationBase.keyRequestTimeoutMs, new Error("Timeout"));
        }); // also request and cache the key backup key

        const backupKeyPromise = new Promise(async resolve => {
          const cachedKey = await client._crypto.getSessionBackupPrivateKey();

          if (!cachedKey) {
            _logger.logger.info("No cached backup key found. Requesting...");

            const secretReq = client.requestSecret('m.megolm_backup.v1', [this.deviceId]);
            const base64Key = await secretReq.promise;

            _logger.logger.info("Got key backup key, decoding...");

            const decodedKey = (0, _olmlib.decodeBase64)(base64Key);

            _logger.logger.info("Decoded backup key, storing...");

            client._crypto.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey));

            _logger.logger.info("Backup key stored. Starting backup restore...");

            const backupInfo = await client.getKeyBackupVersion(); // no need to await for this - just let it go in the bg

            client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => {
              _logger.logger.info("Backup restored.");
            });
          }

          resolve();
        });
        /* We call getCrossSigningKey() for its side-effects */

        return Promise.race([Promise.all([crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("user_signing"), backupKeyPromise]), timeout]).then(resolve, reject);
      }).catch(e => {
        console.warn("VerificationBase: failure while requesting keys:", e);
      }); //#endregion
    }
  }

  cancel(e) {
    this._endTimer(); // always kill the activity timer


    if (!this._done) {
      this.cancelled = true;
      this.request.onVerifierCancelled();

      if (this.userId && this.deviceId) {
        // send a cancellation to the other user (if it wasn't
        // cancelled by the other user)
        if (e === timeoutException) {
          const timeoutEvent = (0, _Error.newTimeoutError)();

          this._send(timeoutEvent.getType(), timeoutEvent.getContent());
        } else if (e instanceof _event.MatrixEvent) {
          const sender = e.getSender();

          if (sender !== this.userId) {
            const content = e.getContent();

            if (e.getType() === "m.key.verification.cancel") {
              content.code = content.code || "m.unknown";
              content.reason = content.reason || content.body || "Unknown reason";

              this._send("m.key.verification.cancel", content);
            } else {
              this._send("m.key.verification.cancel", {
                code: "m.unknown",
                reason: content.body || "Unknown reason"
              });
            }
          }
        } else {
          this._send("m.key.verification.cancel", {
            code: "m.unknown",
            reason: e.toString()
          });
        }
      }

      if (this._promise !== null) {
        // when we cancel without a promise, we end up with a promise
        // but no reject function. If cancel is called again, we'd error.
        if (this._reject) this._reject(e);
      } else {
        // FIXME: this causes an "Uncaught promise" console message
        // if nothing ends up chaining this promise.
        this._promise = Promise.reject(e);
      } // Also emit a 'cancel' event that the app can listen for to detect cancellation
      // before calling verify()


      this.emit('cancel', e);
    }
  }
  /**
   * Begin the key verification
   *
   * @returns {Promise} Promise which resolves when the verification has
   *     completed.
   */


  verify() {
    if (this._promise) return this._promise;
    this._promise = new Promise((resolve, reject) => {
      this._resolve = (...args) => {
        this._done = true;

        this._endTimer();

        resolve(...args);
      };

      this._reject = (...args) => {
        this._done = true;

        this._endTimer();

        reject(...args);
      };
    });

    if (this._doVerification && !this._started) {
      this._started = true;

      this._resetTimer(); // restart the timeout


      Promise.resolve(this._doVerification()).then(this.done.bind(this), this.cancel.bind(this));
    }

    return this._promise;
  }

  async _verifyKeys(userId, keys, verifier) {
    // we try to verify all the keys that we're told about, but we might
    // not know about all of them, so keep track of the keys that we know
    // about, and ignore the rest
    const verifiedDevices = [];

    for (const [keyId, keyInfo] of Object.entries(keys)) {
      const deviceId = keyId.split(':', 2)[1];

      const device = this._baseApis.getStoredDevice(userId, deviceId);

      if (device) {
        await verifier(keyId, device, keyInfo);
        verifiedDevices.push(deviceId);
      } else {
        const crossSigningInfo = this._baseApis._crypto._deviceList.getStoredCrossSigningForUser(userId);

        if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
          await verifier(keyId, _deviceinfo.DeviceInfo.fromStorage({
            keys: {
              [keyId]: deviceId
            }
          }, deviceId), keyInfo);
          verifiedDevices.push(deviceId);
        } else {
          _logger.logger.warn(`verification: Could not find device ${deviceId} to verify`);
        }
      }
    } // if none of the keys could be verified, then error because the app
    // should be informed about that


    if (!verifiedDevices.length) {
      throw new Error("No devices could be verified");
    }

    _logger.logger.info("Verification completed! Marking devices verified: ", verifiedDevices); // TODO: There should probably be a batch version of this, otherwise it's going
    // to upload each signature in a separate API call which is silly because the
    // API supports as many signatures as you like.


    for (const deviceId of verifiedDevices) {
      await this._baseApis.setDeviceVerified(userId, deviceId);
    }
  }

}

exports.VerificationBase = VerificationBase;
(0, _defineProperty2.default)(VerificationBase, "keyRequestTimeoutMs", 1000 * 60);