2021-10-02 00:16:34 +00:00
"use strict" ;
2021-10-02 16:34:15 +00:00
import crypto from 'crypto' ;
2021-10-02 00:16:34 +00:00
import net from 'net' ;
import { EventEmitter , once } from 'events' ;
2022-01-09 16:30:42 +00:00
import log from '../io/NamespacedLog.mjs' ; const l = log ( "connection" ) ;
2021-10-02 00:16:34 +00:00
import settings from '../../settings.mjs' ;
2021-10-02 02:02:49 +00:00
import rekey from './rekey.mjs' ;
2021-10-02 14:56:34 +00:00
import FramedTransport from './FramedTransport.mjs' ;
2021-10-02 02:02:49 +00:00
import { write _safe } from '../io/StreamHelpers.mjs' ;
import { encrypt _bytes , decrypt _bytes } from '../crypto/secretbox.mjs' ;
2021-10-02 00:16:34 +00:00
/ * *
* Represents a connection to a single endpoint .
2021-10-02 23:34:55 +00:00
* @ param { string } secret _join The shared join secret , encoded as base64
* @ param { net . Socket ? } socket Optional . A pre - existing socket to take over and manage .
2021-10-09 15:31:58 +00:00
*
* @ event Connection # connect The initial connection setup is complete .
* @ event Connection # rekey The session key has been re - exchanged .
* @ event Connection # destroy The connection has been closed
* @ event Connection # message A message has been received .
* @ event Connection # message - EVENTNAME A message with a given event name has been received
2021-10-02 00:16:34 +00:00
* /
class Connection extends EventEmitter {
2021-10-03 01:33:54 +00:00
/ * *
* Whether this socket is actually connected or not .
* @ return { bool }
* /
get connected ( ) {
2021-10-03 11:24:32 +00:00
return this . framer == null ? false : this . framer . connected ;
2021-10-03 01:33:54 +00:00
}
constructor ( secret _join , socket = null ) {
2021-10-02 00:16:34 +00:00
super ( ) ;
2021-10-02 02:02:49 +00:00
2021-10-02 23:34:55 +00:00
if ( typeof secret _join !== "string" )
throw new Error ( ` Error: Expected secret_join to be of type string, but received variable of type ${ typeof secret _join } ` ) ;
2021-10-02 16:00:24 +00:00
this . socket = socket ;
this . rekey _last = null ;
2021-10-02 16:34:15 +00:00
this . rekey _interval _base = 30 * 60 * 1000 ; // 30 minutes
this . rekey _interval = this . rekey _interval _base + crypto . randomInt ( 0 , 15 * 60 * 1000 ) ;
this . rekey _in _progress = false ;
2021-10-02 02:02:49 +00:00
this . session _key = Buffer . from ( secret _join , "base64" ) ;
2021-10-02 00:16:34 +00:00
}
/ * *
* Connects to a peer and initialises a secure TCP connection thereto .
* @ param { string } address The address to connect to .
* @ param { string } port The TCP port to connect to .
* @ return { net . Socket } A socket setup for secure communication .
* /
async connect ( address , port ) {
this . address = address ; this . port = port ;
2021-10-02 23:34:55 +00:00
this . socket = new net . Socket ( ) ;
2021-10-02 00:16:34 +00:00
this . socket . connect ( {
address , port
} ) ;
2021-10-02 23:34:55 +00:00
this . socket . once ( "end" , ( ) => {
2022-01-09 16:30:42 +00:00
l . log ( ` ${ this . address } : ${ this . port } disconnected ` ) ;
2021-10-02 23:34:55 +00:00
} ) ;
2021-10-02 00:16:34 +00:00
await once ( this . socket , "connect" ) ;
2021-10-02 16:00:24 +00:00
await this . init ( ) ;
}
async init ( ) {
2021-10-02 23:34:55 +00:00
this . address = this . socket . remoteAddress ;
this . port = this . socket . remotePort ;
2021-10-02 00:16:34 +00:00
this . socket . setKeepAlive ( true ) ;
2021-10-02 14:56:34 +00:00
this . framer = new FramedTransport ( this . socket ) ;
2021-10-02 23:34:55 +00:00
this . framer . on ( "frame" , this . handle _frame . bind ( this ) ) ;
2021-10-02 00:16:34 +00:00
2021-10-02 23:34:55 +00:00
await this . rekey ( ) ;
2021-10-03 11:24:32 +00:00
// We can await .init() or .connect() - this is just another optiom
2021-10-09 15:31:58 +00:00
/ * *
* The initial connection setup is complete .
* @ event Connection # connect
* @ type { void }
* /
2021-10-03 11:24:32 +00:00
this . emit ( ` connect ` ) ;
2021-10-02 00:16:34 +00:00
}
2021-10-02 16:00:24 +00:00
async rekey ( ) {
2021-10-02 16:34:15 +00:00
try {
this . rekey _in _progress = true ;
this . session _key = await rekey ( this , this . session _key ) ;
this . rekey _interval = this . rekey _interval _base + crypto . randomInt ( 0 , 15 * 60 * 1000 ) ;
this . rekey _last = new Date ( ) ;
2021-10-09 15:31:58 +00:00
/ * *
* The session key has been re - exchanged .
* @ event Connection # rekey
* @ type { void }
* /
2021-10-02 16:34:15 +00:00
this . emit ( "rekey" ) ;
}
catch ( error ) {
2021-10-02 23:34:55 +00:00
l . warn ( ` Error when rekeying connection ${ this . address } : ${ this . port } , killing connection ` , settings . cli . verbose ? error : error . message ) ;
2021-10-02 16:34:15 +00:00
await this . destroy ( ) ;
}
finally {
this . rekey _in _progress = false ;
}
2021-10-02 16:00:24 +00:00
}
async destroy ( ) {
2022-01-09 17:37:06 +00:00
l . debug ( ` Killing connection to ${ this . address } : ${ this . port } ` , new Error ( ) . stack ) ;
2021-10-02 23:34:55 +00:00
if ( this . framer instanceof FramedTransport )
await this . framer . destroy ( ) ;
else {
await this . socket . end ( ) ;
await this . socket . destroy ( ) ;
}
2021-10-09 15:31:58 +00:00
/ * *
* The connection has been closed
* @ event Connection # destroy
* @ type { void }
* /
2021-10-02 00:16:34 +00:00
this . emit ( "destroy" ) ;
2021-10-09 17:00:54 +00:00
this . removeAllListeners ( ) ;
2021-10-02 00:16:34 +00:00
}
2021-10-02 14:56:34 +00:00
async handle _frame ( bytes ) {
2021-10-02 00:16:34 +00:00
try {
2021-10-03 11:14:57 +00:00
// l.info(`FRAME length`, bytes.length, `frame`, bytes);
2021-10-03 01:33:54 +00:00
let decrypted = decrypt _bytes ( this . session _key , bytes ) ;
2021-10-02 23:34:55 +00:00
if ( decrypted === null ) {
l . warn ( ` Decryption of message failed ` ) ;
return ;
}
await this . handle _message ( decrypted . toString ( "utf-8" ) ) ;
2021-10-02 00:16:34 +00:00
}
catch ( error ) {
2021-10-02 23:34:55 +00:00
l . warn ( ` Warning: Killing connection to ${ this . address } : ${ this . port } after error: ` , settings . cli . verbose ? error : error . message ) ;
2021-10-02 00:16:34 +00:00
this . destroy ( ) ;
}
}
2021-10-02 14:56:34 +00:00
async handle _message ( msg _text ) {
const msg = JSON . parse ( msg _text ) ;
2021-10-02 16:34:15 +00:00
2021-10-03 11:14:57 +00:00
l . debug ( ` RECEIVE: ${ msg . event } ` , msg . message ) ;
2021-10-02 23:34:55 +00:00
if ( msg . event == "rekey" && ! this . rekey _in _progress ) {
2021-10-02 16:34:15 +00:00
// Set and forget here
2021-10-02 23:34:55 +00:00
this . rekey ( ) ;
2021-10-02 16:34:15 +00:00
}
2021-10-09 15:31:58 +00:00
/ * *
* A message has been received .
* @ event Connection # message
* @ type { string , object } The name of the event , followed by the message content .
* /
2021-10-02 16:34:15 +00:00
this . emit ( "message" , msg . event , msg . message ) ;
2021-10-09 15:31:58 +00:00
/ * *
* A message with a specific event name has been received .
* @ event Connection # message - EVENTNAME
* @ type { object } The message content .
* /
2021-10-02 16:34:15 +00:00
this . emit ( ` message- ${ msg . event } ` , msg . message ) ;
2021-10-02 00:16:34 +00:00
}
2021-10-02 02:02:49 +00:00
2021-10-02 16:34:15 +00:00
async send ( event , message ) {
if ( typeof event !== "string" ) throw new Error ( ` Error: Expected string for event name, but got value of type ${ typeof event } . ` ) ;
2021-10-03 11:14:57 +00:00
l . debug ( ` SEND event ` , event , ` message ` , message ) ;
2021-10-03 01:33:54 +00:00
2021-10-02 16:34:15 +00:00
// Rekey at semi-regular intervals, but only if we're not already in the process of doing so
if ( new Date ( ) - this . rekey _last > this . rekey _interval && ! this . rekey _in _progress )
2021-10-02 16:00:24 +00:00
await this . rekey ( ) ;
2021-10-02 16:34:15 +00:00
// TODO: Consider anonymous TLS, with jpake for mututal authentication
2021-10-02 02:03:49 +00:00
// TODO: Consider https://devdocs.io/node/crypto#crypto.createCipheriv() - which lets us use any openssl ciphers we like - e.g. ChaCha20-Poly1305
2022-02-09 03:03:45 +00:00
2022-02-09 03:14:26 +00:00
// TODO: We're currently vulnerable to a replay attack. We need to mitigate this somehow - probably by maintaining a sequence number. Instead of sending the sequence number though we should instead compute a MAC that also includes the message length and a bunch of other things etc. Of course, we will also need to make sure we don't fall afoul of mac-then-encrypt, encrypt-then-mac, etc issues...
2022-02-09 03:03:45 +00:00
2021-10-02 16:34:15 +00:00
let payload = JSON . stringify ( { event , message } ) ;
2021-10-02 23:34:55 +00:00
payload = encrypt _bytes (
this . session _key ,
Buffer . from ( payload , "utf-8" )
) ;
2021-10-02 16:00:24 +00:00
2021-10-03 01:33:54 +00:00
return await this . framer . write ( payload ) ;
2021-10-02 02:02:49 +00:00
}
2021-10-02 00:16:34 +00:00
}
2021-10-02 16:00:24 +00:00
Connection . Wrap = async function ( secret _join , socket ) {
2021-10-02 23:34:55 +00:00
const socket _wrap = new Connection ( secret _join , socket ) ;
await socket _wrap . init ( ) ;
2021-10-02 16:00:24 +00:00
2021-10-02 23:34:55 +00:00
return socket _wrap ;
2021-10-02 16:00:24 +00:00
}
Connection . Create = async function ( secret _join , address , port ) {
const socket = new Connection ( secret _join ) ;
2021-10-02 23:34:55 +00:00
await socket . connect ( address , port ) ;
return socket ;
2021-10-02 16:00:24 +00:00
}
2021-10-02 00:16:34 +00:00
export default Connection ;