Comparing version 1.0.20 to 1.0.21
@@ -6,8 +6,6 @@ #!/usr/bin/env node | ||
// TODO: | ||
//X- Make into a Wyseman library call | ||
//X- How to abstract db= | ||
//- Implement encryption/wrapping of the stored key (compatible with Wylib version of client) | ||
//X- Implement encryption/wrapping of the stored key (compatible with Wylib version of client) | ||
//- | ||
const { Log } = require('wyclif') | ||
const Crypto = require('crypto') | ||
const Encrypt = require('wylib/src/encrypt') | ||
const Https = require('https') | ||
@@ -19,2 +17,14 @@ const Fetch = require('node-fetch') //Fetch via http | ||
const UserAgent = "Wyseman Websocket Client API" | ||
const Subtle = require('crypto').webcrypto.subtle | ||
const KeyConfig = { | ||
name: 'RSA-PSS', | ||
modulusLength: 2048, | ||
publicExponent: new Uint8Array([1,0,1]), | ||
hash: 'SHA-256' | ||
} | ||
const SignConfig = { //For signing with RSA-PSS | ||
name: 'RSA-PSS', | ||
saltLength: 128 | ||
} | ||
var encrypt = new Encrypt(require('crypto').webcrypto) | ||
@@ -24,45 +34,88 @@ module.exports = class { | ||
this.ca = conf.ca //Certificate Authority file | ||
this.dbInfo = conf.dbInfo //Custom DB listen coes | ||
this.dbInfo = conf.dbInfo //Custom DB listen codes | ||
this.httpPort = conf.httpPort //Where clientinfo query goes | ||
this.keyLength = conf.keyLength || defKeyLength //client's private key length | ||
this.log = conf.log || Log('Wyseman-client') | ||
this.ws = null //No websocket open yet | ||
this.ws = null //No websocket open yet | ||
} | ||
connect(credential, openCB) { //Initiate an authenticate websocket connection, as a specified user | ||
let { host, port, user, token, key } = credential //Grab properties from token or connecton key object | ||
, authString //Will become part of ws URI | ||
, myKey | ||
text2json(text) { //Parse text to a JSON object | ||
let json = {} | ||
if (text) try {json = JSON.parse(text)} catch(e) { | ||
this.log.error("Parsing ticket JSON: ", text) | ||
} | ||
return json | ||
} | ||
async connect(credential, openCB) { //Initiate an authenticate websocket connection, as a specified user | ||
let host, port, user, token, key | ||
if (credential.s && credential.i && credential.d) { //credentials are encrypted | ||
let password = this.passwordCB ? this.passwordCB() : null | ||
this.log.debug("Pre-decrypt:", credential) | ||
encrypt.decrypt(password, JSON.stringify(credential)).then(d=>{ | ||
let plainObj = this.text2json(d) | ||
this.log.debug("Post-decrypt:", plainObj) | ||
if (!('s' in plainObj)) //Call recursively with decrpyted credentials | ||
this.connect(plainObj, openCB) | ||
}).catch(e => this.log.error("Decrypting credentials: ", e.message)) | ||
return | ||
} else if ('login' in credential) { | ||
({ host, port, user, token, key } = credential.login) | ||
} else if ('host' in credential) { | ||
({ host, port, user, token, key } = credential) //Grab properties from token or connecton key object | ||
} else { | ||
this.log.error("Can't find required information in credential: ", credential) | ||
} | ||
if (token) { //The caller has a connection token | ||
let keyPair = Crypto.generateKeyPairSync('rsa',{ //We will build a new keypair for future connections | ||
modulusLength: this.keyLength, | ||
publicKeyEncoding: {type: 'spki', format: 'der'}, | ||
//Fixme: privateKeyEncoding: {type: 'pkcs8', format: 'der', cipher: 'aes-256-cbc', passphrase: ???} | ||
privateKeyEncoding: {type: 'pkcs8', format: 'der'} | ||
let origin = `https://${host}:${this.httpPort}` //Websocket runs within an http origin | ||
, headers = {"user-agent": UserAgent, cookie: Math.random()} | ||
, wsOptions = {origin, headers} //Used when opening websocket | ||
, openWebSocket = (auth) => { //Initiate websocket connection | ||
let dbHex = Buffer.from(JSON.stringify(this.dbInfo)).toString('base64url') //Make hex string of database listen codes | ||
, query = `user=${user}&db=${dbHex}&${auth}` //Build arguments for our URI | ||
, url = `wss://${host}:${port}/?${query}` //and then the full URI | ||
this.log.debug("Ws URL:", url) | ||
if (this.ca) wsOptions.ca = this.ca | ||
this.ws = new Ws(url, wsOptions) //Launch connection | ||
this.ws.on('open', () => openCB(this.ws)) //Invoke caller code when it opens | ||
this.ws.on('error', err => { | ||
if (!this.errCB) throw(err) | ||
this.errCB(err, this.ws) | ||
}) | ||
this.log.trace("Generated key:", user, keyPair.privateKey.toString('hex')) | ||
if (this.keyCB) this.keyCB( //Let the caller store his new key | ||
{login: {host, port, user, key:keyPair.privateKey.toString('hex')}} | ||
) | ||
authString = 'token=' + token + '&pub=' + keyPair.publicKey.toString('hex') | ||
} | ||
} else if (key) { //The caller already has a connection key | ||
let keyData = Buffer.from(key, 'hex') | ||
myKey = Crypto.createPrivateKey({key:keyData, format: 'der', type: 'pkcs8'}) | ||
if (token) { //The caller has a connection token | ||
let keyPair = await Subtle.generateKey(KeyConfig, true, ['sign','verify']) | ||
let exPriv = await Subtle.exportKey('jwk', keyPair.privateKey) | ||
this.log.trace("Generated key:", user, keyPair.privateKey) | ||
if (this.keyCB) { //Let the caller store the new key | ||
let password = this.passwordCB ? this.passwordCB() : null | ||
, saveKey = {login: {host, port, user, key:exPriv}} | ||
if (password) { //Encrypt key with a password? | ||
encrypt.encrypt(password, JSON.stringify(saveKey)).then(d=>this.keyCB(this.text2json(d))) | ||
} else { | ||
this.keyCB(saveKey) | ||
} | ||
} | ||
} else { | ||
// throw "Must specify a token or a key" | ||
let exPub = await Subtle.exportKey('jwk', keyPair.publicKey) | ||
, authString = 'token=' + token + '&pub=' + Buffer.from(JSON.stringify(exPub)).toString('base64url') | ||
this.log.trace("Public:", exPub) | ||
openWebSocket(authString) | ||
return | ||
} | ||
let origin = `https://${host}:${this.httpPort}` //Websocket runs within an http origin | ||
this.log.trace("key:", key) | ||
if (!key) {this.log.error("Connection requested without token or key!"); return} | ||
let myKey = await Subtle.importKey('jwk', key, KeyConfig, true, ['sign']) //The caller already has a connection key | ||
, clientUri = origin + '/clientinfo' //Will grab some data here to encrypt for connection handshake | ||
, headers = {"user-agent": UserAgent, cookie: Math.random()} | ||
, fetchOptions = {headers} //Used in fetch of that data | ||
, wsOptions = {origin,headers} //Used when opening websocket | ||
this.log.trace("myKey:", myKey) | ||
if (this.ca) { //Custom Certificate Authority provided | ||
let agent = new Https.Agent({ca:this.ca}) | ||
fetchOptions.agent = agent //So fetch will recognize our site | ||
wsOptions.ca = this.ca //So websocket will too | ||
} | ||
@@ -76,25 +129,11 @@ this.log.debug("Fetching client info from:", clientUri) | ||
let db = Buffer.from(JSON.stringify(this.dbInfo)).toString('hex') //Make hex string of database listen codes | ||
this.log.debug("DB:", db) | ||
if (myKey) { //If we already have a connection key | ||
let { ip, cookie, userAgent, date } = info //Fodder for what we will digitally sign | ||
, message = JSON.stringify({ip, cookie, userAgent, date}) //Message object has to be built in exactly this order | ||
, signer = Crypto.createSign('SHA256') | ||
, enc = new TextEncoder | ||
this.log.debug("message:", message) // Crypto.getHashes() | ||
//this.log.trace("myKey:", myKey.export({type: 'pkcs8', format: 'pem'})) | ||
signer.update(enc.encode(message)) | ||
signer.end() | ||
let sign = signer.sign({key: myKey, padding: Crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 128}, 'hex') | ||
authString = 'sign=' + sign + '&date=' + date | ||
} | ||
let { ip, cookie, userAgent, date } = info //Fodder for what we will digitally sign | ||
, message = JSON.stringify({ip, cookie, userAgent, date}) //Message object has to be built in exactly this order | ||
, enc = new TextEncoder | ||
this.log.debug("message:", message) | ||
let query = `user=${user}&db=${db}&${authString}` //Build argumeents for our URI | ||
, url = `wss://${host}:${port}/?${query}` //and the full URI | ||
this.log.debug("Ws URL:", url) | ||
this.ws = new Ws(url, wsOptions) //Instantiate the websocket connection | ||
this.ws.on('open', () => openCB(this.ws)) //Invoke caller code when connection opens | ||
this.ws.on('error', err => { | ||
if (!this.errCB) throw(err) | ||
this.errCB(err, this.ws) | ||
Subtle.sign(SignConfig, myKey, enc.encode(message)).then(sign =>{ | ||
this.log.debug("sign:", sign) | ||
let authString = 'sign=' + Buffer.from(sign).toString('base64url') + '&date=' + date | ||
openWebSocket(authString) | ||
}) | ||
@@ -108,10 +147,16 @@ }).catch(err => { | ||
this.log.trace("Setting handler:", event) | ||
if (event == 'key') { //When a new key is generated | ||
this.keyCB = handler | ||
} else if (event == 'error') { | ||
this.errCB = handler | ||
} else { | ||
error('Unknown event:', event) | ||
switch (event) { | ||
case 'key': //When a new key is generated | ||
this.keyCB = handler | ||
break | ||
case 'error': | ||
this.errCB = handler | ||
break | ||
case 'password': | ||
this.passwordCB = handler | ||
break | ||
default: | ||
error('Unknown event:', event) | ||
} | ||
} | ||
} //class Client |
@@ -17,5 +17,5 @@ //Manage the connection between a User Interface and the backend database | ||
const Url = require('url') | ||
const Base64 = require('base64-js') | ||
const Crypto = require('crypto') | ||
const Net = require('net') | ||
//const JWK2PEM = require('pem-jwk').jwk2pem //Shim until Node >= 16 mainstream | ||
@@ -114,11 +114,12 @@ const PemHeader = "-----BEGIN PUBLIC KEY-----\n" | ||
if (err) this.log.error("Error getting user connection key:", user, err) | ||
let pubKey = (!err && res && res.rows && res.rows.length >= 1) ? res.rows[0].conn_pub : null | ||
let pubString = (!err && res && res.rows && res.rows.length >= 1) ? res.rows[0].conn_pub : null | ||
, pubKey = JSON.parse(pubString) | ||
, valid = false //Assume failure | ||
this.log.trace(" public key:", pubKey, res ? res.rows : null) | ||
this.log.trace(" public key:", pubKey, typeof pubKey) | ||
if (pubKey && sign) { //We have the public key from the DB and the signed hash from the client | ||
let rawKey = Buffer.from(pubKey, 'hex') //Hex-to-binary | ||
, rawSig = Buffer.from(sign, 'hex') | ||
, key = PemHeader + Base64.fromByteArray(rawKey) + PemFooter //Raw-to-PEM | ||
let rawKey = Crypto.createPublicKey({key:pubKey,format:'jwk',encoding:'utf-8'}) //JWK to raw | ||
, key = rawKey.export({type:'spki', format: 'pem'}) //raw to PEM (Untested) | ||
, rawSig = Buffer.from(sign, 'base64') | ||
, verify = Crypto.createVerify('SHA256') //Make a verifier | ||
this.log.trace(" user public:", user, key) | ||
this.log.trace(" user public:", user, key, 'sign:', sign) | ||
verify.update(message) //Give it our message | ||
@@ -131,3 +132,3 @@ valid = verify.verify(Object.assign({key}, VerifyTpt), rawSig) //And check it | ||
} catch (e) { | ||
this.log.debug("Validating signature:", e) | ||
this.log.debug("Error validating signature:", e.message) | ||
}}) | ||
@@ -143,12 +144,12 @@ } | ||
, { user, db, sign, date, token, pub } = query | ||
, listen = db ? JSON.parse(Buffer.from(db,'hex').toString()) : null | ||
, listen = db ? JSON.parse(Buffer.from(db,'base64').toString()) : null | ||
, payload = req.WysemanPayload = {} //Custom Wyseman data to pass back to connection | ||
this.log.trace("Checking client:", origin, "cb:", !!cb, "q:", query, "s:", secure, "IP:", req.connection.remoteAddress, "pub:", pub) | ||
if (user && token && pub) //User connecting with a token | ||
this.validateToken(user, token, pub, listen, payload, (valid)=>{ | ||
if (user && token && pub) { //User connecting with a token | ||
let pubJSON = Buffer.from(pub,'base64').toString() | ||
this.validateToken(user, token, pubJSON, listen, payload, (valid)=>{ | ||
cb(valid, 403, 'Invalid Login') //Tell websocket whether or not to connect | ||
}) | ||
else if (user && sign && date) { //User has a signature | ||
} else if (user && sign && date) { //User has a signature | ||
let message = JSON.stringify({ip: req.connection.remoteAddress, cookie: req.headers.cookie, userAgent: req.headers['user-agent'], date}) | ||
@@ -155,0 +156,0 @@ , now = new Date() |
{ | ||
"name": "wyseman", | ||
"version": "1.0.20", | ||
"version": "1.0.21", | ||
"description": "PostgreSQL Schema Manager with Javascript, Ruby, TCL API", | ||
@@ -30,10 +30,10 @@ "main": "lib/index.js", | ||
"wyseman": "bin/wyseman", | ||
"wmmkpkg": "bin/wmmkpkg", | ||
"wysegi": "bin/wysegi" | ||
}, | ||
"dependencies": { | ||
"base64-js": "^1.5.1", | ||
"node-fetch": "^2.6.1", | ||
"pg": "^8.5.1", | ||
"node-fetch": "^2.6.2", | ||
"pg": "^8.7.1", | ||
"pg-format": "^1.0.4", | ||
"ws": "^7.4.3" | ||
"ws": "^7.5.5" | ||
}, | ||
@@ -40,0 +40,0 @@ "devDependencies": { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
676836
4
860
- Removedbase64-js@^1.5.1
- Removedbase64-js@1.5.1(transitive)
Updatednode-fetch@^2.6.2
Updatedpg@^8.7.1
Updatedws@^7.5.5