Sleds/K2Proxy/k2.js

474 lines
17 KiB
JavaScript

/*
Copyright (c) 2014 IOnU Security Inc. All rights reserved
Created April 2014 by Kendrick Webster
K2Proxy/k2.js - interface to K2Client and K2Daemon,
implementation of proxy functionality
*/
"use strict";
var sprintf = require("sprintf-js").sprintf,
hash = require("./hash"),
k2_map = require("./k2_map"),
inum_map = require("./inum_map"),
k2_udp = require("./k2_udp"),
router = require("./router");
function trace(m) { // comment or un-comment as needed for debugging:
// console.log(m);
}
function trace_info(m) { // comment or un-comment as needed for debugging:
console.log(m);
}
/*
Poll messages (GET /recv/<message>, base64) consist of the following fields:
octets description
--------------------------------------------------------------------------
9 nonce (random initialization vector)
4 sequence number (see init message)
n client URN (string, no terminator, implicit length)
9 message integrity code (MIC)
All fields after the nonce are encrypted using an auto-hashing stream cipher
with a hard-coded key. The decrypted MIC will match only for properly encrypted
packets. Spurious packets fail the MIC check. This is used to increase the
difficulty of DOS attacks. The sequence number is included to prevent replay
DOS attacks.
The response to a valid poll message is a text document containing the next
received packet for the client URN, base64 encoded, followed by a period '.'
alone on a line. In case no packet arrives within the poll timeout interval,
the response is "timeout".
Invalid messages result in a 404 response (delayed to minimize DOS impact).
*/
/*
Send messages (POST to /send) consist of the following fields:
octets description
--------------------------------------------------------------------------
9 nonce (random initialization vector)
4 sequence number (see init message)
n K2 UDP packet (containing client URN)
9 message integrity code (MIC)
Encryption is done in the same manner (and for the same reasons) as for poll
messages. The sequence number in send messages is independent from the
sequence number in poll messages.
The K2 UDP packet must be of a type that contains a client URN. This is the
case for all K2 UDP packet types that are sent from a client to the daemon.
The response to a valid send message is a text document containing a single
line with the text "OK".
Invalid messages result in a 404 response (delayed to minimize DOS impact).
*/
/*
Init messages (GET /init/<message>, base64) consist of the following fields:
octets description
--------------------------------------------------------------------------
9 nonce (random initialization vector)
n client URN (string, no terminator, implicit length)
9 message integrity code (MIC)
The response to an init message is a sequence number message:
octets description
--------------------------------------------------------------------------
9 nonce (random initialization vector)
4 poll sequence number (randomly initialized)
4 send sequence number (randomly initialized)
9 message integrity code (MIC)
The sequence numbers are randomly-generated and stored as part of a session's
state (associated with the client URN) when a session is initially established.
Upon receiving each poll or send message, the corresponding sequence number is
checked, and, if it matches, incremented modulo 2^32.
A 'session' remains active as long as there is traffic (i.e. K2IPC hearbeat
messages). An absence of traffic causes a session to timeout.
Repeated init messages return the current sequence numbers associated with the
session. The reply (to all init messages) is delayed a small amount to limit
the impact of a replay attack. Although the delay could be selectively applied
only for existing sessions, doing so would leak information about who is
currently logged in. The delay is thus chosen to be small enough to have a
minimal impact on clients while still being large enough to limit traffic and
server load in a replay DOS attack.
*/
var K2PROXY_KEY = 'ICIRY4arfFbqGz9YGZaFRvv6+RNnXrsy';
var K2PROXY_MIC = 'KyYh/7xgwUsU';
var K2PROXY_NONCE_SIZE = 9;
var K2PROXY_MIC_SIZE = 9;
var K2PROXY_SEQUENCE_NUMBER_SIZE = 4;
var K2PROXY_OVERHEAD = K2PROXY_NONCE_SIZE + K2PROXY_MIC_SIZE;
var K2PROXY_INIT_RESPONSE_SIZE = 2 * K2PROXY_SEQUENCE_NUMBER_SIZE + K2PROXY_OVERHEAD;
var MIN_BASE64_MESSAGE_LEN = (K2PROXY_OVERHEAD * 8) / 6;
var K2PROXY_KEY_MIX_CYCLES = 32;
var POLL_TIMEOUT_RESPONSE = "timeout";
var SEND_MESSAGE_RESPONSE = "OK\r\n.\r\n";
var INIT_RESPONSE_DELAY_MS = 330; // see comments for "Repeated init messages"
/*
K2 UDP packets consist of the following fields:
octets description
--------------------------------------------------------------------------
7 nonce (random initialization vector)
2 protocol version
3 client instance (for multiple clients sharing a UDP port)
1 sequence number (echoed in ACK)
1 opcode
<n> body, per opcode, NUL-terminated string
6 message integrity code (MIC)
All fields after the nonce are encrypted using an auto-hashing stream
cipher with a hard-coded key. The decrypted MIC will match only for
properly encrypted packets. Spurious packets fail the MIC check. This
'encryption' is used only for spurious packet rejection (not security).
Multi-byte fields are in network byte order (big endian).
*/
/* packet encryption/validation key and MIC */
var K2IPC_KEY = new Buffer([
169, 208, 1, 214, 130, 153, 218, 176, 187, 115, 186, 84, 117, 25, 162, 197,
173, 73, 161, 180, 68, 224, 62, 241, 192, 96, 152, 79, 148, 239, 20, 132]);
var K2IPC_KEY_MIX_CYCLES = 32;
var K2IPC_MIC = new Buffer([17, 44, 195, 238, 248, 76]);
/* packet opcodes */
var OPCODES = {
CONNECT : {value: 0, name: "CONNECT"},
ACK_CONNECT : {value: 1, name: "ACK_CONNECT"},
NAK_CONNECT : {value: 2, name: "NAK_CONNECT"},
MESSAGE : {value: 3, name: "MESSAGE"},
ACK_MESSAGE : {value: 4, name: "ACK_MESSAGE"},
LOGOUT : {value: 5, name: "LOGOUT"},
ACK_LOGOUT : {value: 6, name: "ACK_LOGOUT"},
POLL_DB : {value: 7, name: "POLL_DB"},
ACK_POLL_DB : {value: 8, name: "ACK_POLL_DB"},
HEARTBEAT : {value: 9, name: "HEARTBEAT"},
ACK_HEARTBEAT : {value: 10, name: "ACK_HEARTBEAT"},
SUBSCRIBE_OFFICE : {value: 11, name: "SUBSCRIBE_OFFICE"},
ACK_SUBSCRIBE_OFFICE : {value: 12, name: "ACK_SUBSCRIBE_OFFICE"},
DEVICE_STATUS : {value: 13, name: "DEVICE_STATUS"},
ACK_DEVICE_STATUS : {value: 14, name: "ACK_DEVICE_STATUS"},
QUERY_STATS : {value: 15, name: "QUERY_STATS"},
ACK_QUERY_STATS : {value: 16, name: "ACK_QUERY_STATS"},
PING : {value: 17, name: "PING"},
ACK_PING : {value: 18, name: "ACK_PING"}
};
var opcode_lut = [];
for (var opcode in OPCODES) {
if (OPCODES.hasOwnProperty(opcode)) {
opcode_lut[OPCODES[opcode].value] = OPCODES[opcode];
}
}
function showK2Packet(direction, length, meta, sequence) {
trace_info(sprintf('K2 %s %-24s %-20s inum:%9u clinum:%9u len:%5u sequence:%11u',
direction, meta.urn, meta.opcode.name, meta.inum, meta.clinum, length, sequence));
}
function readInt24BE(buf, offset) {
var value = buf[offset];
value <<= 8;
value += buf[offset + 1];
value <<= 8;
value += buf[offset + 2];
return value;
}
function writeInt24BE(buf, value, offset) {
buf[offset] = (value >>> 16) & 0xFF;
buf[offset + 1] = (value >>> 8) & 0xFF;
buf[offset + 2] = value & 0xFF;
}
/*
Re-encode and encrypt the cleartext K2 UDP packet in <buf> with <client_instance>
replacing the previously encoded value.
*/
function encodePacket(buf, client_instance) {
var h;
hash.getbuf(hash.rng_hash, buf, 7); // IV nonce
writeInt24BE(buf, client_instance, 9);
h = new hash.Hash();
hash.hashbuf(h, buf.slice(0, 7));
hash.hashbuf(h, K2IPC_KEY);
hash.mix(h, K2IPC_KEY_MIX_CYCLES);
hash.encrypt(h, buf.slice(7));
}
/*
decrypts K2 UDP packet in <buf>, returns <true> if packet is good (MIC validates
and protocol version is supported), sets properties of <out>:
out.urn = client URN (if contained in packet)
out.inum = client instance number used by proxy for daemon-bound packets
out.clinum = client instance number used by client
out.opcode = packet opcode (member of var OPCODES)
*/
function decodePacket(buf, out) {
var instance, opcode, i, h;
if (buf.length < 20) {
return false;
}
h = new hash.Hash();
hash.hashbuf(h, buf.slice(0, 7)); // IV nonce
hash.hashbuf(h, K2IPC_KEY);
hash.mix(h, K2IPC_KEY_MIX_CYCLES);
hash.decrypt(h, buf.slice(7));
if ((buf.slice(-6).toString('base64') === K2IPC_MIC.toString('base64'))
&& (buf.readInt16BE(7) === 1)) { /* protocol version 1 */
instance = readInt24BE(buf, 9);
opcode = buf[13];
out.opcode = opcode_lut[opcode];
switch (opcode) {
/*
Packets from the client to the daemon, ...
(*) The client_instance fields in the packets are set by the clients.
(*) All of these packet types contain an URN.
*/
case OPCODES.CONNECT.value:
case OPCODES.ACK_MESSAGE.value:
case OPCODES.LOGOUT.value:
case OPCODES.POLL_DB.value:
case OPCODES.HEARTBEAT.value:
case OPCODES.ACK_DEVICE_STATUS.value:
case OPCODES.QUERY_STATS.value:
case OPCODES.ACK_PING.value:
out.urn = buf.slice(14, -7).toString();
out.clinum = instance;
out.inum = inum_map.get_inum(out.urn);
if (OPCODES.CONNECT.value === opcode) {
k2_map.set_clinum(out.urn, instance);
trace('urn(' + out.urn + ') --> clinum(' + instance + ')');
}
break;
case OPCODES.SUBSCRIBE_OFFICE.value:
for (i = 14; i < buf.length; i++) {
if (44 === buf[i]) { // 44 is char code for ','
out.urn = buf.slice(14, i).toString();
break;
}
}
out.clinum = instance;
out.inum = inum_map.get_inum(out.urn);
break;
/*
Packets from the daemon to the client, ...
(*) The client_instance fields in the packets are set by the proxy.
(*) Some of these packet types do not contain an URN.
*/
case OPCODES.ACK_CONNECT.value:
case OPCODES.NAK_CONNECT.value:
case OPCODES.MESSAGE.value: // lacks URN
case OPCODES.ACK_LOGOUT.value:
case OPCODES.ACK_POLL_DB.value:
case OPCODES.ACK_HEARTBEAT.value:
case OPCODES.ACK_SUBSCRIBE_OFFICE.value:
case OPCODES.DEVICE_STATUS.value: // lacks URN
case OPCODES.ACK_QUERY_STATS.value: // lacks URN
case OPCODES.PING.value:
out.urn = inum_map.get_urn(instance); // fetches URN *AND* refreshes mapping
out.inum = instance;
out.clinum = k2_map.get_clinum(out.urn);
break;
}
return true;
}
return false;
}
/*
Encrypts a proxy message
Fills in the <nonce> and <MIC> fields at the start and end of the message
*/
function encryptMessage(buf) {
var h, mic;
hash.getbuf(hash.rng_hash, buf, K2PROXY_NONCE_SIZE);
mic = new Buffer(K2PROXY_MIC, 'base64');
mic.copy(buf, buf.length - mic.length);
h = new hash.Hash();
hash.hashbuf(h, buf, K2PROXY_NONCE_SIZE);
hash.hashstr(h, K2PROXY_KEY);
hash.mix(h, K2PROXY_KEY_MIX_CYCLES);
hash.encrypt(h, buf.slice(K2PROXY_NONCE_SIZE));
}
/*
Decrypts a proxy message, returns <true> if MIC is valid
*/
function decryptMessage(buf) {
var h;
if (buf.length >= K2PROXY_OVERHEAD) {
h = new hash.Hash();
hash.hashbuf(h, buf, K2PROXY_NONCE_SIZE);
hash.hashstr(h, K2PROXY_KEY);
hash.mix(h, K2PROXY_KEY_MIX_CYCLES);
hash.decrypt(h, buf.slice(K2PROXY_NONCE_SIZE));
return (buf.slice(-K2PROXY_MIC_SIZE).toString('base64') === K2PROXY_MIC);
}
return false;
}
/* ---------------------------------------
Helpers for responding to HTTP requests
--------------------------------------- */
function finish_recv(response, packet) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(packet.toString('base64'));
response.write('\r\n.\r\n');
response.end();
}
function poll_timeout(response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(POLL_TIMEOUT_RESPONSE);
response.end();
}
/* --------------------------------------
Handlers for events from other modules
-------------------------------------- */
/*
Handler for received UDP packet
*/
function udp_recv(packet) {
var response, meta = {};
if (decodePacket(packet, meta)) {
showK2Packet('Rx', packet.length, meta, k2_map.get_poll_sequence(meta.urn));
encodePacket(packet, meta.clinum);
response = k2_map.get_response(meta.urn);
if (typeof response === 'object') {
finish_recv(response, packet);
} else {
k2_map.put_packet(meta.urn, packet);
}
}
}
k2_udp.set_receiver(udp_recv);
/*
Handler for timed out HTTP GET '/recv' requests
*/
function recv_expired(urn, response) {
trace_info('recv expired for ' + urn);
poll_timeout(response);
}
k2_map.set_expiry_handler(recv_expired);
/* ---------------------
HTTP request handlers
--------------------- */
/*
Handler for HTTP GET '/init'
*/
function init(response, init_message) {
var buf, urn;
buf = new Buffer(init_message, 'base64');
if (decryptMessage(buf)) {
urn = buf.slice(K2PROXY_NONCE_SIZE, -K2PROXY_MIC_SIZE).toString('utf8');
trace('init(' + urn + ')');
setTimeout(function () {
var b = new Buffer(K2PROXY_INIT_RESPONSE_SIZE), n = {};
k2_map.get_sequence_numbers(urn, n);
b.writeUInt32BE(n.poll_sequence, K2PROXY_NONCE_SIZE);
b.writeUInt32BE(n.send_sequence, K2PROXY_NONCE_SIZE + K2PROXY_SEQUENCE_NUMBER_SIZE);
encryptMessage(b);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(b.toString('base64'));
response.write('\r\n.\r\n');
response.end();
}, INIT_RESPONSE_DELAY_MS);
} else {
router.show404(response);
}
}
/*
Handler for HTTP GET '/recv'
*/
function recv(response, poll_message) {
var buf, sequence, urn, old_response, packet;
buf = new Buffer(poll_message, 'base64');
if (decryptMessage(buf)) {
sequence = buf.readUInt32BE(K2PROXY_NONCE_SIZE);
urn = buf.slice(K2PROXY_NONCE_SIZE + K2PROXY_SEQUENCE_NUMBER_SIZE, -K2PROXY_MIC_SIZE).toString('utf8');
trace('recv(' + urn + '), seq(' + sequence + ')');
// if an old response object is mapped, complete it with 'timeout' status
old_response = k2_map.get_response(urn);
if (typeof old_response === 'object') {
trace_info('completing deferred /recv/ response with timeout status');
poll_timeout(old_response);
}
// if a packet is waiting, return it now, else save the response object for when a packet arrives
if (k2_map.check_poll_sequence(urn, sequence)) {
packet = k2_map.get_packet(urn);
if (typeof packet === 'object') {
finish_recv(response, packet);
} else {
k2_map.set_response(urn, response);
}
} else {
router.show404(response);
}
} else {
router.show404(response);
}
}
/*
Handler for HTTP POST '/send'
*/
function send(response, b64) {
var buf, sequence, packet, meta;
buf = new Buffer(b64, 'base64');
trace('send "' + b64 + '"');
if (!decryptMessage(buf)) {
trace('send: decrypt failed');
return;
}
sequence = buf.readUInt32BE(K2PROXY_NONCE_SIZE);
packet = buf.slice(K2PROXY_NONCE_SIZE + K2PROXY_SEQUENCE_NUMBER_SIZE, -K2PROXY_MIC_SIZE);
meta = {};
if (decodePacket(packet, meta)) {
showK2Packet('Tx', packet.length, meta, sequence);
if (k2_map.check_send_sequence(meta.urn, sequence)) {
encodePacket(packet, meta.inum);
k2_udp.send(packet);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(SEND_MESSAGE_RESPONSE);
response.end();
} else {
router.show404(response);
}
} else {
router.show404(response);
}
}
exports.MIN_BASE64_MESSAGE_LEN = MIN_BASE64_MESSAGE_LEN;
exports.init = init;
exports.recv = recv;
exports.send = send;