Permalink
Browse files

lib: add support for DH group exchange

This should be available for node v0.11.12+
  • Loading branch information...
1 parent 8a02ec5 commit a95bef762a5705e3b2f8cd9bf78797dcb013e4b4 @mscdex committed Mar 9, 2014
Showing with 196 additions and 46 deletions.
  1. +145 −27 lib/Connection.js
  2. +10 −0 lib/Parser.constants.js
  3. +41 −19 lib/Parser.js
View
@@ -19,6 +19,7 @@ var Parser = require('./Parser'),
var MODULE_VER = require('../package.json').version,
SSH_IDENT = 'SSH-2.0-ssh2js' + MODULE_VER,
MAX_CHANNEL = Math.pow(2, 32) - 1,
+ RE_SHA1 = /^group|gex-sha1$/i,
ALGORITHMS = consts.ALGORITHMS,
MESSAGE = consts.MESSAGE,
SSH_TO_OPENSSL = consts.SSH_TO_OPENSSL,
@@ -27,8 +28,17 @@ var MODULE_VER = require('../package.json').version,
EMPTY_BUFFER = new Buffer(0),
PING_PACKET = new Buffer([MESSAGE.IGNORE, 0, 0, 0, 0]),
AUTO_KB_PACKET = new Buffer([consts.USERAUTH_INFO_RESPONSE, 0, 0, 0, 0]),
- NEWKEYS_PACKET = new Buffer([MESSAGE.NEWKEYS]);
-
+ NEWKEYS_PACKET = new Buffer([MESSAGE.NEWKEYS]),
+ KEXDH_GEX_REQ_PACKET = new Buffer([
+ consts.KEXDH_GEX_REQUEST,
+ // minimal size in bits of an acceptable group
+ 0, 0, 4, 0, // 1024, modp2
+ // preferred size in bits of the group the server will send
+ 0, 0, 10, 0, // 4096, modp16
+ // maximal size in bits of an acceptable group
+ 0, 0, 20, 0 // 8192, modp18
+ ]);
+require('buffer').INSPECT_MAX_BYTES = Infinity;
function Connection(opts) {
if (!(this instanceof Connection))
return new Connection(opts);
@@ -68,6 +78,7 @@ function Connection(opts) {
this._hmac = false;
this._server_ident_raw = undefined;
this._kexinit = undefined;
+ this._kexdh = undefined;
this._sessionid = undefined;
this._curChan = -1;
@@ -101,6 +112,10 @@ function Connection(opts) {
onKEXDH_REPLY(self, info);
});
+ this._parser.on('KEXDH_GEX_GROUP', function(prime, gen) {
+ onKEXDH_GEX_GROUP(self, prime, gen);
+ });
+
this._parser.on('NEWKEYS', function() {
onNEWKEYS(self);
});
@@ -219,6 +234,7 @@ Connection.prototype.connect = function(opts) {
this._hmac = false;
this._server_ident_raw = undefined;
this._kexinit = undefined;
+ this._kexdh = undefined;
this._sessionid = undefined;
this._pinger = undefined;
@@ -1525,25 +1541,55 @@ function checkSKEXInit(self, init) {
else if (kex_algorithm === 'diffie-hellman-group14-sha1')
self._kex = crypto.getDiffieHellman('modp14');
- self._pubkey = new Buffer(self._kex.generateKeys('binary'), 'binary');
+ if (self._kex) {
+ self._kexdh = self._parser._kexdh = 'group';
+ self._pubkey = new Buffer(self._kex.generateKeys('binary'), 'binary');
+ if (self._pubkey[0] & 0x80) {
+ var key = new Buffer(self._pubkey.length + 1);
+ key[0] = 0;
+ self._pubkey.copy(key, 1);
+ self._pubkey = key;
+ }
+ } else if (kex_algorithm === 'diffie-hellman-group-exchange-sha1')
+ self._kexdh = self._parser._kexdh = 'gex-sha1';
+ else if (kex_algorithm === 'diffie-hellman-group-exchange-sha256')
+ self._kexdh = self._parser._kexdh = 'gex-sha256';
+
+ return true;
+}
+
+function sendKEXDHGEXReq(self) {
+ self._debug && self._debug('DEBUG: Connection: Sending KEXDH_GEX_REQUEST');
+ if (self._state === 'reexchg')
+ self._send(KEXDH_GEX_REQ_PACKET, undefined, true);
+ else
+ self._send(KEXDH_GEX_REQ_PACKET);
+}
+function onKEXDH_GEX_GROUP(self, prime, gen) {
+ self._kex = crypto.createDiffieHellman(prime, gen);
+ self._pubkey = new Buffer(self._kex.generateKeys('binary'), 'binary');
if (self._pubkey[0] & 0x80) {
var key = new Buffer(self._pubkey.length + 1);
key[0] = 0;
self._pubkey.copy(key, 1);
self._pubkey = key;
}
-
- return true;
+ sendKEXDHInit(self);
}
function sendKEXDHInit(self) {
var bufDHInit = new Buffer(1 + 4 + self._pubkey.length);
- bufDHInit[0] = MESSAGE.KEXDH_INIT;
+ if (self._kexdh !== 'group') {
+ bufDHInit[0] = consts.KEXDH_GEX_INIT;
+ self._debug && self._debug('DEBUG: Connection: Sending KEXDH_GEX_INIT');
+ } else {
+ bufDHInit[0] = MESSAGE.KEXDH_INIT;
+ self._debug && self._debug('DEBUG: Connection: Sending KEXDH_INIT');
+ }
bufDHInit.writeUInt32BE(self._pubkey.length, 1, true);
self._pubkey.copy(bufDHInit, 5);
- self._debug && self._debug('DEBUG: Connection: Sent KEXDH_INIT');
if (self._state === 'reexchg')
self._send(bufDHInit, undefined, true);
else
@@ -1567,8 +1613,12 @@ function onKEXINIT(self, init) {
doCheck();
function doCheck() {
- if (checkSKEXInit(self, init) === true)
- sendKEXDHInit(self);
+ if (checkSKEXInit(self, init) === true) {
+ if (self._kexdh !== 'group')
+ sendKEXDHGEXReq(self);
+ else
+ sendKEXDHInit(self);
+ }
}
}
@@ -1616,16 +1666,32 @@ function onKEXDH_REPLY(self, info) {
info.pubkey = info.pubkey.slice(slicepos + 1);
var compSecret = self._kex.computeSecret(info.pubkey, 'binary', 'binary');
info.secret = new Buffer(compSecret, 'binary');
- // SHA1 for supported DH group1 and group14 kex methods
- var hash = crypto.createHash('sha1');
+ var hash = crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256');
var len_ident = Buffer.byteLength(SSH_IDENT),
len_sident = Buffer.byteLength(self._server_ident_raw),
len_init = self._kexinit.length,
len_sinit = self._parser._kexinit.length,
len_hostkey = info.hostkey.length,
+ len_gex_req = 0,
+ len_gex_prime = 0,
+ len_gex_gen = 0,
len_pubkey = self._pubkey.length,
len_spubkey = info.pubkey.length,
len_secret = info.secret.length;
+ var gex_prime, gex_gen;
+ if (self._kexdh !== 'group') {
+ len_gex_req = 12;
+ gex_prime = self._kex.getPrime();
+ gex_gen = self._kex.getGenerator();
+ len_gex_prime = gex_prime.length;
+ len_gex_gen = gex_gen.length;
+ if (gex_prime[0] & 0x80)
+ ++len_gex_prime;
+ if (gex_gen[0] & 0x80)
+ ++len_gex_gen;
+ }
if (self._pubkey[0] & 0x80)
++len_pubkey;
if (info.pubkey[0] & 0x80)
@@ -1638,10 +1704,14 @@ function onKEXDH_REPLY(self, info) {
+ len_init
+ len_sinit
+ len_hostkey
+ + len_gex_req
+ + len_gex_prime
+ + len_gex_gen
+ len_pubkey
+ len_spubkey
+ len_secret
- + (4 * 8));
+ + (4 * 8)
+ + (self._kexdh !== 'group' ? (4 * 2) : 0));
exchangeBuf.writeUInt32BE(len_ident, bp, true);
bp += 4;
exchangeBuf.write(SSH_IDENT, bp, 'utf8'); // V_C
@@ -1667,6 +1737,25 @@ function onKEXDH_REPLY(self, info) {
info.hostkey.copy(exchangeBuf, bp); // K_S
bp += len_hostkey;
+ if (self._kexdh !== 'group') {
+ KEXDH_GEX_REQ_PACKET.slice(1).copy(exchangeBuf, bp); // min, n, max
+ bp += len_gex_req;
+
+ exchangeBuf.writeUInt32BE(len_gex_prime, bp, true);
+ bp += 4;
+ if (gex_prime[0] & 0x80)
+ exchangeBuf[bp++] = 0;
+ gex_prime.copy(exchangeBuf, bp); // p
+ bp += len_gex_prime - (gex_prime[0] & 0x80 ? 1 : 0);
+
+ exchangeBuf.writeUInt32BE(len_gex_gen, bp, true);
+ bp += 4;
+ if (gex_gen[0] & 0x80)
+ exchangeBuf[bp++] = 0;
+ gex_gen.copy(exchangeBuf, bp); // g
+ bp += len_gex_gen - (gex_gen[0] & 0x80 ? 1 : 0);
+ }
+
exchangeBuf.writeUInt32BE(len_pubkey, bp, true);
bp += 4;
if (self._pubkey[0] & 0x80)
@@ -1772,17 +1861,24 @@ function onKEXDH_REPLY(self, info) {
}
function onNEWKEYS(self) {
- var iv, key, blocklen = 8, keylen = 0, p = 0,
- secret, len_secret = (self._kexreply.secret[0] & 0x80 ? 1 : 0)
- + self._kexreply.secret.length;
+ var iv,
+ key,
+ blocklen = 8,
+ keylen = 0,
+ p = 0,
+ secret,
+ len_secret = (self._kexreply.secret[0] & 0x80 ? 1 : 0)
+ + self._kexreply.secret.length;
secret = new Buffer(4 + len_secret);
secret.writeUInt32BE(len_secret, p, true);
p += 4;
if (self._kexreply.secret[0] & 0x80)
secret[p++] = 0;
self._kexreply.secret.copy(secret, p);
if (!isStreamCipher(self._encryptType)) {
- iv = new Buffer(crypto.createHash('sha1')
+ iv = new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update('A', 'ascii')
@@ -1805,7 +1901,9 @@ function onNEWKEYS(self) {
}
self._encryptSize = blocklen;
while (blocklen > iv.length) {
- iv = Buffer.concat([iv, new Buffer(crypto.createHash('sha1')
+ iv = Buffer.concat([iv, new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update(iv)
@@ -1841,14 +1939,18 @@ function onNEWKEYS(self) {
keylen = 16; // eg. 128 / 8
break;
}
- key = new Buffer(crypto.createHash('sha1')
+ key = new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update('C', 'ascii')
.update(self._sessionid)
.digest('binary'), 'binary');
while (keylen > key.length) {
- key = Buffer.concat([key, new Buffer(crypto.createHash('sha1')
+ key = Buffer.concat([key, new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update(key)
@@ -1872,7 +1974,9 @@ function onNEWKEYS(self) {
blocklen = 8;
keylen = 0;
if (!isStreamCipher(self._parser._decryptType)) {
- iv = new Buffer(crypto.createHash('sha1')
+ iv = new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update('B', 'ascii')
@@ -1898,7 +2002,9 @@ function onNEWKEYS(self) {
else
self._parser._decryptSize = blocklen;
while (blocklen > iv.length) {
- iv = Buffer.concat([iv, new Buffer(crypto.createHash('sha1')
+ iv = Buffer.concat([iv, new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update(iv)
@@ -1938,14 +2044,18 @@ function onNEWKEYS(self) {
keylen = 16; // eg. 128 / 8
break;
}
- key = new Buffer(crypto.createHash('sha1')
+ key = new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update('D', 'ascii')
.update(self._sessionid)
.digest('binary'), 'binary');
while (keylen > key.length) {
- key = Buffer.concat([key, new Buffer(crypto.createHash('sha1')
+ key = Buffer.concat([key, new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update(key)
@@ -2019,14 +2129,18 @@ function onNEWKEYS(self) {
}
if (!isGCM(self._encryptType)) {
- key = new Buffer(crypto.createHash('sha1')
+ key = new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update('E', 'ascii')
.update(self._sessionid)
.digest('binary'), 'binary');
while (createKeyLen > key.length) {
- key = Buffer.concat([key, new Buffer(crypto.createHash('sha1')
+ key = Buffer.concat([key, new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update(key)
@@ -2036,14 +2150,18 @@ function onNEWKEYS(self) {
} else
self._hmacKey = undefined;
if (!isGCM(self._parser._decryptType)) {
- key = new Buffer(crypto.createHash('sha1')
+ key = new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update('F', 'ascii')
.update(self._sessionid)
.digest('binary'), 'binary');
while (checkKeyLen > key.length) {
- key = Buffer.concat([key, new Buffer(crypto.createHash('sha1')
+ key = Buffer.concat([key, new Buffer(crypto.createHash(RE_SHA1.test(self._kexdh)
+ ? 'sha1'
+ : 'sha256')
.update(secret)
.update(self._exchange_hash)
.update(key)
View
@@ -54,6 +54,10 @@ exports.USERAUTH_PASSWD_CHANGEREQ = 60;
exports.USERAUTH_PK_OK = 60;
exports.USERAUTH_INFO_REQUEST = 60;
exports.USERAUTH_INFO_RESPONSE = 61;
+exports.KEXDH_GEX_REQUEST = 34;
+exports.KEXDH_GEX_GROUP = 31;
+exports.KEXDH_GEX_INIT = 32;
+exports.KEXDH_GEX_REPLY = 33;
var DISCONNECT_REASON = exports.DISCONNECT_REASON = {
HOST_NOT_ALLOWED_TO_CONNECT: 1,
@@ -216,6 +220,12 @@ if (process.versions.openssl >= '1.0.1') {
'aes256-gcm',
'aes256-gcm@openssh.com'
].concat(CIPHER);
+
+ KEX = [
+ 'diffie-hellman-group-exchange-sha256',
+ 'diffie-hellman-group-exchange-sha1'
+ ].concat(KEX);
+ KEX_LIST = new Buffer(KEX.join(','));
}
CIPHER = [
// http://tools.ietf.org/html/rfc4344#section-4
Oops, something went wrong.

0 comments on commit a95bef7

Please sign in to comment.