"use strict"

Object.defineProperty(exports, "__esModule", { value: true })

const boom_1 = require("@hapi/boom")
const WAProto_1 = require("../../WAProto")
const Defaults_1 = require("../Defaults")
const WABinary_1 = require("../WABinary")
const crypto_1 = require("./crypto")

const generateIV = (counter) => {
    const iv = new ArrayBuffer(12)
    new DataView(iv).setUint32(8, counter)
    return new Uint8Array(iv)
}

const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publicKey }, NOISE_HEADER, logger, routingInfo }) => {
    logger = logger.child({ class: 'ns' })
    const authenticate = (data) => {
        if (!isFinished) {
            hash = crypto_1.sha256(Buffer.concat([hash, data]))
        }
    }
    const encrypt = (plaintext) => {
        const result = crypto_1.aesEncryptGCM(plaintext, encKey, generateIV(writeCounter), hash)
        writeCounter += 1
        authenticate(result)
        return result
    }
    const decrypt = (ciphertext) => {
        // before the handshake is finished, we use the same counter
        // after handshake, the counters are different
        const iv = generateIV(isFinished ? readCounter : writeCounter)
        const result = crypto_1.aesDecryptGCM(ciphertext, decKey, iv, hash)
        if (isFinished) {
            readCounter += 1
        }
        else {
            writeCounter += 1
        }
        authenticate(ciphertext)
        return result
    }
    const localHKDF = async (data) => {
        const key = await crypto_1.hkdf(Buffer.from(data), 64, { salt, info: '' })
        return [key.slice(0, 32), key.slice(32)]
    }
    const mixIntoKey = async (data) => {
        const [write, read] = await localHKDF(data)
        salt = write
        encKey = read
        decKey = read
        readCounter = 0
        writeCounter = 0
    }
    const finishInit = async () => {
        const [write, read] = await localHKDF(new Uint8Array(0))
        encKey = write
        decKey = read
        hash = Buffer.from([])
        readCounter = 0
        writeCounter = 0
        isFinished = true
    }
    const data = Buffer.from(Defaults_1.NOISE_MODE)
    let hash = data.byteLength === 32 ? data : crypto_1.sha256(data)
    let salt = hash
    let encKey = hash
    let decKey = hash
    let readCounter = 0
    let writeCounter = 0
    let isFinished = false
    let sentIntro = false
    let inBytes = Buffer.alloc(0)
    authenticate(NOISE_HEADER)
    authenticate(publicKey)
    return {
        encrypt,
        decrypt,
        authenticate,
        mixIntoKey,
        finishInit,
        processHandshake: async ({ serverHello }, noiseKey) => {
            authenticate(serverHello.ephemeral)
            await mixIntoKey(crypto_1.Curve.sharedKey(privateKey, serverHello.ephemeral))
            const decStaticContent = decrypt(serverHello.static)
            await mixIntoKey(crypto_1.Curve.sharedKey(privateKey, decStaticContent))
            const certDecoded = decrypt(serverHello.payload)
            const { intermediate: certIntermediate } = WAProto_1.proto.CertChain.decode(certDecoded)
            const { issuerSerial } = WAProto_1.proto.CertChain.NoiseCertificate.Details.decode(certIntermediate.details)
            if (issuerSerial !== Defaults_1.WA_CERT_DETAILS.SERIAL) {
                throw new boom_1.Boom('certification match failed', { statusCode: 400 })
            }
            const keyEnc = encrypt(noiseKey.public)
            await mixIntoKey(crypto_1.Curve.sharedKey(noiseKey.private, serverHello.ephemeral))
            return keyEnc
        },
        encodeFrame: (data) => {
            if (isFinished) {
                data = encrypt(data)
            }
            let header
            if (routingInfo) {
                header = Buffer.alloc(7)
                header.write('ED', 0, 'utf8')
                header.writeUint8(0, 2)
                header.writeUint8(1, 3)
                header.writeUint8(routingInfo.byteLength >> 16, 4)
                header.writeUint16BE(routingInfo.byteLength & 65535, 5)
                header = Buffer.concat([header, routingInfo, NOISE_HEADER])
            }
            else {
                header = Buffer.from(NOISE_HEADER)
            }
            const introSize = sentIntro ? 0 : header.length
            const frame = Buffer.alloc(introSize + 3 + data.byteLength)
            if (!sentIntro) {
                frame.set(header)
                sentIntro = true
            }
            frame.writeUInt8(data.byteLength >> 16, introSize)
            frame.writeUInt16BE(65535 & data.byteLength, introSize + 1)
            frame.set(data, introSize + 3)
            return frame
        },
        decodeFrame: async (newData, onFrame) => {
            // the binary protocol uses its own framing mechanism
            // on top of the WS frames
            // so we get this data and separate out the frames
            const getBytesSize = () => {
                if (inBytes.length >= 3) {
                    return (inBytes.readUInt8() << 16) | inBytes.readUInt16BE(1)
                }
            }
            inBytes = Buffer.concat([inBytes, newData])
            logger.trace(`recv ${newData.length} bytes, total recv ${inBytes.length} bytes`)
            let size = getBytesSize()
            while (size && inBytes.length >= size + 3) {
                let frame = inBytes.slice(3, size + 3)
                inBytes = inBytes.slice(size + 3)
                if (isFinished) {
                    const result = decrypt(frame)
                    frame = await WABinary_1.decodeBinaryNode(result)
                }
                logger.trace({ msg: frame?.attrs?.id }, 'recv frame')
                onFrame(frame)
                size = getBytesSize()
            }
        }
    }
}

module.exports = {
  makeNoiseHandler
}