APIs

Show:
"use strict";
/**
 * @module opcua.miscellaneous
 */

var util = require("util");
var EventEmitter = require("events").EventEmitter;
var assert = require("node-opcua-assert");
var _ = require("underscore");


var readMessageHeader = require("./read_message_header").readMessageHeader;
var BinaryStream = require("node-opcua-binary-stream").BinaryStream;

var buffer_utils = require("node-opcua-buffer-utils");
var createFastUninitializedBuffer = buffer_utils.createFastUninitializedBuffer;


var do_debug = false;

function verify_message_chunk(message_chunk) {
    assert(message_chunk);
    assert(message_chunk instanceof Buffer);
    var header = readMessageHeader(new BinaryStream(message_chunk));
    if (message_chunk.length !== header.length) {
        throw new Error(" chunk length = " + message_chunk.length + " message  length " + header.length);
    }
}

exports.verify_message_chunk = verify_message_chunk;

// see https://github.com/substack/_buffer-handbook
//     http://blog.nodejs.org/2012/12/20/streams2/
//     http://codewinds.com/blog/2013-08-20-nodejs-transform-streams.html

//                                  +----------------+----
//                                  | message header | ^
//                                  +----------------+ |<- data to sign
//                                  | security header| |
//                                  +----------------+ | ---
//                                  | Sequence header| |   ^
//                                  +----------------+ |   |<- data to encrypt
//                                  | BODY           | |   |
//                                  +----------------+ |   |
//                                  | padding        | v   |
//                                  +----------------+---  |
//                                  | Signature      |     v
//                                  +----------------+------
//
//  chunkSize = 8192
//  plainBlockSize = 256-11
//  cipherBlockSize = 256
//  headerSize  = messageHeaderSize + securityHeaderSize
//  maxBodySize = plainBlockSize*floor((chunkSize–headerSize–signatureLength-1)/cipherBlockSize)-sequenceHeaderSize;
// length(data to encrypt) = n *

// Rules:
//  - The SequenceHeaderSize is always 8 bytes
//  - The HeaderSize includes the MessageHeader and the SecurityHeader.
//  - The PaddingSize  and Padding  fields are not present if the  MessageChunk  is not encrypted.
//  - The Signature field is not present if the  MessageChunk  is not signed.


function argsIn(obj, properties) {

    var nbUnwanted = 0;
    /* istanbul ignore next */
    if (do_debug) {
        Object.keys(obj).forEach(function (key) {
            if (properties.indexOf(key) < 0) {
                console.log(" ERROR".red, "invalid property :", key);
                nbUnwanted++;
            }
        });
    }
    return nbUnwanted === 0;
}

var ChunkManager_options = [
    "chunkSize", "headerSize", "signatureLength",
    "sequenceHeaderSize", "cipherBlockSize", "plainBlockSize",
    "compute_signature", "encrypt_buffer", "writeSequenceHeaderFunc", "writeHeaderFunc"
];
/**
 * @class ChunkManager
 * @param options {Object}
 * @param options.chunkSize
 * @param options.padding_size
 * @param [options.headerSize = 0 ]
 * @param [options.signatureLength = 0]
 * @param [options.sequenceHeaderSize = 8] size of the sequence header
 * @extends EventEmitter
 * @constructor
 */
function ChunkManager(options) {

    assert(argsIn(options, ChunkManager_options));

    // { chunkSize : 32, headerSize : 10 ,signatureLength: 32 }
    this.chunkSize = options.chunkSize;

    this.headerSize = options.headerSize || 0;
    if (this.headerSize) {
        this.writeHeaderFunc = options.writeHeaderFunc;
        assert(_.isFunction(this.writeHeaderFunc));
    }

    this.sequenceHeaderSize = options.sequenceHeaderSize === undefined ? 8 : options.sequenceHeaderSize;
    if (this.sequenceHeaderSize > 0) {
        this.writeSequenceHeaderFunc = options.writeSequenceHeaderFunc;
        assert(_.isFunction(this.writeSequenceHeaderFunc));
    }

    this.signatureLength   = options.signatureLength || 0;
    this.compute_signature = options.compute_signature;

    this.plainBlockSize    = options.plainBlockSize   || 0; // 256-14;
    this.cipherBlockSize   = options.cipherBlockSize || 0; // 256;

    if (this.cipherBlockSize === 0) {
        assert(this.plainBlockSize === 0);
        // unencrypted block
        this.maxBodySize = (this.chunkSize - this.headerSize - this.signatureLength - this.sequenceHeaderSize);

    } else {
        assert(this.plainBlockSize !== 0);
        // During encryption a block with a size equal to  PlainTextBlockSize  is processed to produce a block
        // with size equal to  CipherTextBlockSize. These values depend on the encryption algorithm and may
        // be the same.

        this.encrypt_buffer = options.encrypt_buffer;
        assert(_.isFunction(this.encrypt_buffer));

        // this is the formula proposed  by OPCUA
        this.maxBodySize = this.plainBlockSize * Math.floor(
                (this.chunkSize - this.headerSize - this.signatureLength - 1 ) / this.cipherBlockSize) - this.sequenceHeaderSize;

        // this is the formula proposed  by ERN
        this.maxBlock = Math.floor(( this.chunkSize - this.headerSize ) / this.cipherBlockSize);
        this.maxBodySize = this.plainBlockSize * this.maxBlock - this.sequenceHeaderSize - this.signatureLength - 1;

        if(this.plainBlockSize>256) {
            this.maxBodySize -=1;
        }
    }
    assert(this.maxBodySize > 0); // no space left to write data

    // where the data starts in the block
    this.dataOffset = this.headerSize + this.sequenceHeaderSize;

    this.chunk = createFastUninitializedBuffer(this.chunkSize);

    this.cursor = 0;
    this.pending_chunk = null;

}
util.inherits(ChunkManager, EventEmitter);


/**
 * compute the signature of the chunk and append it at the end
 * of the data block.
 *
 * @method _write_signature
 * @private
 */
ChunkManager.prototype._write_signature = function (chunk) {

    if (this.compute_signature) {
        assert(_.isFunction(this.compute_signature));
        assert(this.signatureLength !== 0);

        var signature_start = this.dataEnd;
        var section_to_sign = chunk.slice(0, signature_start);


        var signature = this.compute_signature(section_to_sign);
        assert(signature.length === this.signatureLength);

        signature.copy(chunk, signature_start);


    } else {
        assert(this.signatureLength === 0, "expecting NO SIGN");
    }
};



ChunkManager.prototype._encrypt = function (chunk) {

    if (this.plainBlockSize > 0) {

        var startEncryptionPos = this.headerSize;
        var endEncryptionPos = this.dataEnd + this.signatureLength;

        var area_to_encrypt = chunk.slice(startEncryptionPos, endEncryptionPos);

        assert(area_to_encrypt.length % this.plainBlockSize === 0); // padding should have been applied
        var nbBlock = area_to_encrypt.length / this.plainBlockSize;

        var encrypted_buf = this.encrypt_buffer(area_to_encrypt);
        assert(encrypted_buf.length % this.cipherBlockSize === 0);
        assert(encrypted_buf.length === nbBlock * this.cipherBlockSize);

        encrypted_buf.copy(chunk, this.headerSize, 0);

    }
};

ChunkManager.prototype._push_pending_chunk = function (isLast) {

    if (this.pending_chunk) {

        var expected_length = this.pending_chunk.length;

        if (this.headerSize > 0) {
            // Release 1.02  39  OPC Unified Architecture, Part 6:
            // The sequence header ensures that the first  encrypted block of every  Message  sent over
            // a channel will start with different data.
            this.writeHeaderFunc(this.pending_chunk.slice(0, this.headerSize), isLast, expected_length);
        }
        if (this.sequenceHeaderSize > 0) {
            this.writeSequenceHeaderFunc(this.pending_chunk.slice(this.headerSize, this.headerSize + this.sequenceHeaderSize));
        }

        this._write_signature(this.pending_chunk);

        this._encrypt(this.pending_chunk);

        /**
         * @event chunk
         * @param chunk {Buffer}
         * @param isLast {Boolean} , true if final chunk
         */
        this.emit("chunk", this.pending_chunk, isLast);
        this.pending_chunk = null;
    }
};


/**
 * @method write
 * @param buffer {Buffer}
 * @param length {Number}
 */
ChunkManager.prototype.write = function (buffer, length) {

    length = length || buffer.length;
    assert(buffer instanceof Buffer || (buffer === null));
    assert(length > 0);

    var l = length;
    var input_cursor = 0;

    while (l > 0) {
        assert(length - input_cursor !== 0);

        if (this.cursor === 0) {
            this._push_pending_chunk(false);
        }

        // space left in current chunk
        var space_left = this.maxBodySize - this.cursor;

        var nb_to_write = Math.min(length - input_cursor, space_left);

        if (buffer) {
            buffer.copy(this.chunk, this.cursor + this.dataOffset, input_cursor, input_cursor + nb_to_write);
        }

        input_cursor += nb_to_write;
        this.cursor += nb_to_write;

        if (this.cursor >= this.maxBodySize) {
            this._postprocess_current_chunk();
        }
        l -= nb_to_write;
    }
};



ChunkManager.prototype._write_padding_bytes = function(nbPaddingByteTotal) {

    var nbPaddingByte = nbPaddingByteTotal % 256;
    var extraNbPaddingByte = Math.floor( nbPaddingByteTotal / 256);

    assert(extraNbPaddingByte === 0 || this.plainBlockSize>256 ,"extraNbPaddingByte only requested when key size > 2048" );

    // write the padding byte
    this.chunk.writeUInt8(nbPaddingByte, this.cursor + this.dataOffset);
    this.cursor += 1;

    for (var i = 0; i < nbPaddingByteTotal; i++) {
        this.chunk.writeUInt8(nbPaddingByte, this.cursor + this.dataOffset + i);
    }
    this.cursor += nbPaddingByteTotal;

    if (this.plainBlockSize>256) {
        this.chunk.writeUInt8(extraNbPaddingByte, this.cursor + this.dataOffset);
        this.cursor += 1;
    }

};


ChunkManager.prototype._postprocess_current_chunk = function () {

    var extra_encryption_bytes = 0;
    // add padding bytes if needed
    if (this.plainBlockSize > 0) {
        // write padding ( if encryption )

        // let's calculatee curLength = the length of the block to encrypt without padding yet
        // +---------------+---------------+-------------+---------+--------------+------------+
        // |SequenceHeader | data          | paddingByte | padding | extraPadding | signature  |
        // +---------------+---------------+-------------+---------+--------------+------------+
        var curLength = this.sequenceHeaderSize + this.cursor + this.signatureLength;
        if (this.plainBlockSize>256) {
            curLength +=2; // account for extraPadding Byte Number;
        } else {
            curLength +=1;
        }
        // let's calculate the required number of padding bytes
        var n = (curLength % this.plainBlockSize);
        var nbPaddingByteTotal = (this.plainBlockSize - n) % this.plainBlockSize;

        this._write_padding_bytes(nbPaddingByteTotal);
        var adjustedLength = this.sequenceHeaderSize + this.cursor + this.signatureLength;

        assert(adjustedLength % this.plainBlockSize === 0);
        var nbBlock = adjustedLength / this.plainBlockSize;
        extra_encryption_bytes =  nbBlock * (this.cipherBlockSize - this.plainBlockSize );
    }

    this.dataEnd = this.dataOffset + this.cursor;

    // calculate the expected length of the chunk, once encrypted if encryption apply
    var expected_length = this.dataEnd + this.signatureLength + extra_encryption_bytes;

    this.pending_chunk = this.chunk.slice(0, expected_length);
    // note :
    //  - this.pending_chunk has the correct size but is not signed nor encrypted yet
    //    as we don't know what to write in the header yet
    //  - as a result,
    this.cursor = 0;
};

/**
 * @method end
 */
ChunkManager.prototype.end = function () {

    if (this.cursor > 0) {
        this._postprocess_current_chunk();
    }

    this._push_pending_chunk(true);

};

exports.ChunkManager = ChunkManager;