UNPKG

microtunnel-server

Version:

NodeJS library for post-quantum communication between apps

313 lines (306 loc) 13 kB
"use strict"; const { superSphincs } = require( 'supersphincs' ), { kyber } = require( 'kyber-crystals' ), symCryptor = require( 'symcryptor' ), { encode: cbEncode, decode: cbDecode} = require( 'cbor-x' ); if ( process.argv[1] === __filename && process.argv[2] === 'cred-generate' ) { Promise.all( [ superSphincs.keyPair(), symCryptor.rndBytes( 24 ) ] ) .then( res => { const cred = { publicKey: Buffer.from( res[0].publicKey ).toString( 'base64' ), privateKey: Buffer.from( res[0].privateKey ).toString( 'base64' ), agent: res[1].toString( 'base64' ) } console.log( JSON.stringify( cred, null, 4 ) ); } ) .catch( console.error ) .finally( process.exit ); } else { const octetParser = require( 'express' ).raw( { inflate: false, limit: '1mb', type: 'application/octet-stream' } ); const auth1Len = 64; const auth2Len = 31554; const servAuth = ( app, options = {} ) => { const defaultOptions = { api: '/microtunnel', resetMinutes: 15, appCredFile: process.env.APP_CRED, authClientsFile: process.env.AUTH_CLTS, ...options }; const { api, resetMinutes, appCredFile, authClientsFile } = defaultOptions; class AuthServerSex { constructor( name, clts ) { this.name = name; if ( clts.constructor !== Array ) clts = [clts]; this.clients = clts.map( c => ( { name, ip: c.ip, agent: c.agent, signature: Buffer.from( c.publicKey, 'base64' ), decrypting: 0, state: 0, key: undefined, shaKey: undefined, pendingReset: false, beginDecrypt() { this.decrypting = this.decrypting + 1; }, endDecrypt() { this.decrypting = this.decrypting - 1; if ( this.decrypting === 0 && this.pendingReset ) { this.reset(); } }, delete() { this.state = 0; this.key = undefined; this.shaKey = undefined; this.pendingReset = false; }, resetTimer() { clearTimeout( this.timer ); this.timer = setTimeout( () => { if ( this.decrypting ) return this.pendingReset = true; this.delete(); }, resetMinutes * 60000 ); }, reset() { this.resetTimer(); this.delete(); } } ) ); } } class AuthServerSessions extends Array { constructor( authClients ) { const clients = []; for ( const clt in authClients ) { clients.push( new AuthServerSex( clt, authClients[clt] ) ); } super( ...clients ); } get( str ) { if ( !str || typeof str !== 'string' ) return false; const found = this.find( el => el.name === str ); if ( found ) return found; return false; } findSrv( ip, agent ) { if ( ( !ip || typeof ip !== 'string' ) || ( !agent || typeof agent !== 'string' ) ) return false; let found = false; for ( let i = 0; i < this.length; i++ ) { const clt = this[i].clients.find( c => c.ip === ip && c.agent === agent ); if ( clt ) { found = clt; break; } } if ( found ) return found; return false; } } const sendError = function () { this.status( 404 ); this.send(); }; const sessions = new AuthServerSessions( require( authClientsFile ) ); const appCred = require( appCredFile ); const unless = ( middleware ) => { return ( req, res, next ) => { if ( req.originalUrl.startsWith( api + '/' ) ) { return next(); } else { return middleware( req, res, next ); } }; }; const origUse = app.use; app.use = function ( ...callbacks ) { if ( !callbacks.length ) throw new Error( '.use() method requires at least one function' ); if ( typeof callbacks[0] ==='string' || callbacks[0].constructor === RegExp || callbacks[0].constructor === Array ) { if ( !( callbacks.length -1 ) ) throw new Error( '.use() method requires at least one function' ); const route = callbacks.shift(); for ( let i = 0; i < callbacks.length; i++ ) { origUse.call( this, route, unless( callbacks[i] ) ); } } else { for ( let i = 0; i < callbacks.length; i++ ) { origUse.call( this, unless( callbacks[i] ) ); } } }; app.post( api +'/auth1', octetParser, ( req, res ) => { const ip = req.ip; const agent = req.get( 'User-Agent' ); if ( !ip || !agent ) return sendError.call( res ); const clt = sessions.findSrv( ip, agent ); if ( !clt ) return sendError.call( res ); clt.reset(); if ( parseInt( req.headers['content-length'], 10 ) !== auth1Len ) return sendError.call( res ); Promise.all( [ kyber.keyPair(), symCryptor.rndBytes( 64 ), superSphincs.signDetached( req.body, Buffer.from( appCred.privateKey, 'base64' ), appCred.agent ) ] ) .then( result => { clt.key = result[0].privateKey; clt.shaKey = result[1]; const ret = Buffer.concat( [result[0].publicKey, Buffer.from( result[2] ), result[1]] ); clt.state = 1; res.set( { 'Content-Type': 'application/octet-stream' } ); res.send( ret ); return res.end(); } ) .catch( () => sendError.call( res ) ); } ); app.post( api +'/auth2', octetParser, ( req, res ) => { const ip = req.ip; const agent = req.get( 'User-Agent' ); if ( !ip || !agent ) return sendError.call( res ); const clt = sessions.findSrv( ip, agent ); if ( !clt ) { return sendError.call( res ); } else if ( clt.state !== 1 ) { clt.reset(); return sendError.call( res ); } if ( parseInt( req.headers['content-length'], 10 ) !== auth2Len ) return sendError.call( res ); const data = new Uint8Array( req.body ); const ciphertext = data.slice( 0, 1568 ), encShaKey = data.slice( 1568, 1616 ), encSignedRnd = data.slice( 1616 ); kyber.decrypt( ciphertext, clt.key ) .then( async decryptedKey => { const shaKey = await symCryptor.decrypt( encShaKey, decryptedKey ); const signToCheck = new Uint8Array( await symCryptor.decrypt( encSignedRnd, decryptedKey, shaKey, clt.agent ) ); const verySign = await superSphincs.verifyDetached( signToCheck, clt.shaKey, clt.signature, shaKey ); if ( !verySign ) throw new Error( 'Internal server error' ); clt.key = decryptedKey; clt.shaKey = shaKey; const confirmation = await symCryptor.encrypt( Buffer.from( 'true' ), decryptedKey, shaKey, appCred.agent ); res.send( confirmation ); clt.state = 2; res.set( { 'Content-Type': 'application/octet-stream' } ); return res.end(); } ) .catch( () => sendError.call( res ) ); } ); const clientParser = ( clts, parseBody = false ) => { return async ( req, res, next ) => { const ip = req.ip; const agent = req.get( 'User-Agent' ); if ( !ip || !agent ) return sendError.call( res ); let clt; for ( let i = 0; i < clts.length; i++ ) { const found = clts[i].clients.find( c => c.ip === ip && c.agent === agent && c.state === 2 ); if ( found ) { clt = found; break; } } if ( !clt ) return sendError.call( res ); const cltClone = { name: clt.name, ip: clt.ip, agent: clt.agent, signature: clt.signature, state: clt.state, key: Buffer.from( clt.key ), shaKey: Buffer.from( clt.shaKey ), timer: clt.timer, delete() { clt.delete(); }, resetTimer() { clt.resetTimer(); }, reset() { clt.reset(); } }; req.tunnelClt = cltClone; const origSend = res.send; res.send = async function ( obj ) { try { const encrypted = await symCryptor.encrypt( cbEncode( obj ), cltClone.key, cltClone.shaKey, appCred.agent ); origSend.call( this, encrypted ); res.end(); } catch { res.status( 400 ); res.end(); } }; res.json = async ( obj ) => { await res.send( obj ); } res.set( { 'Content-Type': 'application/octet-stream' } ); if ( !parseBody ) return next(); while ( clt.pendingReset ) { await new Promise( r => setTimeout( r, 200 ) ); } clt.beginDecrypt(); try { const decBody = await symCryptor.decrypt( req.body, cltClone.key, cltClone.shaKey, cltClone.agent ); req.body = cbDecode( decBody ); next(); } catch { sendError.call( res ); } finally { clt.endDecrypt(); } }; }; const addAppCltRoute = ( cltNames, method, route = '/', ...callbacks ) => { const clts = []; if ( typeof cltNames === 'string' ) { clts.push( sessions.get( cltNames ) ); } else if ( cltNames.constructor === Array ) { for ( let clt of cltNames ) { if ( !clts.find( el => el.name === clt ) ) clts.push( sessions.get( clt ) ); } } else if ( cltNames === true ) { for ( let clt of sessions ) { clts.push( clt ); } } if ( !clts.length ) throw new Error( 'Invalid server name' ); if ( !clts.every( a => a ) ) throw new Error( 'Invalid server name' ); if ( !callbacks.length ) throw new Error( 'Callbacks argument requires at least one function' ); if ( method === 'get' ) { app.get( api + route, clientParser( clts ), ...callbacks ); } else if ( method === 'post' ) { app.post( api + route, octetParser, clientParser( clts, true ), ...callbacks ); } else { throw new Error( 'Invalid route method' ); } }; app.authGet = function ( clientName, route, ...callbacks ) { addAppCltRoute( clientName, 'get', route, ...callbacks ); } app.authPost = function ( clientName, route, ...callbacks ) { addAppCltRoute( clientName, 'post', route, ...callbacks ); } }; module.exports = servAuth; }