Skip to content

Commit

Permalink
feat: support NTLM authentication on Node.js 17+ out of the box (#1451)
Browse files Browse the repository at this point in the history
  • Loading branch information
mShan0 authored Jun 29, 2022
1 parent 7ebd236 commit 318aacc
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 168 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"bl": "^5.0.0",
"es-aggregate-error": "^1.0.8",
"iconv-lite": "^0.6.3",
"js-md4": "^0.3.2",
"jsbi": "^4.3.0",
"native-duplexpair": "^1.0.0",
"node-abort-controller": "^3.0.1",
Expand Down
43 changes: 15 additions & 28 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3416,34 +3416,21 @@ Connection.prototype.STATE = {
return this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL);
}
} else if (this.ntlmpacket) {
try {
const authentication = this.config.authentication as NtlmAuthentication;

const payload = new NTLMResponsePayload({
domain: authentication.options.domain,
userName: authentication.options.userName,
password: authentication.options.password,
ntlmpacket: this.ntlmpacket
});

this.messageIo.sendMessage(TYPE.NTLMAUTH_PKT, payload.data);
this.debug.payload(function() {
return payload.toString(' ');
});

this.ntlmpacket = undefined;

} catch (error: any) {
if (error.code === 'ERR_OSSL_EVP_UNSUPPORTED') {
const node17Message = new ConnectionError('Node 17 now uses OpenSSL 3, which considers md4 encryption a legacy type.' +
' In order to use NTLM with Node 17, enable the `--openssl-legacy-provider` command line flag.' +
' Check the Tedious FAQ for more information.', 'ELOGIN');
this.emit('connect', new AggregateError([error, node17Message]));
} else {
throw error;
}
return this.transitionTo(this.STATE.FINAL);
}
const authentication = this.config.authentication as NtlmAuthentication;

const payload = new NTLMResponsePayload({
domain: authentication.options.domain,
userName: authentication.options.userName,
password: authentication.options.password,
ntlmpacket: this.ntlmpacket
});

this.messageIo.sendMessage(TYPE.NTLMAUTH_PKT, payload.data);
this.debug.payload(function() {
return payload.toString(' ');
});

this.ntlmpacket = undefined;
} else if (this.loginError) {
if (isTransientError(this.loginError)) {
this.debug.log('Initiating retry on transient error');
Expand Down
3 changes: 2 additions & 1 deletion src/ntlm-payload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import WritableTrackingBuffer from './tracking-buffer/writable-tracking-buffer';
import * as crypto from 'crypto';
import JSBI from 'jsbi';
import md4 from 'js-md4';

interface Options {
domain: string;
Expand Down Expand Up @@ -143,7 +144,7 @@ class NTLMResponsePayload {

ntHash(text: string) {
const unicodeString = Buffer.from(text, 'ucs2');
return crypto.createHash('md4').update(unicodeString).digest();
return Buffer.from(md4.arrayBuffer(unicodeString));
}

hmacMD5(data: Buffer, key: Buffer) {
Expand Down
33 changes: 0 additions & 33 deletions test/integration/child-processes/ntlm-connect-node17.js

This file was deleted.

46 changes: 9 additions & 37 deletions test/integration/connection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const fs = require('fs');
const homedir = require('os').homedir();
const assert = require('chai').assert;
const os = require('os');
const childProcess = require('child_process');

import Connection from '../../src/connection';
import { ConnectionError, RequestError } from '../../src/errors';
Expand Down Expand Up @@ -476,44 +475,17 @@ describe('Ntlm Test', function() {
});
}

const nodeVersion = parseInt(process.versions.node.split('.')[0]);
if (nodeVersion <= 16 || (process.execArgv.includes('--openssl-legacy-provider') && nodeVersion >= 17)) {
it('should ntlm', function(done) {
runNtlmTest.call(this, done, DomainCaseEnum.AsIs);
});

it('should ntlm lower', function(done) {
runNtlmTest.call(this, done, DomainCaseEnum.Lower);
});

it('should ntlm upper', function(done) {
runNtlmTest.call(this, done, DomainCaseEnum.Upper);
});

} else {
const ntlmConfig = getNtlmConfig();

(ntlmConfig ? it : it.skip)('should throw an aggregate error with node 17', () => {
const child = childProcess.spawnSync(process.execPath,
['./test/integration/child-processes/ntlm-connect-node17.js'],
{ encoding: 'utf8' });
const thrownError = child.stderr;
assert.isTrue(thrownError.includes('ERR_OSSL_EVP_UNSUPPORTED'));
assert.isTrue(thrownError.includes('ConnectionError'));
assert.isTrue(thrownError.includes('--openssl-legacy-provider'));
assert.strictEqual(child.status, 1);
});

(ntlmConfig ? it : it.skip)('should ntlm with node 17 when `--openssl-legacy-provider` flag enabled', () => {
const child = childProcess.spawnSync(process.execPath,
['--openssl-legacy-provider',
'./test/integration/child-processes/ntlm-connect-node17.js'],
{ encoding: 'utf8' });
assert.strictEqual(child.status, 0);
});
it('should ntlm', function(done) {
runNtlmTest.call(this, done, DomainCaseEnum.AsIs);
});

}
it('should ntlm lower', function(done) {
runNtlmTest.call(this, done, DomainCaseEnum.Lower);
});

it('should ntlm upper', function(done) {
runNtlmTest.call(this, done, DomainCaseEnum.Upper);
});
});

describe('Encrypt Test', function() {
Expand Down
13 changes: 0 additions & 13 deletions test/unit/child-processes/ntlm-payload-node17.js

This file was deleted.

91 changes: 35 additions & 56 deletions test/unit/ntlm-payload-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const NTLMPayload = require('../../src/ntlm-payload');
const assert = require('chai').assert;
const childProcess = require('child_process');
const nodeVersion = parseInt(process.versions.node.split('.')[0]);

const challenge = {
domain: 'domain',
Expand All @@ -13,58 +11,39 @@ const challenge = {
}
};

if (nodeVersion <= 16 || (process.execArgv.includes('--openssl-legacy-provider') && nodeVersion >= 17)) {
describe('ntlm payload test', function() {
it('should respond to challenge', function() {

const response = new NTLMPayload(challenge);

const expectedLength =
8 + // NTLM protocol header
4 + // NTLM message type
8 + // lmv index
8 + // ntlm index
8 + // domain index
8 + // user index
16 + // header index
4 + // flags
2 * 6 + // domain
2 * 8 + // username
24 + // lmv2 data
16 + // ntlmv2 data
8 + // flags
8 + // timestamp
8 + // client nonce
4 + // placeholder
4 + // target data
4; // placeholder

const domainName = response.data.slice(64, 76).toString('ucs2');
const userName = response.data.slice(76, 92).toString('ucs2');
const targetData = response.data.slice(160, 164).toString('hex');

assert.strictEqual(domainName, 'domain');
assert.strictEqual(userName, 'username');
assert.strictEqual(targetData, 'aaaaaaaa');

assert.strictEqual(expectedLength, response.data.length);
});
});
} else {
describe('ntlm payload test node 17 and newer', function() {

it('should throw error when `--openssl-legacy-provider` is not enabled', function() {
try {
new NTLMPayload(challenge);
} catch (err) {
assert.strictEqual(err.code, 'ERR_OSSL_EVP_UNSUPPORTED');
}
});

it('should respond to challenge when `--openssl-legacy-provider` is enabled`', function() {
const child = childProcess.spawnSync(process.execPath, ['node_modules/mocha/bin/mocha', '--openssl-legacy-provider',
'./test/unit/child-processes/ntlm-payload-node17.js'], { encoding: 'utf8' });
assert.strictEqual(child.status, 0);
});
describe('ntlm payload test', function() {
it('should respond to challenge', function() {

const response = new NTLMPayload(challenge);

const expectedLength =
8 + // NTLM protocol header
4 + // NTLM message type
8 + // lmv index
8 + // ntlm index
8 + // domain index
8 + // user index
16 + // header index
4 + // flags
2 * 6 + // domain
2 * 8 + // username
24 + // lmv2 data
16 + // ntlmv2 data
8 + // flags
8 + // timestamp
8 + // client nonce
4 + // placeholder
4 + // target data
4; // placeholder

const domainName = response.data.slice(64, 76).toString('ucs2');
const userName = response.data.slice(76, 92).toString('ucs2');
const targetData = response.data.slice(160, 164).toString('hex');

assert.strictEqual(domainName, 'domain');
assert.strictEqual(userName, 'username');
assert.strictEqual(targetData, 'aaaaaaaa');

assert.strictEqual(expectedLength, response.data.length);
});
}
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},

"include": [
"types/*.d.ts",
"src/**/*.ts",
"test/**/*.ts",
"test/**/*.js",
Expand Down
7 changes: 7 additions & 0 deletions types/js-md4.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module 'js-md4' {
declare const md4: {
arrayBuffer(message: string | ArrayBuffer): ArrayBuffer;
};

export = md4;
}

0 comments on commit 318aacc

Please sign in to comment.