/**
 * Coding algorithms: Base64, Hex, Int16, Chars, BER and PEM
 * version 1.76
 * 2014-2016, Rudolf Nickolaev. All rights reserved.
 * 
 * Exported for CyberChef by mshwed [m@ttshwed.com]
 */

/*
 * 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.
 *    
 * THIS SOfTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES Of MERCHANTABILITY AND fITNESS fOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * fOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT Of SUBSTITUTE GOODS OR
 * SERVICES; LOSS Of USE, DATA, OR PROfITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY Of LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT Of THE USE
 * Of THIS SOfTWARE, EVEN If ADVISED Of THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */

import gostCrypto from './gostCrypto.mjs';

/**
 * The Coding interface provides string converting methods: Base64, Hex, 
 * Int16, Chars, BER and PEM
 * @class GostCoding
 * 
 */ // <editor-fold defaultstate="collapsed">
var root = {};
var DataError = Error;
var CryptoOperationData = ArrayBuffer;
var Date = Date;

function buffer(d) {
    if (d instanceof CryptoOperationData)
        return d;
    else if (d && d.buffer && d.buffer instanceof CryptoOperationData)
        return d.byteOffset === 0 && d.byteLength === d.buffer.byteLength ?
                d.buffer : new Uint8Array(new Uint8Array(d, d.byteOffset, d.byteLength)).buffer;
    else
        throw new DataError('CryptoOperationData required');
} // </editor-fold>

function GostCoding() {
}

/**
 * BASE64 conversion
 * 
 * @class GostCoding.Base64
 */
var Base64 = {// <editor-fold defaultstate="collapsed">
    /**
     * Base64.decode convert BASE64 string s to CryptoOperationData
     * 
     * @memberOf GostCoding.Base64
     * @param {String} s BASE64 encoded string value
     * @returns {CryptoOperationData} Binary decoded data
     */
    decode: function (s) {
        s = s.replace(/[^A-Za-z0-9\+\/]/g, '');
        var n = s.length,
                k = n * 3 + 1 >> 2, r = new Uint8Array(k);

        for (var m3, m4, u24 = 0, j = 0, i = 0; i < n; i++) {
            m4 = i & 3;
            var c = s.charCodeAt(i);

            c = c > 64 && c < 91 ?
                    c - 65 : c > 96 && c < 123 ?
                    c - 71 : c > 47 && c < 58 ?
                    c + 4 : c === 43 ?
                    62 : c === 47 ?
                    63 : 0;

            u24 |= c << 18 - 6 * m4;
            if (m4 === 3 || n - i === 1) {
                for (m3 = 0; m3 < 3 && j < k; m3++, j++) {
                    r[j] = u24 >>> (16 >>> m3 & 24) & 255;
                }
                u24 = 0;

            }
        }
        return r.buffer;
    },
    /**
     * Base64.encode(data) convert CryptoOperationData data to BASE64 string
     * 
     * @memberOf GostCoding.Base64
     * @param {CryptoOperationData} data Bynary data for encoding
     * @returns {String} BASE64 encoded data
     */
    encode: function (data) {
        var slen = 8, d = new Uint8Array(buffer(data));
        var m3 = 2, s = '';
        for (var n = d.length, u24 = 0, i = 0; i < n; i++) {
            m3 = i % 3;
            if (i > 0 && (i * 4 / 3) % (12 * slen) === 0)
                s += '\r\n';
            u24 |= d[i] << (16 >>> m3 & 24);
            if (m3 === 2 || n - i === 1) {
                for (var j = 18; j >= 0; j -= 6) {
                    var c = u24 >>> j & 63;
                    c = c < 26 ? c + 65 : c < 52 ? c + 71 : c < 62 ? c - 4 :
                            c === 62 ? 43 : c === 63 ? 47 : 65;
                    s += String.fromCharCode(c);
                }
                u24 = 0;
            }
        }
        return s.substr(0, s.length - 2 + m3) + (m3 === 2 ? '' : m3 === 1 ? '=' : '==');
    } // </editor-fold>
};

/**
 * BASE64 conversion
 * 
 * @memberOf GostCoding
 * @insnance
 * @type GostCoding.Base64
 */
GostCoding.prototype.Base64 = Base64;

/**
 * Text string conversion <br>
 * Methods support charsets: ascii, win1251, utf8, utf16 (ucs2, unicode), utf32 (ucs4)
 * 
 * @class GostCoding.Chars
 */
var Chars = (function () { // <editor-fold defaultstate="collapsed">

    var _win1251_ = {
        0x402: 0x80, 0x403: 0x81, 0x201A: 0x82, 0x453: 0x83, 0x201E: 0x84, 0x2026: 0x85, 0x2020: 0x86, 0x2021: 0x87,
        0x20AC: 0x88, 0x2030: 0x89, 0x409: 0x8A, 0x2039: 0x8B, 0x40A: 0x8C, 0x40C: 0x8D, 0x40B: 0x8E, 0x40f: 0x8f,
        0x452: 0x90, 0x2018: 0x91, 0x2019: 0x92, 0x201C: 0x93, 0x201D: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97,
        0x2122: 0x99, 0x459: 0x9A, 0x203A: 0x9B, 0x45A: 0x9C, 0x45C: 0x9D, 0x45B: 0x9E, 0x45f: 0x9f,
        0xA0: 0xA0, 0x40E: 0xA1, 0x45E: 0xA2, 0x408: 0xA3, 0xA4: 0xA4, 0x490: 0xA5, 0xA6: 0xA6, 0xA7: 0xA7,
        0x401: 0xA8, 0xA9: 0xA9, 0x404: 0xAA, 0xAB: 0xAB, 0xAC: 0xAC, 0xAD: 0xAD, 0xAE: 0xAE, 0x407: 0xAf,
        0xB0: 0xB0, 0xB1: 0xB1, 0x406: 0xB2, 0x456: 0xB3, 0x491: 0xB4, 0xB5: 0xB5, 0xB6: 0xB6, 0xB7: 0xB7,
        0x451: 0xB8, 0x2116: 0xB9, 0x454: 0xBA, 0xBB: 0xBB, 0x458: 0xBC, 0x405: 0xBD, 0x455: 0xBE, 0x457: 0xBf
    };
    var _win1251back_ = {};
    for (var from in _win1251_) {
        var to = _win1251_[from];
        _win1251back_[to] = from;
    }

    return {
        /**
         * Chars.decode(s, charset) convert string s with defined charset to CryptoOperationData 
         * 
         * @memberOf GostCoding.Chars
         * @param {string} s Javascript string
         * @param {string} charset Charset, default 'win1251'
         * @returns {CryptoOperationData} Decoded binary data
         */
        decode: function (s, charset) {
            charset = (charset || 'win1251').toLowerCase().replace('-', '');
            var r = [];
            for (var i = 0, j = s.length; i < j; i++) {
                var c = s.charCodeAt(i);
                if (charset === 'utf8') {
                    if (c < 0x80) {
                        r.push(c);
                    } else if (c < 0x800) {
                        r.push(0xc0 + (c >>> 6));
                        r.push(0x80 + (c & 63));
                    } else if (c < 0x10000) {
                        r.push(0xe0 + (c >>> 12));
                        r.push(0x80 + (c >>> 6 & 63));
                        r.push(0x80 + (c & 63));
                    } else if (c < 0x200000) {
                        r.push(0xf0 + (c >>> 18));
                        r.push(0x80 + (c >>> 12 & 63));
                        r.push(0x80 + (c >>> 6 & 63));
                        r.push(0x80 + (c & 63));
                    } else if (c < 0x4000000) {
                        r.push(0xf8 + (c >>> 24));
                        r.push(0x80 + (c >>> 18 & 63));
                        r.push(0x80 + (c >>> 12 & 63));
                        r.push(0x80 + (c >>> 6 & 63));
                        r.push(0x80 + (c & 63));
                    } else {
                        r.push(0xfc + (c >>> 30));
                        r.push(0x80 + (c >>> 24 & 63));
                        r.push(0x80 + (c >>> 18 & 63));
                        r.push(0x80 + (c >>> 12 & 63));
                        r.push(0x80 + (c >>> 6 & 63));
                        r.push(0x80 + (c & 63));
                    }
                } else if (charset === 'unicode' || charset === 'ucs2' || charset === 'utf16') {
                    if (c < 0xD800 || (c >= 0xE000 && c <= 0x10000)) {
                        r.push(c >>> 8);
                        r.push(c & 0xff);
                    } else if (c >= 0x10000 && c < 0x110000) {
                        c -= 0x10000;
                        var first = ((0xffc00 & c) >> 10) + 0xD800;
                        var second = (0x3ff & c) + 0xDC00;
                        r.push(first >>> 8);
                        r.push(first & 0xff);
                        r.push(second >>> 8);
                        r.push(second & 0xff);
                    }
                } else if (charset === 'utf32' || charset === 'ucs4') {
                    r.push(c >>> 24 & 0xff);
                    r.push(c >>> 16 & 0xff);
                    r.push(c >>> 8 & 0xff);
                    r.push(c & 0xff);
                } else if (charset === 'win1251') {
                    if (c >= 0x80) {
                        if (c >= 0x410 && c < 0x450) // А..Яа..я
                            c -= 0x350;
                        else
                            c = _win1251_[c] || 0;
                    }
                    r.push(c);
                } else
                    r.push(c & 0xff);
            }
            return new Uint8Array(r).buffer;
        },
        /**
         * Chars.encode(data, charset) convert CryptoOperationData data to string with defined charset
         * 
         * @memberOf GostCoding.Chars
         * @param {CryptoOperationData} data Binary data
         * @param {string} charset Charset, default win1251
         * @returns {string} Encoded javascript string
         */
        encode: function (data, charset) {
            charset = (charset || 'win1251').toLowerCase().replace('-', '');
            var r = [], d = new Uint8Array(buffer(data));
            for (var i = 0, n = d.length; i < n; i++) {
                var c = d[i];
                if (charset === 'utf8') {
                    c = c >= 0xfc && c < 0xfe && i + 5 < n ? // six bytes
                            (c - 0xfc) * 1073741824 + (d[++i] - 0x80 << 24) + (d[++i] - 0x80 << 18) + (d[++i] - 0x80 << 12) + (d[++i] - 0x80 << 6) + d[++i] - 0x80
                            : c >> 0xf8 && c < 0xfc && i + 4 < n ? // five bytes 
                            (c - 0xf8 << 24) + (d[++i] - 0x80 << 18) + (d[++i] - 0x80 << 12) + (d[++i] - 0x80 << 6) + d[++i] - 0x80
                            : c >> 0xf0 && c < 0xf8 && i + 3 < n ? // four bytes 
                            (c - 0xf0 << 18) + (d[++i] - 0x80 << 12) + (d[++i] - 0x80 << 6) + d[++i] - 0x80
                            : c >= 0xe0 && c < 0xf0 && i + 2 < n ? // three bytes 
                            (c - 0xe0 << 12) + (d[++i] - 0x80 << 6) + d[++i] - 0x80
                            : c >= 0xc0 && c < 0xe0 && i + 1 < n ? // two bytes 
                            (c - 0xc0 << 6) + d[++i] - 0x80
                            : c; // one byte 
                } else if (charset === 'unicode' || charset === 'ucs2' || charset === 'utf16') {
                    c = (c << 8) + d[++i];
                    if (c >= 0xD800 && c < 0xE000) {
                        var first = (c - 0xD800) << 10;
                        c = d[++i];
                        c = (c << 8) + d[++i];
                        var second = c - 0xDC00;
                        c = first + second + 0x10000;
                    }
                } else if (charset === 'utf32' || charset === 'ucs4') {
                    c = (c << 8) + d[++i];
                    c = (c << 8) + d[++i];
                    c = (c << 8) + d[++i];
                } else if (charset === 'win1251') {
                    if (c >= 0x80) {
                        if (c >= 0xC0 && c < 0x100)
                            c += 0x350; // А..Яа..я
                        else
                            c = _win1251back_[c] || 0;
                    }
                }
                r.push(String.fromCharCode(c));
            }
            return r.join('');
        }
    }; // </editor-fold>
})();

/**
 * Text string conversion
 * 
 * @memberOf GostCoding
 * @insnance
 * @type GostCoding.Chars
 */
GostCoding.prototype.Chars = Chars;

/**
 * HEX conversion
 * 
 * @class GostCoding.Hex
 */
var Hex = {// <editor-fold defaultstate="collapsed">
    /**
     * Hex.decode(s, endean) convert HEX string s to CryptoOperationData in endean mode
     * 
     * @memberOf GostCoding.Hex
     * @param {string} s Hex encoded string
     * @param {boolean} endean Little or Big Endean, default Little
     * @returns {CryptoOperationData} Decoded binary data
     */
    decode: function (s, endean) {
        s = s.replace(/[^A-fa-f0-9]/g, '');
        var n = Math.ceil(s.length / 2), r = new Uint8Array(n);
        s = (s.length % 2 > 0 ? '0' : '') + s;
        if (endean && ((typeof endean !== 'string') ||
                (endean.toLowerCase().indexOf('little') < 0)))
            for (var i = 0; i < n; i++)
                r[i] = parseInt(s.substr((n - i - 1) * 2, 2), 16);
        else
            for (var i = 0; i < n; i++)
                r[i] = parseInt(s.substr(i * 2, 2), 16);
        return r.buffer;
    },
    /**
     * Hex.encode(data, endean) convert CryptoOperationData data to HEX string in endean mode
     * 
     * @memberOf GostCoding.Hex 
     * @param {CryptoOperationData} data Binary data
     * @param {boolean} endean Little/Big Endean, default Little
     * @returns {string} Hex decoded string
     */
    encode: function (data, endean) {
        var s = [], d = new Uint8Array(buffer(data)), n = d.length;
        if (endean && ((typeof endean !== 'string') ||
                (endean.toLowerCase().indexOf('little') < 0)))
            for (var i = 0; i < n; i++) {
                var j = n - i - 1;
                s[j] = (j > 0 && j % 32 === 0 ? '\r\n' : '') +
                        ('00' + d[i].toString(16)).slice(-2);
            }
        else
            for (var i = 0; i < n; i++)
                s[i] = (i > 0 && i % 32 === 0 ? '\r\n' : '') +
                        ('00' + d[i].toString(16)).slice(-2);
        return s.join('');
    } // </editor-fold>
};

/**
 *  HEX conversion
 * @memberOf GostCoding
 * @insnance
 * @type GostCoding.Hex
 */
GostCoding.prototype.Hex = Hex;

/**
 * String hex-encoded integer conversion
 * 
 * @class GostCoding.Int16
 */
var Int16 = {// <editor-fold defaultstate="collapsed">
    /**
     * Int16.decode(s) convert hex big insteger s to CryptoOperationData
     * 
     * @memberOf GostCoding.Int16 
     * @param {string} s Int16 string 
     * @returns {CryptoOperationData} Decoded binary data
     */
    decode: function (s) {
        s = (s || '').replace(/[^\-A-fa-f0-9]/g, '');
        if (s.length === 0)
            s = '0';
        // Signature
        var neg = false;
        if (s.charAt(0) === '-') {
            neg = true;
            s = s.substring(1);
        }
        // Align 2 chars
        while (s.charAt(0) === '0' && s.length > 1)
            s = s.substring(1);
        s = (s.length % 2 > 0 ? '0' : '') + s;
        // Padding for singanuture
        // '800000' - 'ffffff' - for positive
        // '800001' - 'ffffff' - for negative
        if ((!neg && !/^[0-7]/.test(s)) ||
                (neg && !/^[0-7]|8[0]+$/.test(s)))
            s = '00' + s;
        // Convert hex
        var n = s.length / 2, r = new Uint8Array(n), t = 0;
        for (var i = n - 1; i >= 0; --i) {
            var c = parseInt(s.substr(i * 2, 2), 16);
            if (neg && (c + t > 0)) {
                c = 256 - c - t;
                t = 1;
            }
            r[i] = c;
        }
        return r.buffer;
    },
    /**
     * Int16.encode(data) convert CryptoOperationData data to big integer hex string
     * 
     * @memberOf GostCoding.Int16
     * @param {CryptoOperationData} data Binary data
     * @returns {string} Int16 encoded string
     */
    encode: function (data) {
        var d = new Uint8Array(buffer(data)), n = d.length;
        if (d.length === 0)
            return '0x00';
        var s = [], neg = d[0] > 0x7f, t = 0;
        for (var i = n - 1; i >= 0; --i) {
            var v = d[i];
            if (neg && (v + t > 0)) {
                v = 256 - v - t;
                t = 1;
            }
            s[i] = ('00' + v.toString(16)).slice(-2);
        }
        s = s.join('');
        while (s.charAt(0) === '0')
            s = s.substring(1);
        return (neg ? '-' : '') + '0x' + s;
    } // </editor-fold>
};

/**
 * String hex-encoded integer conversion
 * @memberOf GostCoding
 * @insnance
 * @type GostCoding.Int16
 */
GostCoding.prototype.Int16 = Int16;

/**
 * BER, DER, CER conversion
 * 
 * @class GostCoding.BER
 */
var BER = (function () { // <editor-fold defaultstate="collapsed">

    // Predefenition block
    function encodeBER(source, format, onlyContent) {
        // Correct primitive type
        var object = source.object;
        if (object === undefined)
            object = source;

        // Determinate tagClass
        var tagClass = source.tagClass = source.tagClass || 0; // Universial default

        // Determinate tagNumber. Use only for Universal class
        if (tagClass === 0) {
            var tagNumber = source.tagNumber;
            if (typeof tagNumber === 'undefined') {
                if (typeof object === 'string') {
                    if (object === '')   // NULL
                        tagNumber = 0x05;
                    else if (/^\-?0x[0-9a-fA-F]+$/.test(object)) // INTEGER
                        tagNumber = 0x02;
                    else if (/^(\d+\.)+\d+$/.test(object)) // OID
                        tagNumber = 0x06;
                    else if (/^[01]+$/.test(object)) // BIT STRING
                        tagNumber = 0x03;
                    else if (/^(true|false)$/.test(object)) // BOOLEAN
                        tagNumber = 0x01;
                    else if (/^[0-9a-fA-F]+$/.test(object)) // OCTET STRING
                        tagNumber = 0x04;
                    else
                        tagNumber = 0x13; // Printable string (later can be changed to UTF8String)
                } else if (typeof object === 'number') { // INTEGER
                    tagNumber = 0x02;
                } else if (typeof object === 'boolean') { // BOOLEAN
                    tagNumber = 0x01;
                } else if (object instanceof Array) { // SEQUENCE
                    tagNumber = 0x10;
                } else if (object instanceof Date) { // GeneralizedTime
                    tagNumber = 0x18;
                } else if (object instanceof CryptoOperationData || (object && object.buffer instanceof CryptoOperationData)) {
                    tagNumber = 0x04;
                } else
                    throw new DataError('Unrecognized type for ' + object);
            }
        }

        // Determinate constructed
        var tagConstructed = source.tagConstructed;
        if (typeof tagConstructed === 'undefined')
            tagConstructed = source.tagConstructed = object instanceof Array;

        // Create content
        var content;
        if (object instanceof CryptoOperationData || (object && object.buffer instanceof CryptoOperationData)) { // Direct
            content = new Uint8Array(buffer(object));
            if (tagNumber === 0x03) { // BITSTRING
                // Set unused bits
                var a = new Uint8Array(buffer(content));
                content = new Uint8Array(a.length + 1);
                content[0] = 0; // No unused bits
                content.set(a, 1);
            }
        } else if (tagConstructed) { // Sub items coding
            if (object instanceof Array) {
                var bytelen = 0, ba = [], offset = 0;
                for (var i = 0, n = object.length; i < n; i++) {
                    ba[i] = encodeBER(object[i], format);
                    bytelen += ba[i].length;
                }
                if (tagNumber === 0x11)
                    ba.sort(function (a, b) { // Sort order for SET components
                        for (var i = 0, n = Math.min(a.length, b.length); i < n; i++) {
                            var r = a[i] - b[i];
                            if (r !== 0)
                                return r;
                        }
                        return a.length - b.length;
                    });
                if (format === 'CER') { // final for CER 00 00
                    ba[n] = new Uint8Array(2);
                    bytelen += 2;
                }
                content = new Uint8Array(bytelen);
                for (var i = 0, n = ba.length; i < n; i++) {
                    content.set(ba[i], offset);
                    offset = offset + ba[i].length;
                }
            } else
                throw new DataError('Constracted block can\'t be primitive');
        } else {
            switch (tagNumber) {
                // 0x00: // EOC
                case 0x01: // BOOLEAN
                    content = new Uint8Array(1);
                    content[0] = object ? 0xff : 0;
                    break;
                case 0x02: // INTEGER
                case 0x0a: // ENUMIRATED
                    content = Int16.decode(
                            typeof object === 'number' ? object.toString(16) : object);
                    break;
                case 0x03: // BIT STRING
                    if (typeof object === 'string') {
                        var unusedBits = 7 - (object.length + 7) % 8;
                        var n = Math.ceil(object.length / 8);
                        content = new Uint8Array(n + 1);
                        content[0] = unusedBits;
                        for (var i = 0; i < n; i++) {
                            var c = 0;
                            for (var j = 0; j < 8; j++) {
                                var k = i * 8 + j;
                                c = (c << 1) + (k < object.length ? (object.charAt(k) === '1' ? 1 : 0) : 0);
                            }
                            content[i + 1] = c;
                        }
                    }
                    break;
                case 0x04:
                    content = Hex.decode(
                            typeof object === 'number' ? object.toString(16) : object);
                    break;
                    // case 0x05: // NULL
                case 0x06: // OBJECT IDENTIFIER
                    var a = object.match(/\d+/g), r = [];
                    for (var i = 1; i < a.length; i++) {
                        var n = +a[i], r1 = [];
                        if (i === 1)
                            n = n + a[0] * 40;
                        do {
                            r1.push(n & 0x7F);
                            n = n >>> 7;
                        } while (n);
                        // reverse order
                        for (j = r1.length - 1; j >= 0; --j)
                            r.push(r1[j] + (j === 0 ? 0x00 : 0x80));
                    }
                    content = new Uint8Array(r);
                    break;
                    // case 0x07: // ObjectDescriptor
                    // case 0x08: // EXTERNAL
                    // case 0x09: // REAL
                    // case 0x0A: // ENUMERATED
                    // case 0x0B: // EMBEDDED PDV
                case 0x0C: // UTF8String
                    content = Chars.decode(object, 'utf8');
                    break;
                    // case 0x10: // SEQUENCE
                    // case 0x11: // SET
                case 0x12: // NumericString
                case 0x16: // IA5String // ASCII
                case 0x13: // PrintableString // ASCII subset
                case 0x14: // TeletexString // aka T61String
                case 0x15: // VideotexString
                case 0x19: // GraphicString
                case 0x1A: // VisibleString // ASCII subset
                case 0x1B: // GeneralString
                    // Reflect on character encoding
                    for (var i = 0, n = object.length; i < n; i++)
                        if (object.charCodeAt(i) > 255)
                            tagNumber = 0x0C;
                    if (tagNumber === 0x0C)
                        content = Chars.decode(object, 'utf8');
                    else
                        content = Chars.decode(object, 'ascii');
                    break;
                case 0x17: // UTCTime
                case 0x18: // GeneralizedTime
                    var result = object.original;
                    if (!result) {
                        var date = new Date(object);
                        date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); // to UTC
                        var ms = tagNumber === 0x18 ? date.getMilliseconds().toString() : ''; // Milliseconds, remove trailing zeros
                        while (ms.length > 0 && ms.charAt(ms.length - 1) === '0')
                            ms = ms.substring(0, ms.length - 1);
                        if (ms.length > 0)
                            ms = '.' + ms;
                        result = (tagNumber === 0x17 ? date.getYear().toString().slice(-2) : date.getFullYear().toString()) +
                                ('00' + (date.getMonth() + 1)).slice(-2) +
                                ('00' + date.getDate()).slice(-2) +
                                ('00' + date.getHours()).slice(-2) +
                                ('00' + date.getMinutes()).slice(-2) +
                                ('00' + date.getSeconds()).slice(-2) + ms + 'Z';
                    }
                    content = Chars.decode(result, 'ascii');
                    break;
                case 0x1C: // UniversalString
                    content = Chars.decode(object, 'utf32');
                    break;
                case 0x1E: // BMPString
                    content = Chars.decode(object, 'utf16');
                    break;
            }
        }

        if (!content)
            content = new Uint8Array(0);
        if (content instanceof CryptoOperationData)
            content = new Uint8Array(content);

        if (!tagConstructed && format === 'CER') {
            // Encoding CER-form for string types
            var k;
            switch (tagNumber) {
                case 0x03: // BIT_STRING
                    k = 1; // ingnore unused bit for bit string
                case 0x04: // OCTET_STRING
                case 0x0C: // UTF8String
                case 0x12: // NumericString
                case 0x13: // PrintableString
                case 0x14: // TeletexString
                case 0x15: // VideotexString
                case 0x16: // IA5String
                case 0x19: // GraphicString
                case 0x1A: // VisibleString
                case 0x1B: // GeneralString
                case 0x1C: // UniversalString
                case 0x1E: // BMPString
                    k = k || 0;
                    // Split content on 1000 octet len parts 
                    var size = 1000;
                    var bytelen = 0, ba = [], offset = 0;
                    for (var i = k, n = content.length; i < n; i += size - k) {
                        ba[i] = encodeBER({
                            object: new Unit8Array(content.buffer, i, Math.min(size - k, n - i)),
                            tagNumber: tagNumber,
                            tagClass: 0,
                            tagConstructed: false
                        }, format);
                        bytelen += ba[i].length;
                    }
                    ba[n] = new Uint8Array(2); // final for CER 00 00
                    bytelen += 2;
                    content = new Uint8Array(bytelen);
                    for (var i = 0, n = ba.length; i < n; i++) {
                        content.set(ba[i], offset);
                        offset = offset + ba[i].length;
                    }
            }
        }

        // Restore tagNumber for all classes
        if (tagClass === 0)
            source.tagNumber = tagNumber;
        else
            source.tagNumber = tagNumber = source.tagNumber || 0;
        source.content = content;

        if (onlyContent)
            return content;

        // Create header
        // tagNumber
        var ha = [], first = tagClass === 3 ? 0xC0 : tagClass === 2 ? 0x80 :
                tagClass === 1 ? 0x40 : 0x00;
        if (tagConstructed)
            first |= 0x20;
        if (tagNumber < 0x1F) {
            first |= tagNumber & 0x1F;
            ha.push(first);
        } else {
            first |= 0x1F;
            ha.push(first);
            var n = tagNumber, ha1 = [];
            do {
                ha1.push(n & 0x7F);
                n = n >>> 7;
            } while (n)
            // reverse order
            for (var j = ha1.length - 1; j >= 0; --j)
                ha.push(ha1[j] + (j === 0 ? 0x00 : 0x80));
        }
        // Length
        if (tagConstructed && format === 'CER') {
            ha.push(0x80);
        } else {
            var len = content.length;
            if (len > 0x7F) {
                var l2 = len, ha2 = [];
                do {
                    ha2.push(l2 & 0xff);
                    l2 = l2 >>> 8;
                } while (l2);
                ha.push(ha2.length + 0x80); // reverse order
                for (var j = ha2.length - 1; j >= 0; --j)
                    ha.push(ha2[j]);
            } else {
                // simple len
                ha.push(len);
            }
        }
        var header = source.header = new Uint8Array(ha);

        // Result - complete buffer
        var block = new Uint8Array(header.length + content.length);
        block.set(header, 0);
        block.set(content, header.length);
        return block;
    }

    function decodeBER(source, offset) {

        // start pos
        var pos = offset || 0, start = pos;
        var tagNumber, tagClass, tagConstructed,
                content, header, buffer, sub, len;

        if (source.object) {
            // Ready from source
            tagNumber = source.tagNumber;
            tagClass = source.tagClass;
            tagConstructed = source.tagConstructed;
            content = source.content;
            header = source.header;
            buffer = source.object instanceof CryptoOperationData ?
                    new Uint8Array(source.object) : null;
            sub = source.object instanceof Array ? source.object : null;
            len = buffer && buffer.length || null;
        } else {
            // Decode header
            var d = source;

            // Read tag
            var buf = d[pos++];
            tagNumber = buf & 0x1f;
            tagClass = buf >> 6;
            tagConstructed = (buf & 0x20) !== 0;
            if (tagNumber === 0x1f) { // long tag
                tagNumber = 0;
                do {
                    if (tagNumber > 0x1fffffffffff80)
                        throw new DataError('Convertor not supported tag number more then (2^53 - 1) at position ' + offset);
                    buf = d[pos++];
                    tagNumber = (tagNumber << 7) + (buf & 0x7f);
                } while (buf & 0x80);
            }

            // Read len        
            buf = d[pos++];
            len = buf & 0x7f;
            if (len !== buf) {
                if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways
                    throw new DataError('Length over 48 bits not supported at position ' + offset);
                if (len === 0)
                    len = null; // undefined
                else {
                    buf = 0;
                    for (var i = 0; i < len; ++i)
                        buf = (buf << 8) + d[pos++];
                    len = buf;
                }
            }

            start = pos;
            sub = null;

            if (tagConstructed) {
                // must have valid content
                sub = [];
                if (len !== null) {
                    // definite length
                    var end = start + len;
                    while (pos < end) {
                        var s = decodeBER(d, pos);
                        sub.push(s);
                        pos += s.header.length + s.content.length;
                    }
                    if (pos !== end)
                        throw new DataError('Content size is not correct for container starting at offset ' + start);
                } else {
                    // undefined length
                    try {
                        for (; ; ) {
                            var s = decodeBER(d, pos);
                            pos += s.header.length + s.content.length;
                            if (s.tagClass === 0x00 && s.tagNumber === 0x00)
                                break;
                            sub.push(s);
                        }
                        len = pos - start;
                    } catch (e) {
                        throw new DataError('Exception ' + e + ' while decoding undefined length content at offset ' + start);
                    }
                }
            }

            // Header and content
            header = new Uint8Array(d.buffer, offset, start - offset);
            content = new Uint8Array(d.buffer, start, len);
            buffer = content;
        }

        // Constructed types - check for string concationation
        if (sub !== null && tagClass === 0) {
            var k;
            switch (tagNumber) {
                case 0x03: // BIT_STRING
                    k = 1; // ingnore unused bit for bit string
                case 0x04: // OCTET_STRING
                case 0x0C: // UTF8String
                case 0x12: // NumericString
                case 0x13: // PrintableString
                case 0x14: // TeletexString
                case 0x15: // VideotexString
                case 0x16: // IA5String
                case 0x19: // GraphicString
                case 0x1A: // VisibleString
                case 0x1B: // GeneralString
                case 0x1C: // UniversalString
                case 0x1E: // BMPString
                    k = k || 0;
                    // Concatination
                    if (sub.length === 0)
                        throw new DataError('No constructed encoding content of string type at offset ' + start);
                    len = k;
                    for (var i = 0, n = sub.length; i < n; i++) {
                        var s = sub[i];
                        if (s.tagClass !== tagClass || s.tagNumber !== tagNumber || s.tagConstructed)
                            throw new DataError('Invalid constructed encoding of string type at offset ' + start);
                        len += s.content.length - k;
                    }
                    buffer = new Uint8Array(len);
                    for (var i = 0, n = sub.length, j = k; i < n; i++) {
                        var s = sub[i];
                        if (k > 0)
                            buffer.set(s.content.subarray(1), j);
                        else
                            buffer.set(s.content, j);
                        j += s.content.length - k;
                    }
                    tagConstructed = false; // follow not required
                    sub = null;
                    break;
            }
        }
        // Primitive types
        var object = '';
        if (sub === null) {
            if (len === null)
                throw new DataError('Invalid tag with undefined length at offset ' + start);

            if (tagClass === 0) {
                switch (tagNumber) {
                    case 0x01: // BOOLEAN
                        object = buffer[0] !== 0;
                        break;
                    case 0x02: // INTEGER
                    case 0x0a: // ENUMIRATED
                        if (len > 6) {
                            object = Int16.encode(buffer);
                        } else {
                            var v = buffer[0];
                            if (buffer[0] > 0x7f)
                                v = v - 256;
                            for (var i = 1; i < len; i++)
                                v = v * 256 + buffer[i];
                            object = v;
                        }
                        break;
                    case 0x03: // BIT_STRING
                        if (len > 5) { // Content buffer
                            object = new Uint8Array(buffer.subarray(1)).buffer;
                        } else { // Max bit mask only for 32 bit
                            var unusedBit = buffer[0],
                                    skip = unusedBit, s = [];
                            for (var i = len - 1; i >= 1; --i) {
                                var b = buffer[i];
                                for (var j = skip; j < 8; ++j)
                                    s.push((b >> j) & 1 ? '1' : '0');
                                skip = 0;
                            }
                            object = s.reverse().join('');
                        }
                        break;
                    case 0x04: // OCTET_STRING
                        object = new Uint8Array(buffer).buffer;
                        break;
                        //  case 0x05: // NULL
                    case 0x06: // OBJECT_IDENTIFIER
                        var s = '',
                                n = 0,
                                bits = 0;
                        for (var i = 0; i < len; ++i) {
                            var v = buffer[i];
                            n = (n << 7) + (v & 0x7F);
                            bits += 7;
                            if (!(v & 0x80)) { // finished
                                if (s === '') {
                                    var m = n < 80 ? n < 40 ? 0 : 1 : 2;
                                    s = m + "." + (n - m * 40);
                                } else
                                    s += "." + n.toString();
                                n = 0;
                                bits = 0;
                            }
                        }
                        if (bits > 0)
                            throw new DataError('Incompleted OID at offset ' + start);
                        object = s;
                        break;
                        //case 0x07: // ObjectDescriptor
                        //case 0x08: // EXTERNAL
                        //case 0x09: // REAL
                        //case 0x0A: // ENUMERATED
                        //case 0x0B: // EMBEDDED_PDV
                    case 0x10: // SEQUENCE
                    case 0x11: // SET
                        object = [];
                        break;
                    case 0x0C: // UTF8String
                        object = Chars.encode(buffer, 'utf8');
                        break;
                    case 0x12: // NumericString
                    case 0x13: // PrintableString
                    case 0x14: // TeletexString
                    case 0x15: // VideotexString
                    case 0x16: // IA5String
                    case 0x19: // GraphicString
                    case 0x1A: // VisibleString
                    case 0x1B: // GeneralString
                        object = Chars.encode(buffer, 'ascii');
                        break;
                    case 0x1C: // UniversalString
                        object = Chars.encode(buffer, 'utf32');
                        break;
                    case 0x1E: // BMPString
                        object = Chars.encode(buffer, 'utf16');
                        break;
                    case 0x17: // UTCTime
                    case 0x18: // GeneralizedTime
                        var shortYear = tagNumber === 0x17;
                        var s = Chars.encode(buffer, 'ascii'),
                                m = (shortYear ?
                                        /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/ :
                                        /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/).exec(s);
                        if (!m)
                            throw new DataError('Unrecognized time format "' + s + '" at offset ' + start);
                        if (shortYear) {
                            // Where YY is greater than or equal to 50, the year SHALL be interpreted as 19YY; and
                            // Where YY is less than 50, the year SHALL be interpreted as 20YY                                
                            m[1] = +m[1];
                            m[1] += (m[1] < 50) ? 2000 : 1900;
                        }
                        var dt = new Date(m[1], +m[2] - 1, +m[3], +(m[4] || '0'), +(m[5] || '0'), +(m[6] || '0'), +(m[7] || '0')),
                                tz = dt.getTimezoneOffset();
                        if (m[8] || tagNumber === 0x17) {
                            if (m[8].toUpperCase() !== 'Z' && m[9]) {
                                tz = tz + parseInt(m[9]);
                            }
                            dt.setMinutes(dt.getMinutes() - tz);
                        }
                        dt.original = s;
                        object = dt;
                        break;
                }
            } else // OCTET_STRING
                object = new Uint8Array(buffer).buffer;
        } else
            object = sub;

        // result
        return {
            tagConstructed: tagConstructed,
            tagClass: tagClass,
            tagNumber: tagNumber,
            header: header,
            content: content,
            object: object
        };
    }

    return {
        /**
         * BER.decode(object, format) convert javascript object to ASN.1 format CryptoOperationData<br><br>
         * If object has members tagNumber, tagClass and tagConstructed
         * it is clear define encoding rules. Else method use defaul rules:
         * <ul>
         *   <li>Empty string or null - NULL</li>
         *   <li>String starts with '0x' and has 0-9 and a-f characters - INTEGER</li>
         *   <li>String like d.d.d.d (d - set of digits) - OBJECT IDENTIFIER</li>
         *   <li>String with characters 0 and 1 - BIT STRING</li>
         *   <li>Strings 'true' or 'false' - BOOLEAN</li>
         *   <li>String has only 0-9 and a-f characters - OCTET STRING</li>
         *   <li>String has only characters with code 0-255 - PrintableString</li>
         *   <li>Other strings - UTF8String</li>
         *   <li>Number - INTEGER</li>
         *   <li>Date - GeneralizedTime</li>
         *   <li>Boolean - SEQUENCE</li>
         *   <li>CryptoOperationData - OCTET STRING</li>
         * </ul>
         * SEQUENCE or SET arrays recursively encoded for each item.<br>
         * OCTET STRING and BIT STRING can presents as array with one item. 
         * It means encapsulates encoding for child element.<br>
         * 
         * If CONTEXT or APPLICATION classes item presents as array with one 
         * item we use EXPLICIT encoding for element, else IMPLICIT encoding.<br>
         * 
         * @memberOf GostCoding.BER
         * @param {Object} object Object to encoding
         * @param {string} format Encoding rule: 'DER' or 'CER', default 'DER'
         * @param {boolean} onlyContent Encode content only, without header
         * @returns {CryptoOperationData} BER encoded data
         */
        encode: function (object, format, onlyContent) {
            return encodeBER(object, format, onlyContent).buffer;
        },
        /**
         * BER.encode(data) convert ASN.1 format CryptoOperationData data to javascript object<br><br>
         * 
         * Conversion rules to javascript object:
         *  <ul>
         *      <li>BOOLEAN - Boolean object</li>
         *      <li>INTEGER, ENUMIRATED - Integer object if len <= 6 (48 bits) else Int16 encoded string</li>
         *      <li>BIT STRING - Integer object if len <= 5 (w/o unsedBit octet - 32 bits) else String like '10111100' or  Array with one item in case of incapsulates encoding</li>
         *      <li>OCTET STRING - Hex encoded string or Array with one item in case of incapsulates encoding</li>
         *      <li>OBJECT IDENTIFIER - String with object identifier</li>
         *      <li>SEQUENCE, SET - Array of encoded items</li>
         *      <li>UTF8String, NumericString, PrintableString, TeletexString, VideotexString, 
         *          IA5String, GraphicString, VisibleString, GeneralString, UniversalString,
         *          BMPString - encoded String</li>
         *      <li>UTCTime, GeneralizedTime - Date</li>
         *  </ul>
         * @memberOf GostCoding.BER
         * @param {(CryptoOperationData|GostCoding.BER)} data Binary data to decode
         * @returns {Object} Javascript object with result of decoding
         */
        decode: function (data) {
            return decodeBER(data.object ? data : new Uint8Array(buffer(data)), 0);
        }
    }; // </editor-fold>
})();

/**
 * BER, DER, CER conversion
 * @memberOf GostCoding
 * @insnance
 * @type GostCoding.BER
 */
GostCoding.prototype.BER = BER;

/**
 * PEM conversion
 * @class GostCoding.PEM
 */
var PEM = {// <editor-fold defaultstate="collapsed">
    /**
     * PEM.encode(data, name) encode CryptoOperationData to PEM format with name label
     * 
     * @memberOf GostCoding.PEM
     * @param {(Object|CryptoOperationData)} data Java script object or BER-encoded binary data
     * @param {string} name Name of PEM object: 'certificate', 'private key' etc.
     * @returns {string} Encoded object
     */
    encode: function (data, name) {
        return (name ? '-----BEGIN ' + name.toUpperCase() + '-----\r\n' : '') +
                Base64.encode(data instanceof CryptoOperationData ? data : BER.encode(data)) +
                (name ? '\r\n-----END ' + name.toUpperCase() + '-----' : '');
    },
    /**
     * PEM.decode(s, name, deep) decode PEM format s labeled name to CryptoOperationData or javascript object in according to deep parameter
     * 
     * @memberOf GostCoding.PEM
     * @param {string} s PEM encoded string
     * @param {string} name Name of PEM object: 'certificate', 'private key' etc.
     * @param {boolean} deep If true method do BER-decoding, else only BASE64 decoding
     * @param {integer} index Index of decoded value
     * @returns {(Object|CryptoOperationData)} Decoded javascript object if deep=true, else CryptoOperationData for father BER decoding
     */
    decode: function (s, name, deep, index) {
        // Try clear base64
        var re1 = /([A-Za-z0-9\+\/\s\=]+)/g,
                valid = re1.exec(s);
        if (valid[1].length !== s.length)
            valid = false;
        if (!valid && name) {
            // Try with the name
            var re2 = new RegExp(
                    '-----\\s?BEGIN ' + name.toUpperCase() +
                    '-----([A-Za-z0-9\\+\\/\\s\\=]+)-----\\s?END ' +
                    name.toUpperCase() + '-----', 'g');
            valid = re2.exec(s);
        }
        if (!valid) {
            // Try with some name
            var re3 = new RegExp(
                    '-----\\s?BEGIN [A-Z0-9\\s]+' +
                    '-----([A-Za-z0-9\\+\\/\\s\\=]+)-----\\s?END ' +
                    '[A-Z0-9\\s]+-----', 'g');
            valid = re3.exec(s);
        }
        var r = valid && valid[1 + (index || 0)];
        if (!r)
            throw new DataError('Not valid PEM format');
        var out = Base64.decode(r);
        if (deep)
            out = BER.decode(out);
        return out;
    } // </editor-fold>
};

/**
 * PEM conversion
 * @memberOf GostCoding
 * @insnance
 * @type GostCoding.PEM
 */
GostCoding.prototype.PEM = PEM;

if (gostCrypto)
    /**
     * Coding algorithms: Base64, Hex, Int16, Chars, BER and PEM
     * 
     * @memberOf gostCrypto
     * @type GostCoding
     */
    gostCrypto.coding = new GostCoding();

export default GostCoding;