Skip to content

Commit 3f5b524

Browse files
authored
Merge pull request #209 from cplussharp/ldap_support
Support LDAP authentication
2 parents 1b8d858 + c955f80 commit 3f5b524

File tree

11 files changed

+313
-33
lines changed

11 files changed

+313
-33
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Below is a major feature comparison chart between the two drivers:
6666
| Password/PBKDF2 Authentication |:heavy_check_mark:|:heavy_check_mark:|
6767
| SAML Authentication |:heavy_check_mark:|:heavy_check_mark:|
6868
| JWT Authentication |:heavy_check_mark:|:heavy_check_mark:|
69-
| LDAP Authentication |:heavy_check_mark:|:x:|
69+
| LDAP Authentication |:heavy_check_mark:|:heavy_check_mark:|
7070
| Kerberos Authentication |:heavy_check_mark:|:x:|
7171
| X.509 Authentication |:heavy_check_mark:|:x:|
7272
| Secure User Store Integration (hdbuserstore) |:heavy_check_mark:|:x:|
@@ -195,7 +195,7 @@ This is suitable for multiple-host SAP HANA systems which are distributed over s
195195
Details about the different authentication methods can be found in the [SAP HANA Security Guide](https://help.sap.com/viewer/6b94445c94ae495c83a19646e7c3fd56/latest/en-US/440f6efe693d4b82ade2d8b182eb1efb.html).
196196

197197
#### User / Password
198-
Users authenticate themselves with their database `user` and `password`.
198+
Users authenticate themselves with their database `user` and `password`.
199199

200200
#### SAML assertion
201201
SAML bearer assertions as well as unsolicited SAML responses that include an

lib/protocol/Writer.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -621,21 +621,21 @@ function createReadStreamError() {
621621
function createBinaryOutBuffer(type, value) {
622622
var length = value.length;
623623
var buffer;
624-
if (length <= 245) {
624+
if (length <= common.DATA_LENGTH_MAX1BYTE_LENGTH) {
625625
buffer = new Buffer(2 + length);
626626
buffer[0] = type;
627627
buffer[1] = length;
628628
value.copy(buffer, 2);
629-
} else if (length <= 32767) {
629+
} else if (length <= common.DATA_LENGTH_MAX2BYTE_LENGTH) {
630630
buffer = new Buffer(4 + length);
631631
buffer[0] = type;
632-
buffer[1] = 246;
632+
buffer[1] = common.DATA_LENGTH_2BYTE_LENGTH_INDICATOR;
633633
buffer.writeInt16LE(length, 2);
634634
value.copy(buffer, 4);
635635
} else {
636636
buffer = new Buffer(6 + length);
637637
buffer[0] = type;
638-
buffer[1] = 247;
638+
buffer[1] = common.DATA_LENGTH_4BYTE_LENGTH_INDICATOR;
639639
buffer.writeInt32LE(length, 2);
640640
value.copy(buffer, 6);
641641
}

lib/protocol/auth/LDAP.js

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2022 SAP SE.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http: //www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing,
10+
// software distributed under the License is distributed on an
11+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12+
// either express or implied. See the License for the specific
13+
// language governing permissions and limitations under the License.
14+
'use strict';
15+
16+
var crypto = require('crypto');
17+
var util = require('../../util');
18+
var Fields = require('../data/Fields');
19+
20+
var CLIENT_NONCE_SIZE = 64;
21+
var CAPABILITIES_SIZE = 8;
22+
var DEFAULT_CAPABILITIES = 1;
23+
var SESSION_KEY_SIZE = 32; // AES256 key size
24+
25+
module.exports = LDAP;
26+
27+
/**
28+
* Handle LDAP authentication
29+
*
30+
* @param {object} options
31+
* @param {string|Buffer} options.password The LDAP password of the user
32+
* @param {Buffer} [options.clientChallenge] (for test only) the client nonce (64 bytes) to use
33+
* @param {Buffer} [options.sessionKey] (for test only) the AES256 key (32 bytes) for the encryption of the password
34+
*/
35+
function LDAP(options) {
36+
this.name = 'LDAP';
37+
this.password = options.password;
38+
if (util.isString(this.password)) {
39+
this.password = new Buffer(this.password, 'utf8');
40+
}
41+
this.clientNonce = options.clientChallenge || crypto.randomBytes(CLIENT_NONCE_SIZE);
42+
this.clientProof = null;
43+
this.sessionKey = options.sessionKey;
44+
}
45+
46+
/**
47+
* Return the initial data to send to HANA (client none + capabilities)
48+
* @return {Buffer}
49+
*/
50+
LDAP.prototype.initialData = function() {
51+
// prepare capabilities
52+
var capabilities = Buffer.allocUnsafe ? Buffer.allocUnsafe(CAPABILITIES_SIZE) : new Buffer(CAPABILITIES_SIZE);
53+
capabilities.writeInt8(DEFAULT_CAPABILITIES, 0);
54+
capabilities.fill(0, 1); // fill the remaining 7 bytes with 0
55+
56+
// write fields
57+
var data = Fields.write(null, [this.clientNonce, capabilities]).buffer;
58+
return data;
59+
};
60+
61+
/**
62+
* Gets the first response from the server and calculates the data for the next request
63+
* @param {Buffer} buffer
64+
* @param {function(Error?)} cb
65+
*/
66+
LDAP.prototype.initialize = function(buffer, cb) {
67+
// read server challenge
68+
var serverChallengeData = Fields.read({
69+
buffer: buffer
70+
});
71+
72+
// check number of fields
73+
if (serverChallengeData.length < 4) {
74+
var error = new Error('Unexpected number of fields [' + serverChallengeData.length + '] in server challenge (LDAP authentication)');
75+
error.code = 'EHDBAUTHPROTOCOL';
76+
cb(error);
77+
return;
78+
}
79+
80+
// check client nonce
81+
var clientNonceProof = serverChallengeData[0];
82+
if (!clientNonceProof.equals(this.clientNonce)) {
83+
var error = new Error('Client nonce does not match (LDAP authentication)');
84+
error.code = 'EHDBAUTHCLIENTNONCE';
85+
cb(error);
86+
return;
87+
}
88+
89+
// check capabilities
90+
var serverCapabilities = serverChallengeData[3];
91+
if (serverCapabilities.readInt8() != DEFAULT_CAPABILITIES) {
92+
var error = new Error('Unsupported capabilities (LDAP authentication)');
93+
error.code = 'EHDBAUTHCAPABILITIES';
94+
cb(error);
95+
return;
96+
}
97+
98+
// generate session key (for AES256 encryption of the password)
99+
if (!this.sessionKey) {
100+
this.sessionKey = crypto.randomBytes(SESSION_KEY_SIZE);
101+
}
102+
103+
// generate the encrypted session key
104+
var serverNonce = serverChallengeData[1];
105+
var serverPublicKey = serverChallengeData[2].toString('ascii'); // RSA public key (PKCS8 PEM)
106+
var sessionKeyContent = Buffer.concat([this.sessionKey, serverNonce]);
107+
var encryptedSessionKey = crypto.publicEncrypt({
108+
key: serverPublicKey,
109+
format: 'pem',
110+
type: 'spki'
111+
}, sessionKeyContent);
112+
113+
// encrypt the password
114+
var iv = serverNonce.slice(0, 16);
115+
var cipher = crypto.createCipheriv("aes-256-cbc", this.sessionKey, iv);
116+
var passwordContent = Buffer.concat([this.password, new Buffer(1), serverNonce]);
117+
var encryptedPassword = cipher.update(passwordContent);
118+
encryptedPassword = Buffer.concat([encryptedPassword, cipher.final()]);
119+
120+
// generate client proof
121+
this.clientProof = Fields.write(null, [encryptedSessionKey, encryptedPassword]).buffer;
122+
123+
// done
124+
cb();
125+
};
126+
127+
LDAP.prototype.finalData = function finalData() {
128+
return this.clientProof;
129+
};
130+
131+
LDAP.prototype.finalize = function finalize(buffer) {
132+
/* jshint unused:false */
133+
};

lib/protocol/auth/Manager.js

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var SCRAMSHA256 = require('./SCRAMSHA256');
1717
var SAML = require('./SAML');
1818
var JWT = require('./JWT');
1919
var SessionCookie = require('./SessionCookie');
20+
var LDAP = require('./LDAP');
2021

2122
module.exports = Manager;
2223

@@ -35,6 +36,7 @@ function Manager(options) {
3536
this._authMethods.push(new SessionCookie(options));
3637
}
3738
if (options.user && options.password) {
39+
this._authMethods.push(new LDAP(options));
3840
this._authMethods.push(new SCRAMSHA256(options, true)); // with PBKDF2
3941
this._authMethods.push(new SCRAMSHA256(options, false)); // no PBKDF2
4042
}

lib/protocol/common/Constants.js

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ module.exports = {
2626
MAX_PACKET_SIZE: Math.pow(2, 17),
2727
MAX_RESULT_SET_SIZE: Math.pow(2, 20),
2828
EMPTY_BUFFER: new Buffer(0),
29+
DATA_LENGTH_MAX1BYTE_LENGTH: 245,
30+
DATA_LENGTH_MAX2BYTE_LENGTH: 32767,
31+
DATA_LENGTH_2BYTE_LENGTH_INDICATOR: 246,
32+
DATA_LENGTH_4BYTE_LENGTH_INDICATOR: 247,
2933
DEFAULT_CONNECT_OPTIONS: [{
3034
name: ConnectOption.CLIENT_LOCALE,
3135
value: 'en_US',

lib/protocol/data/Fields.js

+37-8
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@
1313
// language governing permissions and limitations under the License.
1414
'use strict';
1515

16+
const common = require('../../protocol/common');
1617
var util = require('../../util');
1718

1819
exports.read = read;
1920
exports.write = write;
2021
exports.getByteLength = getByteLength;
2122
exports.getArgumentCount = getArgumentCount;
2223

24+
/**
25+
* Read sub-parameters from a buffer (fieldCount, field1Size, field1Data, field2Size, field2Data, ...)
26+
* @param {{buffer:Buffer}} part
27+
* @returns {Array<Buffer>} the sub-parameters
28+
*/
2329
function read(part) {
2430
var offset = 0;
2531
var buffer = part.buffer;
@@ -31,16 +37,32 @@ function read(part) {
3137
for (var i = 0; i < numberOfFields; i++) {
3238
fieldLength = buffer[offset];
3339
offset += 1;
34-
if (fieldLength > 245) {
35-
fieldLength = buffer.readUInt16LE(offset);
36-
offset += 2;
40+
if (fieldLength > common.DATA_LENGTH_MAX1BYTE_LENGTH) {
41+
if (fieldLength == common.DATA_LENGTH_2BYTE_LENGTH_INDICATOR) {
42+
fieldLength = buffer.readUInt16LE(offset);
43+
offset += 2;
44+
} else if (fieldLength == common.DATA_LENGTH_4BYTE_LENGTH_INDICATOR) {
45+
fieldLength = buffer.readUInt32LE(offset);
46+
offset += 4;
47+
} else if (fieldLength == 255) { // indicates that the following two bytes contain the length in big endian (bug?)
48+
fieldLength = buffer.readUInt16BE(offset);
49+
offset += 2;
50+
} else {
51+
throw Error("Unsupported length indicator: " + fieldLength);
52+
}
3753
}
3854
fields.push(buffer.slice(offset, offset + fieldLength));
3955
offset += fieldLength;
4056
}
4157
return fields;
4258
}
4359

60+
/**
61+
* Write sub-parameters to a buffer
62+
* @param {object?} part
63+
* @param {Array<Buffer|string>} fields
64+
* @returns {{argumentCount:number, buffer:Buffer}}
65+
*/
4466
function write(part, fields) {
4567
/* jshint validthis:true */
4668

@@ -65,14 +87,19 @@ function write(part, fields) {
6587
data = new Buffer(field, 'ascii');
6688
}
6789
fieldLength = data.length;
68-
if (fieldLength <= 245) {
90+
if (fieldLength <= common.DATA_LENGTH_MAX1BYTE_LENGTH) {
6991
buffer[offset] = fieldLength;
7092
offset += 1;
71-
} else {
72-
buffer[offset] = 0xf6;
93+
} else if (fieldLength <= common.DATA_LENGTH_MAX2BYTE_LENGTH) {
94+
buffer[offset] = common.DATA_LENGTH_2BYTE_LENGTH_INDICATOR;
7395
offset += 1;
7496
buffer.writeUInt16LE(fieldLength, offset);
7597
offset += 2;
98+
} else {
99+
buffer[offset] = common.DATA_LENGTH_4BYTE_LENGTH_INDICATOR;
100+
offset += 1;
101+
buffer.writeInt32LE(fieldLength, offset);
102+
offset += 4;
76103
}
77104
data.copy(buffer, offset);
78105
offset += fieldLength;
@@ -87,10 +114,12 @@ function getByteLength(fields) {
87114
var fieldLength;
88115
for (var i = 0; i < fields.length; i++) {
89116
fieldLength = getByteLengthOfField(fields[i]);
90-
if (fieldLength <= 245) {
117+
if (fieldLength <= common.DATA_LENGTH_MAX1BYTE_LENGTH) {
91118
byteLength += fieldLength + 1;
92-
} else {
119+
} else if (fieldLength <= common.DATA_LENGTH_MAX2BYTE_LENGTH) {
93120
byteLength += fieldLength + 3;
121+
} else {
122+
byteLength += fieldLength + 5;
94123
}
95124
}
96125
return byteLength;

lib/protocol/data/TextList.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
'use strict';
1515

1616
var util = require('../../util');
17+
var common = require('../common');
1718

1819
exports.write = write;
1920
exports.getByteLength = getByteLength;
@@ -34,11 +35,11 @@ function write(part, fields) {
3435
data = util.convert.encode(field, part.useCesu8);
3536

3637
fieldLength = data.length;
37-
if (fieldLength <= 245) {
38+
if (fieldLength <= common.DATA_LENGTH_MAX1BYTE_LENGTH) {
3839
buffer[offset] = fieldLength;
3940
offset += 1;
4041
} else {
41-
buffer[offset] = 0xf6;
42+
buffer[offset] = common.DATA_LENGTH_2BYTE_LENGTH_INDICATOR;
4243
offset += 1;
4344
buffer.writeUInt16LE(fieldLength, offset);
4445
offset += 2;
@@ -57,7 +58,7 @@ function getByteLength(fields, useCesu8) {
5758
var fieldLength;
5859
for (var i = 0; i < fields.length; i++) {
5960
fieldLength = getByteLengthOfField(fields[i], useCesu8);
60-
if (fieldLength <= 245) {
61+
if (fieldLength <= common.DATA_LENGTH_MAX1BYTE_LENGTH) {
6162
byteLength += fieldLength + 1;
6263
} else {
6364
byteLength += fieldLength + 3;

0 commit comments

Comments
 (0)