var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    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) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { bufferToHexString } from '@iotize/common/byte-converter';
import { AesEcb128Converter } from '@iotize/common/crypto';
import { deepCopy } from '@iotize/common/utility';
import { ServiceCallRunner, TapError, TapResponse, TapResponseStatusError, } from '@iotize/tap';
import { ResultCode, } from '@iotize/tap/client/api';
import { CryptedFrameConverter, TapClientError, TapRequestHelper, TapStreamWriter, } from '@iotize/tap/client/impl';
import { ScramService, } from '@iotize/tap/service/impl/scram';
import { defer, throwError } from 'rxjs';
import { map, mergeMap, tap as rxTap, shareReplay } from 'rxjs/operators';
import { debug } from './debug';
import { TapScramError } from './scram-errors';
export const INITIALIZATION_VECTOR_LENGTH = 16;
const DEFAULT_INITIALIZATION_VECTOR = '00000000000000000000000000000000';
const TAG = 'ScramInterceptor';
function _hex(data) {
    return data ? bufferToHexString(data) : '';
}
export class ScramInterceptor {
    constructor(scramService) {
        this.scramService = scramService;
        this._ivFrameCounter = 0;
        this._ivSupported = true;
        this._options = {
            encryption: false,
            initializationVectorResetPeriod: 100,
            keys: {},
            frameCounter: 0,
        };
        this._encryptedFrameConverter = new CryptedFrameConverter({
            next: () => {
                return this._options.frameCounter++;
            },
        });
        this.initializationVectorGenerator = (length) => {
            const data = new Array(length).fill(0).map((_) => {
                return Math.floor(Math.random() * 0xff);
            });
            return Uint8Array.from(data);
        };
    }
    get encryptionAlgo() {
        if (!this._encryptionAlgo) {
            throw TapClientError.illegalStateError(`Encryption algo has not been set yet`);
        }
        return this._encryptionAlgo;
    }
    set ivSupported(v) {
        this._ivSupported = v;
    }
    /**
     * 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 sessionKey(key) {
        this._options.keys.sessionKey = key;
        this._options.frameCounter = 0;
        if (this._options.keys.sessionKey) {
            this.refreshEncryptionAlgo();
        }
        else {
            this.pauseEncryption();
        }
    }
    get sessionKey() {
        return this._options.keys.sessionKey;
    }
    /**
     * Get a copy of encryption options
     */
    get options() {
        return deepCopy(this._options);
    }
    setEncryptionKeys(options) {
        this._options.keys = options;
        this.refreshEncryptionAlgo();
    }
    getEncryptionKeys() {
        return this._options.keys;
    }
    setFrameCounter(value) {
        this._options.frameCounter = value;
    }
    setInitializationVectorRefreshPeriod(period) {
        this._options.initializationVectorResetPeriod = period;
    }
    intercept(context, next) {
        const request = context.request;
        debug(TAG, `exec ${TapRequestHelper.toString(request)} (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)})`);
        let obs;
        if (this._options.encryption && !context.skipEncryption) {
            obs = this._sendWithEncryption(context, next).pipe(map((response) => {
                const data = response.rawBody();
                return {
                    status: response.status,
                    data,
                };
            }));
        }
        else {
            obs = next.handle(context);
        }
        return obs.pipe(rxTap((response) => {
            debug(TAG, `Response to ${TapRequestHelper.toString(request)} => ${response.status} ${bufferToHexString(response.data)}`);
        }));
    }
    _sendWithEncryption(context, next) {
        if (!this._encryptedFrameConverter) {
            return throwError(TapClientError.illegalStateError(`Encrypted frame converter has not been specified yet`));
        }
        let callObs;
        let newClientIV;
        if (this._options.initializationVectorResetPeriod === 1) {
            newClientIV = this.initializationVectorGenerator(INITIALIZATION_VECTOR_LENGTH);
            this._setEncodeIV(newClientIV);
            debug(TAG, `Changing initialization vector for every requests. New client IV: ${_hex(newClientIV)}`);
            const cryptedFrame = this._buildEncryptedFrame(context.request);
            const call = this.scramService.sendWithIVCall({
                request: cryptedFrame,
                iv: newClientIV,
            });
            callObs = this._toCallObservable(Object.assign(Object.assign({}, context), { skipEncryption: true }), next, call).pipe(map((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`)
                //     );
                // }
                const body = response.body();
                debug(TAG, `Refreshing decoding initialization vector: ${_hex(body.iv)}`);
                this._setDecodeIV(body.iv);
                return TapResponse.create(response.status, body.response);
            }));
        }
        else {
            if (this._options.initializationVectorResetPeriod > 1 &&
                this._ivFrameCounter >= this._options.initializationVectorResetPeriod &&
                !isRefreshInitializationVectorRequest(context.request)) {
                debug(TAG, `_ivInterceptor refreshEncryptionInitializationVector is required ${this._ivFrameCounter}/${this._options.initializationVectorResetPeriod}`);
                if (!this._refreshingInitializationVectorObs) {
                    this._refreshingInitializationVectorObs = defer(() => __awaiter(this, void 0, void 0, function* () {
                        try {
                            return yield this.refreshInitializationVectors();
                        }
                        finally {
                            this._refreshingInitializationVectorObs = undefined;
                        }
                    })).pipe(shareReplay({ bufferSize: 1, refCount: true }));
                }
                else {
                    // debug(TAG, `Refreshing initialization vector has already been asked`);
                }
                callObs = this._refreshingInitializationVectorObs.pipe(mergeMap((_) => {
                    const encryptedFrame = this._buildEncryptedFrame(context.request);
                    return this._toCallObservable(context, next, this.scramService.sendCall(encryptedFrame));
                }));
            }
            else {
                const encryptedFrame = this._buildEncryptedFrame(context.request);
                const call = this.scramService.sendCall(encryptedFrame);
                callObs = this._toCallObservable(context, next, call);
            }
        }
        return callObs.pipe(map((encryptedWarpperResponse) => {
            this._ivFrameCounter++;
            // debug(TAG, `Iotize client crypted frame: ${encryptedWarpperResponse}`);
            if (!encryptedWarpperResponse.isSuccessful()) {
                if (encryptedWarpperResponse.codeRet() ===
                    ResultCode.SERVICE_UNAVAILABLE) {
                    throw TapScramError.scramNotStartedYet(context.request);
                }
                else if (encryptedWarpperResponse.codeRet() === ResultCode.BAD_REQUEST) {
                    throw TapScramError.invalidScramKey(context.request);
                }
                encryptedWarpperResponse.successful();
            }
            const cryptedFrameContent = this.encryptionAlgo.decode(encryptedWarpperResponse.rawBody());
            // debug(TAG, `Decrypted frame value: ${_hex(cryptedFrameContent)}`);
            const apduFrame = this._encryptedFrameConverter.decode(cryptedFrameContent);
            // debug(TAG, `apduFrame frame value: ${_hex(apduFrame)}`);
            const tapResponse = context.client.responseDecoder.decode(apduFrame);
            const apiResponse = new TapResponse(tapResponse, context.request);
            // let response: TapResponse<any> = this.lwm2mResponseConverter.decode(lwm2mResponseFrame.data);
            if (context.bodyDecoder) {
                apiResponse.setBodyDecoder(context.bodyDecoder);
            }
            // debug(TAG, `Iotize client decoded response: ${response}`)
            return apiResponse;
        }));
    }
    _buildEncryptedFrame(request) {
        const iotizeFrame = TapStreamWriter.create(10 + request.payload.length).writeTapRequestFrame(request).toBytes;
        const encryptedFrameModel = this._encryptedFrameConverter.encode(iotizeFrame);
        const encryptedFrame = this.encryptionAlgo.encode(encryptedFrameModel);
        return encryptedFrame;
    }
    _setEncodeIV(iv) {
        this._options.keys.ivEncode = iv;
        this.refreshEncryptionAlgo();
    }
    _setDecodeIV(iv) {
        this._options.keys.ivDecode = iv;
        this.refreshEncryptionAlgo();
    }
    _toCallObservable(context, next, call) {
        const tapRequest = ServiceCallRunner.toTapRequest(call);
        const bodyDecoderFct = ServiceCallRunner.resolveResponseBodyDecoder(call);
        const bodyDecoder = bodyDecoderFct
            ? {
                decode: bodyDecoderFct,
            }
            : undefined;
        return next
            .handle(Object.assign(Object.assign({}, context), { request: tapRequest, bodyDecoder }))
            .pipe(map((tapResponse) => {
            return new TapResponse(tapResponse, context.request, bodyDecoder);
        }));
    }
    /**
     * Refresh initialization vectors
     */
    refreshInitializationVectors() {
        return __awaiter(this, void 0, void 0, function* () {
            const clientIV = this.initializationVectorGenerator(INITIALIZATION_VECTOR_LENGTH);
            try {
                debug(TAG, `Performing initialization vector refresh`);
                const serverIV = (yield this.scramService.setInitializationVector(clientIV)).body();
                this._options.keys.ivEncode = clientIV;
                this._options.keys.ivDecode = serverIV;
                this._ivFrameCounter = 0;
                debug(TAG, `Initialization vector refreshed: server ${_hex(serverIV)} client=${_hex(clientIV)})`);
                this.refreshEncryptionAlgo();
            }
            catch (err) {
                debug(TAG, `Cannot refresh initialization vector: ${err.message}`);
                if (err instanceof TapResponseStatusError) {
                    const tapResponseStatus = err.response.status;
                    if (tapResponseStatus === ResultCode.NOT_IMPLEMENTED ||
                        tapResponseStatus === ResultCode.NOT_FOUND) {
                        debug(TAG, `Cannot initialize encryption vectors (firmware version does not support it). Error: ${err.message}`);
                        this._ivSupported = false;
                        throw TapError.initializationVectorNotSupported(err);
                    }
                }
                throw err;
            }
        });
    }
    /**
     * Resume encryption session
     */
    resumeEncryption() {
        debug(TAG, `resume encryption`);
        this._options.encryption = true;
    }
    /**
     * Pause encryption without destorying session (session keys will not be removed)
     */
    pauseEncryption() {
        debug(TAG, `pause encryption`);
        this._options.encryption = false;
    }
    /**
     * Initialize a new encrypted sesssion
     * Session key will be changed. Initialization vector too (if firmware supports it)
     */
    newSession() {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                debug(TAG, `Creating new encryption session`);
                this.clearSession();
                const response = yield this.scramService.initialize();
                response.successful();
                const newSessionKey = response.body();
                this.sessionKey = newSessionKey;
                debug(TAG, `Session key will be ${_hex(newSessionKey)} (length=${newSessionKey.length})`);
                if (this._options.initializationVectorResetPeriod > 0) {
                    try {
                        yield this.refreshInitializationVectors();
                    }
                    catch (err) {
                        // Ignore initialization vector not supported error
                        if (err.code !==
                            TapError.Code.InitializationVectorNotSupported) {
                            throw err;
                        }
                    }
                }
                return newSessionKey;
            }
            catch (err) {
                throw TapError.cannotStartScram(err);
            }
        });
    }
    /**
     * Clear encrypted session.
     * Destroys frame counter, session keys and initialization vectors
     * This will also stop encrypted communication if it was running
     */
    clearSession() {
        this.pauseEncryption();
        this._options.keys.ivDecode = undefined;
        this._options.keys.ivEncode = undefined;
        this._options.keys.sessionKey = undefined;
        this._ivFrameCounter = 0;
    }
    refreshEncryptionAlgo() {
        if (this._options.keys.sessionKey) {
            const algo = new 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;
        }
    }
}
const SCRAM_CALL_FACTORY = new ScramService(undefined);
function isRefreshInitializationVectorRequest(request) {
    return (TapRequestHelper.pathToString(request.header.path) ===
        SCRAM_CALL_FACTORY.resources.setInitializationVector.path);
}
//# sourceMappingURL=data:application/json;base64, 