Skip to content

Commit

Permalink
lib: perform async crypto init
Browse files Browse the repository at this point in the history
Projects like Electron may use the browser's implementation of
WebAssembly which may have strict limitations on compiling and/or
instantiating wasm files on the main thread. This commit uses the
async WebAssembly API instead, which should avoid those potential
issues.

Fixes: #1014
  • Loading branch information
mscdex committed May 29, 2021
1 parent 22dbedc commit 0a6cd71
Show file tree
Hide file tree
Showing 5 changed files with 727 additions and 636 deletions.
73 changes: 53 additions & 20 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const {
SUPPORTED_MAC,
SUPPORTED_SERVER_HOST_KEY,
} = require('./protocol/constants.js');
const { init: cryptoInit } = require('./protocol/crypto.js');
const Protocol = require('./protocol/Protocol.js');
const { parseKey } = require('./protocol/keyParser.js');
const { SFTP } = require('./protocol/SFTP.js');
Expand Down Expand Up @@ -624,21 +625,7 @@ class Client extends EventEmitter {
},
});

sock.on('data', (data) => {
try {
proto.parse(data, 0, data.length);
} catch (ex) {
this.emit('error', ex);
try {
if (isWritable(sock))
sock.end();
} catch {}
}
});

// Drain stderr if we are connection hopping using an exec stream
if (sock.stderr && typeof sock.stderr.resume === 'function')
sock.stderr.resume();
sock.pause();

// TODO: check keepalive implementation
// Keepalive-related
Expand Down Expand Up @@ -688,12 +675,47 @@ class Client extends EventEmitter {
}
};
})();
const onConnect = (() => {
let called = false;
return () => {
if (called)
return;
called = true;

wasConnected = true;
debug && debug('Socket connected');
this.emit('connect');

cryptoInit.then(() => {
sock.on('data', (data) => {
try {
proto.parse(data, 0, data.length);
} catch (ex) {
this.emit('error', ex);
try {
if (isWritable(sock))
sock.end();
} catch {}
}
});

// Drain stderr if we are connection hopping using an exec stream
if (sock.stderr && typeof sock.stderr.resume === 'function')
sock.stderr.resume();

sock.resume();
}).catch((err) => {
this.emit('error', err);
try {
if (isWritable(sock))
sock.end();
} catch {}
});
};
})();
let wasConnected = false;
sock.on('connect', () => {
wasConnected = true;
debug && debug('Socket connected');
this.emit('connect');
}).on('timeout', () => {
sock.on('connect', onConnect)
.on('timeout', () => {
this.emit('timeout');
}).on('error', (err) => {
debug && debug(`Socket error: ${err.message}`);
Expand Down Expand Up @@ -1018,6 +1040,17 @@ class Client extends EventEmitter {
} else {
// Custom socket passed in
startTimeout();
if (typeof sock.connecting === 'boolean') {
// net.Socket

if (!sock.connecting) {
// Already connected
onConnect();
}
} else {
// Assume socket/stream is already "connected"
onConnect();
}
}

return this;
Expand Down
75 changes: 46 additions & 29 deletions lib/protocol/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,11 @@ class NullCipher {
}


const CCP_ZEROS = Buffer.alloc(32);
const CCP_OUT_COMPUTE = Buffer.alloc(16);
const CCP_WASM_MODULE = require('./crypto/poly1305.js');
const CCP_RESULT_MALLOC = CCP_WASM_MODULE._malloc(16);
const poly1305_auth = CCP_WASM_MODULE.cwrap(
'poly1305_auth',
null,
['number', 'array', 'number', 'array', 'number', 'array']
);
const POLY1305_ZEROS = Buffer.alloc(32);
const POLY1305_OUT_COMPUTE = Buffer.alloc(16);
let POLY1305_WASM_MODULE;
let POLY1305_RESULT_MALLOC;
let poly1305_auth;
class ChaChaPolyCipherNative {
constructor(config) {
const enc = config.outbound;
Expand Down Expand Up @@ -199,35 +195,37 @@ class ChaChaPolyCipherNative {
return;

// Generate Poly1305 key
CCP_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
writeUInt32BE(CCP_OUT_COMPUTE, this.outSeqno, 12);
POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
writeUInt32BE(POLY1305_OUT_COMPUTE, this.outSeqno, 12);
const polyKey =
createCipheriv('chacha20', this._encKeyMain, CCP_OUT_COMPUTE)
.update(CCP_ZEROS);
createCipheriv('chacha20', this._encKeyMain, POLY1305_OUT_COMPUTE)
.update(POLY1305_ZEROS);

// Encrypt packet length
const pktLenEnc =
createCipheriv('chacha20', this._encKeyPktLen, CCP_OUT_COMPUTE)
createCipheriv('chacha20', this._encKeyPktLen, POLY1305_OUT_COMPUTE)
.update(packet.slice(0, 4));
this._onWrite(pktLenEnc);

// Encrypt rest of packet
CCP_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian)
POLY1305_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian)
const payloadEnc =
createCipheriv('chacha20', this._encKeyMain, CCP_OUT_COMPUTE)
createCipheriv('chacha20', this._encKeyMain, POLY1305_OUT_COMPUTE)
.update(packet.slice(4));
this._onWrite(payloadEnc);

// Calculate Poly1305 MAC
poly1305_auth(CCP_RESULT_MALLOC,
poly1305_auth(POLY1305_RESULT_MALLOC,
pktLenEnc,
pktLenEnc.length,
payloadEnc,
payloadEnc.length,
polyKey);
const mac = Buffer.allocUnsafe(16);
mac.set(
new Uint8Array(CCP_WASM_MODULE.HEAPU8.buffer, CCP_RESULT_MALLOC, 16),
new Uint8Array(POLY1305_WASM_MODULE.HEAPU8.buffer,
POLY1305_RESULT_MALLOC,
16),
0
);
this._onWrite(mac);
Expand Down Expand Up @@ -653,11 +651,11 @@ class ChaChaPolyDecipherNative {
if (this._lenPos < 4)
return;

CCP_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
writeUInt32BE(CCP_OUT_COMPUTE, this.inSeqno, 12);
POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
writeUInt32BE(POLY1305_OUT_COMPUTE, this.inSeqno, 12);

const decLenBytes =
createDecipheriv('chacha20', this._decKeyPktLen, CCP_OUT_COMPUTE)
createDecipheriv('chacha20', this._decKeyPktLen, POLY1305_OUT_COMPUTE)
.update(this._lenBuf);
this._len = readUInt32BE(decLenBytes, 0);

Expand Down Expand Up @@ -710,31 +708,33 @@ class ChaChaPolyDecipherNative {
}

// Generate Poly1305 key
CCP_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
writeUInt32BE(CCP_OUT_COMPUTE, this.inSeqno, 12);
POLY1305_OUT_COMPUTE[0] = 0; // Set counter to 0 (little endian)
writeUInt32BE(POLY1305_OUT_COMPUTE, this.inSeqno, 12);
const polyKey =
createCipheriv('chacha20', this._decKeyMain, CCP_OUT_COMPUTE)
.update(CCP_ZEROS);
createCipheriv('chacha20', this._decKeyMain, POLY1305_OUT_COMPUTE)
.update(POLY1305_ZEROS);

// Calculate and compare Poly1305 MACs
poly1305_auth(CCP_RESULT_MALLOC,
poly1305_auth(POLY1305_RESULT_MALLOC,
this._lenBuf,
4,
this._packet,
this._packet.length,
polyKey);

this._calcMac.set(
new Uint8Array(CCP_WASM_MODULE.HEAPU8.buffer, CCP_RESULT_MALLOC, 16),
new Uint8Array(POLY1305_WASM_MODULE.HEAPU8.buffer,
POLY1305_RESULT_MALLOC,
16),
0
);
if (!timingSafeEqual(this._calcMac, this._mac))
throw new Error('Invalid MAC');

// Decrypt packet
CCP_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian)
POLY1305_OUT_COMPUTE[0] = 1; // Set counter to 1 (little endian)
const packet =
createDecipheriv('chacha20', this._decKeyMain, CCP_OUT_COMPUTE)
createDecipheriv('chacha20', this._decKeyMain, POLY1305_OUT_COMPUTE)
.update(this._packet);

const payload = new FastBuffer(packet.buffer,
Expand Down Expand Up @@ -1602,6 +1602,23 @@ module.exports = {
CIPHER_INFO,
MAC_INFO,
bindingAvailable: !!binding,
init: (() => {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
POLY1305_WASM_MODULE = await require('./crypto/poly1305.js')();
POLY1305_RESULT_MALLOC = POLY1305_WASM_MODULE._malloc(16);
poly1305_auth = POLY1305_WASM_MODULE.cwrap(
'poly1305_auth',
null,
['number', 'array', 'number', 'array', 'number', 'array']
);
} catch (ex) {
return reject(ex);
}
resolve();
});
})(),

NullCipher,
createCipher,
Expand Down
Loading

0 comments on commit 0a6cd71

Please # to comment.