"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NoiseSocket = void 0;
const assert_1 = __importDefault(require("assert"));
const net_1 = require("net");
const stream_1 = require("stream");
const handshake_state_1 = require("./handshake-state");
const noise_error_1 = require("./noise-error");
const read_state_1 = require("./read-state");
class NoiseSocket extends stream_1.Duplex {
    /**
     * NoiseSocket is a Duplex Stream that wraps a standard TCP Socket
     * and layers in the BOLT #8 implementation of the Noise Protocol
     * Framework.
     *
     * This socket can be used for any communication that wants to
     * benefit from the security and privacy enhancing used by the
     * Noise Protocol Framework.
     */
    constructor({ socket, noiseState, rpk, logger, highWaterMark = 2048, }) {
        super({ objectMode: true, highWaterMark });
        // perform type assertions
        assert_1.default.ok(socket instanceof net_1.Socket, new noise_error_1.NoiseError("socket argument must be an instance of Socket")); // prettier-ignore
        this.rpk = rpk;
        // if remote is provided, then we are the initiator
        this.initiator = rpk !== undefined;
        this._handshakeState = this.initiator
            ? handshake_state_1.HANDSHAKE_STATE.INITIATOR_INITIATING
            : handshake_state_1.HANDSHAKE_STATE.AWAITING_INITIATOR;
        this._readState = read_state_1.READ_STATE.READY_FOR_LEN;
        this._noiseState = noiseState;
        this.messagesReceived = 0;
        this._socket = socket;
        this._socket.on('close', (hadError) => this.emit('close', hadError));
        this._socket.on('connect', this._onConnected.bind(this));
        this._socket.on('drain', () => this.emit('drain'));
        this._socket.on('end', () => this.emit('end'));
        this._socket.on('error', (err) => this.emit('error', err));
        this._socket.on('lookup', (e, a, f, h) => this.emit('lookup', e, a, f, h));
        this._socket.on('readable', this._onData.bind(this));
        this._socket.on('timeout', () => this.emit('timeout'));
        this.logger = logger;
    }
    /**
     * Half-closes the socket. It is still possible that the opposite
     * side is still sending data.
     */
    end() {
        this._socket.end();
        return this;
    }
    /**
     * Destroys the socket and ensures that no more I/O activity happens
     * on the socket. When an `err` is included, an 'error' event will
     * be emitted and all listeners will receive the error as an
     * argument.
     * @param err optional error to send
     */
    destroy(err) {
        this._socket.destroy(err);
        return this;
    }
    /**
     * Fires when the socket has connected. This method initiates the
     * handshake and if there is a failure, terminates the connection.
     */
    _onConnected() {
        try {
            if (this.initiator) {
                this._initiateHandshake();
            }
        }
        catch (err) {
            this.destroy(err);
        }
    }
    /**
     * _onData is triggered by the "readable" event on the
     * underlying TCP socket. It is called each time there is new data
     * received. It is responsible for reading data from the socket and
     * performing the appropriate action given the current read state.
     */
    _onData() {
        try {
            // Loop while there was still data to process on the socket's
            // buffer. This will stop when we don't have enough data or
            // we encounter a back pressure issue;
            let readMore = true;
            do {
                if (this._handshakeState !== handshake_state_1.HANDSHAKE_STATE.READY) {
                    switch (this._handshakeState) {
                        // Initiator received data before initialized
                        case handshake_state_1.HANDSHAKE_STATE.INITIATOR_INITIATING:
                            throw new noise_error_1.NoiseError('Pending state is invalid');
                        // Responder Act1
                        case handshake_state_1.HANDSHAKE_STATE.AWAITING_INITIATOR:
                            readMore = this._processInitiator();
                            break;
                        // Responder Act3
                        case handshake_state_1.HANDSHAKE_STATE.AWAITING_INITIATOR_REPLY:
                            readMore = this._processInitiatorReply();
                            break;
                        // Initiator Act2
                        case handshake_state_1.HANDSHAKE_STATE.AWAITING_RESPONDER_REPLY:
                            readMore = this._processResponderReply();
                            break;
                    }
                }
                else {
                    switch (this._readState) {
                        case read_state_1.READ_STATE.READY_FOR_LEN:
                            readMore = this._processPacketLength();
                            break;
                        case read_state_1.READ_STATE.READY_FOR_BODY:
                            readMore = this._processPacketBody();
                            break;
                        case read_state_1.READ_STATE.BLOCKED:
                            readMore = false;
                            break;
                        default:
                            throw new noise_error_1.NoiseError('Unknown read state');
                    }
                }
            } while (readMore);
        }
        catch (err) {
            // Terminate on failures as we won't be able to recovery
            // since the noise state has rotated nonce and we won't
            // be able to any more data without additional errors.
            this.destroy(err);
        }
    }
    _initiateHandshake() {
        // create Initiator Act 1 message
        const m = this._noiseState.initiatorAct1(this.rpk);
        // send message to Responder
        this._socket.write(m);
        // transition state
        this._handshakeState = handshake_state_1.HANDSHAKE_STATE.AWAITING_RESPONDER_REPLY;
    }
    _processInitiator() {
        // must read 50 bytes
        let m = this._socket.read(50);
        if (!m)
            return false;
        // validate initiator act1 message
        this._noiseState.receiveAct1(m);
        // create reply message
        m = this._noiseState.recieveAct2();
        // send the reply
        this._socket.write(m);
        // transition
        this._handshakeState = handshake_state_1.HANDSHAKE_STATE.AWAITING_INITIATOR_REPLY;
        // indicate processing was successful
        return true;
    }
    _processInitiatorReply() {
        // must read 66 bytes
        const m = this._socket.read(66);
        if (!m)
            return false;
        // validate initiator act3 message
        this._noiseState.receiveAct3(m);
        // transition
        this._handshakeState = handshake_state_1.HANDSHAKE_STATE.READY;
        // capture the rpk since it is now available
        this.rpk = this._noiseState.rpk;
        // emit that we're ready!
        this.emit('connect');
        this.emit('ready');
        // return true to continue processing
        return true;
    }
    _processResponderReply() {
        // must read 50 bytes
        let m = this._socket.read(50);
        if (!m)
            return;
        // process reply
        this._noiseState.initiatorAct2(m);
        // create final act of the handshake
        m = this._noiseState.initiatorAct3();
        // send final handshake
        this._socket.write(m);
        // transition
        this._handshakeState = handshake_state_1.HANDSHAKE_STATE.READY;
        // emit that we're ready!
        this.emit('connect');
        this.emit('ready');
        // return true to continue processing
        return true;
    }
    _processPacketLength() {
        const LEN_CIPHER_BYTES = 2;
        const LEN_MAC_BYTES = 16;
        // Try to read the length cipher bytes and the length MAC bytes
        // If we cannot read the 18 bytes, the attempt to process the
        // message will abort.
        const lc = this._socket.read(LEN_CIPHER_BYTES + LEN_MAC_BYTES);
        if (!lc)
            return;
        // Decrypt the length including the MAC
        const l = this._noiseState.decryptLength(lc);
        // We need to store the value in a local variable in case
        // we are unable to read the message body in its entirety.
        // This allows us to skip the length read and prevents
        // nonce issues since we've already decrypted the length.
        this._l = l;
        // Transition state
        this._readState = read_state_1.READ_STATE.READY_FOR_BODY;
        // return true to continue reading
        return true;
    }
    _processPacketBody() {
        const MESSAGE_MAC_BYTES = 16;
        // With the length, we can attempt to read the message plus
        // the MAC for the message. If we are unable to read because
        // there is not enough data in the read buffer, we need to
        // store l. We are not able to simply unshift it becuase we
        // have already rotated the keys.
        const c = this._socket.read(this._l + MESSAGE_MAC_BYTES);
        if (!c)
            return;
        // Decrypt the full message cipher + MAC
        const m = this._noiseState.decryptMessage(c);
        // Now that we've read the message, we can remove the
        // cached length before we transition states
        this._l = null;
        // Increment the number of messages received
        this.messagesReceived++;
        // Push the message onto the read buffer for the consumer to
        // read. We are mindful of slow reads by the consumer and
        // will respect backpressure signals.
        const pushOk = this.push(m);
        if (pushOk) {
            this._readState = read_state_1.READ_STATE.READY_FOR_LEN;
            return true;
        }
        else {
            if (this.logger)
                this.logger.debug('socket read is blocked');
            this._readState = read_state_1.READ_STATE.BLOCKED;
            return false;
        }
    }
    // tslint:disable-next-line: member-ordering
    _read() {
        if (this._handshakeState !== handshake_state_1.HANDSHAKE_STATE.READY) {
            return;
        }
        if (this._readState === read_state_1.READ_STATE.BLOCKED) {
            if (this.logger)
                this.logger.debug('socket read is unblocked');
            this._readState = read_state_1.READ_STATE.READY_FOR_LEN;
        }
        // Trigger a read but wait until the end of the event loop.
        // This is necessary when reading in paused mode where
        // _read was triggered by stream.read() originating inside
        // a "readable" event handler. Attempting to push more data
        // synchronously will not trigger another "readable" event.
        setImmediate(() => this._onData());
    }
    // tslint:disable-next-line: member-ordering
    _write(data, encoding, cb) {
        const c = this._noiseState.encryptMessage(data);
        this._socket.write(c, cb);
    }
    _final() {
        //
    }
}
exports.NoiseSocket = NoiseSocket;
NoiseSocket.READ_STATE = read_state_1.READ_STATE;
NoiseSocket.HANDSHAKE_STATE = handshake_state_1.HANDSHAKE_STATE;
//# sourceMappingURL=noise-socket.js.map