"use strict";
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
Object.defineProperty(exports, "__esModule", { value: true });
var rxjs_1 = require("rxjs");
var operators_1 = require("rxjs/operators");
var response_1 = require("../../client/api/response");
var impl_1 = require("../../client/impl");
var crypted_frame_converter_1 = require("../../client/impl/converter/crypted-frame-converter");
var lwm2m_command_converter_1 = require("../../client/impl/converter/lwm2m-command-converter");
var lwm2m_response_converter_1 = require("../../client/impl/converter/lwm2m-response-converter");
var crypto_1 = require("../../core/crypto");
var format_helper_1 = require("../../core/format/format-helper");
var logger_1 = require("../../logger");
var service_1 = require("../service");
var tap_errors_1 = require("../tap-errors");
var INITIALIZATION_VECTOR_LENGTH = 16;
var DEFAULT_INITIALIZATION_VECTOR = "00000000000000000000000000000000";
var logger = logger_1.default('ScramInterceptor');
function _hex(data) {
    return data ? format_helper_1.FormatHelper.toHexString(data) : '';
}
var ScramInterceptor = /** @class */ (function () {
    function ScramInterceptor(scramService, apiConfig) {
        var _this = this;
        this.scramService = scramService;
        this.apiConfig = apiConfig;
        this._lwm2mResponseConverter = new lwm2m_response_converter_1.IotizeLWM2MResponseConverter();
        this._lwm2mCommandConverter = new lwm2m_command_converter_1.IotizeLWM2MCommandConverter();
        this._ivFrameCounter = 0;
        this._ivSupported = true;
        this._options = {
            encryption: false,
            initializationVectorResetPeriod: 0,
            keys: {},
            frameCounter: 0
        };
        this._cryptedFrameConverter = new crypted_frame_converter_1.CryptedFrameConverter({
            next: function () {
                return _this._options.frameCounter++;
            }
        });
        this.initializationVectorGenerator = function (length) {
            var data = new Array(length)
                .fill(0)
                .map(function (_) {
                return Math.floor(Math.random() * 0xFF);
            });
            return Uint8Array.from(data);
        };
    }
    ;
    Object.defineProperty(ScramInterceptor.prototype, "ivSupported", {
        set: function (v) {
            this._ivSupported = v;
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(ScramInterceptor.prototype, "sessionKey", {
        get: function () {
            return this._options.keys.sessionKey;
        },
        /**
         * Setter for the session key
         * @param key if null, it will stop encryption and remove session key. If true it will update session key used for encryption
         */
        set: function (key) {
            this._options.keys.sessionKey = key;
            this._options.frameCounter = 0;
            if (this._options.keys.sessionKey) {
                this.refreshEncryptionAlgo();
            }
            else {
                this._stopEncryption();
            }
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(ScramInterceptor.prototype, "options", {
        get: function () {
            return this._options;
        },
        enumerable: true,
        configurable: true
    });
    ScramInterceptor.prototype.setEncryptionKeys = function (options) {
        this._options.keys = options;
        this.refreshEncryptionAlgo();
    };
    ScramInterceptor.prototype.setFrameCounter = function (value) {
        this._options.frameCounter = value;
    };
    ScramInterceptor.prototype.setInitializationVectorRefreshPeriod = function (period) {
        this._options.initializationVectorResetPeriod = period;
    };
    ScramInterceptor.prototype.intercept = function (context, next) {
        var command = context.command;
        logger.debug("exec " + command.toString() + " (encryption:" + this._options.encryption + ", frameCounter: " + this._options.frameCounter + " - skip " + context['skipEncryption'] + ", key: " + _hex(this.sessionKey) + " iv decode: " + _hex(this._options.keys.ivDecode) + ", iv encode: " + _hex(this._options.keys.ivEncode) + ")");
        if (this._options.encryption && !context['skipEncryption']) {
            return this._sendWithEncryption(context, next);
        }
        else {
            return next.handle(context);
        }
    };
    ScramInterceptor.prototype._sendWithEncryption = function (context, next) {
        var _this = this;
        if (!this._cryptedFrameConverter) {
            return rxjs_1.throwError(impl_1.TapClientError.illegalStateError("Crypted convert has not been specified yet"));
        }
        if (!this._encryptionAlgo) {
            return rxjs_1.throwError(impl_1.TapClientError.illegalStateError("Cryto algo has not been specified yet"));
        }
        var callObs;
        var newClientIV;
        if (this._options.initializationVectorResetPeriod === 1) {
            newClientIV = this.initializationVectorGenerator(INITIALIZATION_VECTOR_LENGTH);
            this._setEncodeIV(newClientIV);
            logger.debug("Changing initialization vector for every requests. New client IV: " + _hex(newClientIV));
            var cryptedFrame = this._buildEncryptedFrame(context.command);
            var call = this.scramService.sendWithIVCall({
                command: cryptedFrame,
                iv: newClientIV
            });
            callObs = this._toCallObservable(__assign({}, context, { skipEncryption: true }), next, call)
                .pipe(operators_1.map(function (response) {
                // if (response.isSuccessful() && response.rawBody().length < INITIALIZATION_VECTOR_LENGTH){
                //     // tap firmware version is too old apparentl. 
                //     // CCOM IV resource was already used to generate rand number before firmware version 1.83, thats why there is a success code result
                //     throw TapError.initializationVectorNotSupported(
                //         new Error(`Tap response is too short`)
                //     );
                // }
                var body = response.body();
                logger.debug("Refreshing decoding initialization vector: " + _hex(body.iv));
                _this._setDecodeIV(body.iv);
                return impl_1.ResponseImpl.create(response_1.ResultCode.IOTIZE_205_CONTENT, body.response);
            }));
        }
        else {
            if (this._options.initializationVectorResetPeriod > 1 && this._ivFrameCounter >= this._options.initializationVectorResetPeriod) {
                logger.debug("sending with iv period " + this._ivFrameCounter + "/" + this._options.initializationVectorResetPeriod);
                this._ivFrameCounter = -1;
                logger.debug("_ivInterceptor refreshEncryptionInitializationVector");
                callObs = rxjs_1.from(this.refreshEncryptionInitializationVector())
                    .pipe(
                // catchError((err) => {
                //     logger.debug(`refreshEncryptionInitializationVector failed: ${err.message}`);
                //     if (err.code === TapResponseErrorCode) {
                //         let tapResultCode = (err as ResponseError).response.codeRet();
                //         if (tapResultCode == ResultCode.IOTIZE_501_NOT_IMPLEMENTED
                //             || ResultCode.IOTIZE_404_NOT_FOUND) {
                //             this._ivSupported = false;
                //             return of(1)
                //         }
                //     }
                //     return of(err);
                // }),
                operators_1.flatMap(function (_) {
                    var encryptedFrame = _this._buildEncryptedFrame(context.command);
                    return _this._toCallObservable(context, next, _this.scramService.sendCall(encryptedFrame));
                }));
            }
            else {
                var encryptedFrame = this._buildEncryptedFrame(context.command);
                var call = this.scramService.sendCall(encryptedFrame);
                callObs = this._toCallObservable(context, next, call);
            }
        }
        return callObs
            .pipe(operators_1.map(function (encrypterWarpperResponse) {
            _this._ivFrameCounter++;
            // logger.debug(`Iotize client crypted frame: ${crypterWarpperResponse}`);
            encrypterWarpperResponse.successful();
            if (!_this._encryptionAlgo) {
                throw impl_1.TapClientError.illegalStateError("Encryption algo has not been set yet");
            }
            var cryptedFrameContent = _this._encryptionAlgo.decode(encrypterWarpperResponse.rawBody());
            // logger.debug(`Decrypted frame value: ${_hex(cryptedFrameContent)}`);
            var apduFrame = _this._cryptedFrameConverter.decode(cryptedFrameContent);
            // logger.debug(`apduFrame frame value: ${_hex(apduFrame)}`);
            var response = context.client.responseDecoder.decode(apduFrame);
            // let response: ResponseInterface<any> = this.lwm2mResponseConverter.decode(lwm2mResponseFrame.data);
            if (context.bodyDecoder) {
                if (response instanceof impl_1.ResponseImpl) {
                    response.setBodyDecoder(context.bodyDecoder);
                }
            }
            // logger.debug(`Iotize client decoded response: ${response}`)
            return response;
        }));
    };
    ScramInterceptor.prototype._buildEncryptedFrame = function (command) {
        var iotizeFrame = this._lwm2mCommandConverter.encode(command);
        var encryptedFrameModel = this._cryptedFrameConverter.encode(iotizeFrame);
        var encryptedFrame = this._encryptionAlgo.encode(encryptedFrameModel);
        return encryptedFrame;
    };
    ScramInterceptor.prototype._setEncodeIV = function (iv) {
        this._options.keys.ivEncode = iv;
        this.refreshEncryptionAlgo();
    };
    ScramInterceptor.prototype._setDecodeIV = function (iv) {
        this._options.keys.ivDecode = iv;
        this.refreshEncryptionAlgo();
    };
    ScramInterceptor.prototype._toCallObservable = function (context, next, call) {
        var _a = service_1.AbstractService.callToCommandArgs(call, this.apiConfig), request = _a[0], bodyDecoder = _a[1];
        return next
            .handle(__assign({}, context, { command: request, bodyDecoder: bodyDecoder }));
    };
    ScramInterceptor.prototype.refreshEncryptionInitializationVector = function () {
        return __awaiter(this, void 0, void 0, function () {
            var clientIV, serverIV, err_1, tapResponseError, codeRet;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        clientIV = this.initializationVectorGenerator(INITIALIZATION_VECTOR_LENGTH);
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4 /*yield*/, this.scramService.setInitializationVector(clientIV)];
                    case 2:
                        serverIV = (_a.sent()).body();
                        logger.debug("Initialization vector refreshed: server " + _hex(serverIV) + " client=" + _hex(clientIV) + ")");
                        this._options.keys.ivEncode = clientIV;
                        this._options.keys.ivDecode = serverIV;
                        this._ivFrameCounter = 0;
                        this.refreshEncryptionAlgo();
                        return [3 /*break*/, 4];
                    case 3:
                        err_1 = _a.sent();
                        if (err_1.code !== response_1.TapResponseErrorCode) {
                            throw tap_errors_1.TapError.cannotStartScram(err_1);
                        }
                        tapResponseError = err_1;
                        codeRet = tapResponseError.response.codeRet();
                        if (codeRet == response_1.ResultCode.IOTIZE_501_NOT_IMPLEMENTED) {
                            logger.debug("Cannot initialize encryption vectors (firmware version does not support it). Error: " + err_1.message);
                            this._ivSupported = false;
                            throw tap_errors_1.TapError.initializationVectorNotSupported(tapResponseError);
                        }
                        else {
                            throw tap_errors_1.TapError.cannotStartScram(err_1);
                        }
                        return [3 /*break*/, 4];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    ScramInterceptor.prototype.refreshEncryptionAlgo = function () {
        if (this._options.keys.sessionKey) {
            var algo = new crypto_1.AesEcb128Converter({
                key: _hex(this._options.keys.sessionKey),
                ivDecode: this._options.keys.ivDecode
                    ? _hex(this._options.keys.ivDecode)
                    : DEFAULT_INITIALIZATION_VECTOR,
                ivEncode: this._options.keys.ivEncode
                    ? _hex(this._options.keys.ivEncode)
                    : DEFAULT_INITIALIZATION_VECTOR
            });
            this._encryptionAlgo = algo;
        }
    };
    ScramInterceptor.prototype._startEncryption = function () {
        logger.debug("_startEncryption");
        this._options.encryption = true;
        return Promise.resolve();
    };
    ScramInterceptor.prototype._stopEncryption = function () {
        logger.debug("_stopEncryption");
        this._options.encryption = false;
        return Promise.resolve();
    };
    /**
     * Enable/Disable encrytion when communicating with a device
     * @param enable true if requests must be encrypted
     * @param resetSessionKey true if you want to reset the the session key.
     * @param refreshInitializationVector true if you want to change initialization vectors
     *
     * @deprecated use {@link encryption()} instead
     */
    ScramInterceptor.prototype.encryption = function (enable, resetSessionKey, refreshInitializationVector) {
        if (resetSessionKey === void 0) { resetSessionKey = false; }
        if (refreshInitializationVector === void 0) { refreshInitializationVector = false; }
        return __awaiter(this, void 0, void 0, function () {
            var response, newSessionKey, err_2, err_3;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!enable) return [3 /*break*/, 5];
                        if (!(resetSessionKey || !this._options.keys.sessionKey)) return [3 /*break*/, 4];
                        logger.debug("No session key defined. Using random com start");
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4 /*yield*/, this.scramService.initialize()];
                    case 2:
                        response = (_a.sent());
                        if (!this._options.encryption) {
                            this._options.keys.ivDecode = undefined;
                            this._options.keys.ivEncode = undefined;
                            this._ivFrameCounter = 0;
                        }
                        response.successful();
                        newSessionKey = response.rawBody();
                        this.sessionKey = newSessionKey;
                        logger.debug("Session key will be " + _hex(newSessionKey) + " (length=" + newSessionKey.length + ")");
                        return [3 /*break*/, 4];
                    case 3:
                        err_2 = _a.sent();
                        throw tap_errors_1.TapError.cannotStartScram(err_2);
                    case 4: return [3 /*break*/, 6];
                    case 5:
                        if (resetSessionKey) {
                            this._options.keys.sessionKey = undefined;
                        }
                        if (refreshInitializationVector) {
                            this._options.keys.ivDecode = undefined;
                            this._options.keys.ivEncode = undefined;
                            this._ivFrameCounter = 0;
                        }
                        _a.label = 6;
                    case 6:
                        if (enable) {
                            this._startEncryption();
                        }
                        else {
                            this._stopEncryption();
                        }
                        if (!(refreshInitializationVector && (this._ivSupported && !this._options.keys.ivEncode || !this._options.keys.ivDecode))) return [3 /*break*/, 10];
                        _a.label = 7;
                    case 7:
                        _a.trys.push([7, 9, , 10]);
                        return [4 /*yield*/, this.refreshEncryptionInitializationVector()];
                    case 8:
                        _a.sent();
                        return [3 /*break*/, 10];
                    case 9:
                        err_3 = _a.sent();
                        // Ignore initialization vector not supported error
                        if (err_3.code != tap_errors_1.TapError.Code.InitializationVectorNotSupported) {
                            throw err_3;
                        }
                        return [3 /*break*/, 10];
                    case 10: return [2 /*return*/];
                }
            });
        });
    };
    return ScramInterceptor;
}());
exports.ScramInterceptor = ScramInterceptor;
