Skip to content

Commit 042e695

Browse files
committed
feat!: add support for binary passwords (as Buffers)
1 parent 12aceee commit 042e695

File tree

5 files changed

+99
-19
lines changed

5 files changed

+99
-19
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ The callback function is called with the following three arguments:
307307
* `handshake` {Object} - A handshake object
308308
* `protocols` {Set} - A set of subprotocols purportedly supported by the client.
309309
* `identity` {String} - The identity portion of the connection URL, decoded.
310-
* `password` {String} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Read [Security Profile 1](#security-profile-1) for more details of how this works.
310+
* `password` {Buffer} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Typically this password would be a string, but the OCPP specs allow for this to be binary, so it is provided as a `Buffer` for you to interpret as you wish. Read [Security Profile 1](#security-profile-1) for more details of how this works.
311311
* `endpoint` {String} - The endpoint path portion of the connection URL. This is the part of the path before the identity.
312312
* `query` {URLSearchParams} - The query string parsed as [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).
313313
* `remoteAddress` {String} - The remote IP address of the socket.
@@ -384,7 +384,7 @@ Returns a `Promise` which resolves when the server has completed closing.
384384
- `endpoint` {String} - The RPC server's endpoint (a websocket URL). **Required**.
385385
- `identity` {String} - The RPC client's identity. Will be automatically encoded. **Required**.
386386
- `protocols` {Array<String>} - Array of subprotocols supported by this client. Defaults to `[]`.
387-
- `password` {String} - Optional password to use in [HTTP Basic auth](#security-profile-1). (The username will always be the identity).
387+
- `password` {String|Buffer} - Optional password to use in [HTTP Basic auth](#security-profile-1). This can be a Buffer to allow for binary auth keys as recommended in the OCPP security whitepaper. If provided as a string, it will be encoded as UTF-8. (The corresponding username will always be the identity).
388388
- `headers` {Object} - Additional HTTP headers to send along with the websocket upgrade request. Defaults to `{}`.
389389
- `query` {Object|String} - An optional query string or object to append as the query string of the connection URL. Defaults to `''`.
390390
- `callTimeoutMs` {Number} - Milliseconds to wait before unanswered outbound calls are rejected automatically. Defaults to `60000`.
@@ -675,7 +675,7 @@ The RPCServerClient is a subclass of RPCClient. This represents an RPCClient fro
675675
* {Object}
676676
* `protocols` {Set} - A set of subprotocols purportedly supported by the client.
677677
* `identity` {String} - The identity portion of the connection URL, decoded.
678-
* `password` {String} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Read [Security Profile 1](#security-profile-1) for more details of how this works.
678+
* `password` {Buffer} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Typically this password would be a string, but the OCPP specs allow for this to be binary, so it is provided as a `Buffer` for you to interpret as you wish. Read [Security Profile 1](#security-profile-1) for more details of how this works.
679679
* `endpoint` {String} - The endpoint path portion of the connection URL. This is the part of the path before the identity.
680680
* `query` {URLSearchParams} - The query string parsed as [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).
681681
* `remoteAddress` {String} - The remote IP address of the socket.
@@ -855,7 +855,7 @@ const cli = new RPCClient({
855855

856856
const server = new RPCServer();
857857
server.auth((accept, reject, handshake) => {
858-
if (handshake.identity === "AzureDiamond" && handshake.password === "hunter2") {
858+
if (handshake.identity === "AzureDiamond" && handshake.password.toString('utf8') === "hunter2") {
859859
accept();
860860
} else {
861861
reject(401);
@@ -876,6 +876,8 @@ In practice, it's not uncommon to see violations of RFC7617 in the wild. All maj
876876

877877
However, in OCPP, since we have the luxury of knowing that the username must always be equal to the client's identity, it is no longer necessary to rely upon a colon to delineate the username from the password. This module makes use of this guarantee to enable identities and passwords to contain as many or as few colons as you wish.
878878

879+
Additionally, the OCPP security whitepaper recommends passwords consist purely of random bytes (for maximum entropy), although this violates the Basic Auth RFC which requires all passwords to be TEXT (US-ASCII compatible with no control characters). For this reason, this library will not make any presumptions about the character encoding (or otherwise) of the password provided, and present the password as a `Buffer`.
880+
879881
```js
880882
const { RPCClient, RPCServer } = require('ocpp-rpc');
881883

@@ -886,8 +888,8 @@ const cli = new RPCClient({
886888

887889
const server = new RPCServer();
888890
server.auth((accept, reject, handshake) => {
889-
console.log(handshake.identity); // "this:is:ok"
890-
console.log(handshake.password); // "as:is:this"
891+
console.log(handshake.identity); // "this:is:ok"
892+
console.log(handshake.password.toString('utf8')); // "as:is:this"
891893
accept();
892894
});
893895

@@ -910,8 +912,8 @@ const server = new RPCServer();
910912
server.auth((accept, reject, handshake) => {
911913
const cred = auth.parse(handshake.headers.authorization);
912914

913-
console.log(cred.name); // "this"
914-
console.log(cred.pass); // "is:broken:as:is:this"
915+
console.log(cred.name); // "this"
916+
console.log(cred.pass.toString('utf8')); // "is:broken:as:is:this"
915917
accept();
916918
});
917919

lib/client.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,13 @@ class RPCClient extends EventEmitter {
492492
Object.assign(wsOpts.headers, this._options.headers);
493493

494494
if (this._options.password != null) {
495-
const userPass = [this._identity, this._options.password].join(':');
496-
const b64 = Buffer.from(userPass).toString('base64');
495+
const usernameBuffer = Buffer.from(this._identity + ':');
496+
let passwordBuffer = this._options.password;
497+
if (typeof passwordBuffer === 'string') {
498+
passwordBuffer = Buffer.from(passwordBuffer, 'utf8');
499+
}
500+
501+
const b64 = Buffer.concat([usernameBuffer, passwordBuffer]).toString('base64');
497502
wsOpts.headers.authorization = 'Basic ' + b64;
498503
}
499504

lib/server.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,18 @@ class RPCServer extends EventEmitter {
144144
* However, this shouldn't cause any confusion as we have a
145145
* guarantee from OCPP that the username will always be equal to
146146
* the identity.
147+
* It also supports binary passwords, which is also a spec violation
148+
* but is necessary for allowing truly random binary keys as
149+
* recommended by the OCPP security whitepaper.
147150
*/
148151
const b64up = headers.authorization.match(/^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/)[1];
149-
const userPass = Buffer.from(b64up, 'base64').toString('utf8');
150-
if (userPass.substring(0, identity.length + 1) === (identity + ':')) {
151-
// identity matches!
152-
password = userPass.substring(identity.length + 1);
152+
const userPassBuffer = Buffer.from(b64up, 'base64');
153+
154+
const clientIdentityUserBuffer = Buffer.from(identity + ':');
155+
156+
if (clientIdentityUserBuffer.compare(userPassBuffer, 0, clientIdentityUserBuffer.length) === 0) {
157+
// first part of buffer matches `${identity}:`
158+
password = userPassBuffer.subarray(clientIdentityUserBuffer.length);
153159
}
154160
} catch (err) {
155161
// failing to parse authorization header is no big deal.

test/client.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ describe('RPCClient', function(){
835835
}
836836
});
837837

838-
it('should authenticate with password', async () => {
838+
it('should authenticate with string passwords', async () => {
839839

840840
const password = 'hunter2';
841841
let recPass;
@@ -854,7 +854,39 @@ describe('RPCClient', function(){
854854

855855
try {
856856
await cli.connect();
857-
assert.equal(password, recPass);
857+
assert.equal(password, recPass.toString('utf8'));
858+
859+
} finally {
860+
cli.close();
861+
close();
862+
}
863+
});
864+
865+
it('should authenticate with binary passwords', async () => {
866+
867+
const password = Buffer.from([
868+
0,1,2,3,4,5,6,7,8,9,
869+
65,66,67,68,69,
870+
251,252,253,254,255,
871+
]);
872+
let recPass;
873+
874+
const {endpoint, close, server} = await createServer();
875+
server.auth((accept, reject, handshake) => {
876+
recPass = handshake.password;
877+
accept();
878+
});
879+
880+
const cli = new RPCClient({
881+
endpoint,
882+
identity: 'X',
883+
password,
884+
});
885+
886+
try {
887+
await cli.connect();
888+
// console.log(Buffer.from(recPass, 'ascii'));
889+
assert.equal(password.toString('hex'), recPass.toString('hex'));
858890

859891
} finally {
860892
cli.close();

test/server.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ describe('RPCServer', function(){
427427
}});
428428

429429
server.auth((accept, reject, handshake) => {
430-
accept({pwd: handshake.password});
430+
accept({pwd: handshake.password.toString('utf8')});
431431
});
432432

433433
const cli = new RPCClient({
@@ -470,7 +470,7 @@ describe('RPCServer', function(){
470470

471471
try {
472472
await cli.connect();
473-
assert.equal(password, recPass);
473+
assert.equal(password, recPass.toString('utf8'));
474474
assert.equal(identity, recIdent);
475475

476476
} finally {
@@ -498,7 +498,7 @@ describe('RPCServer', function(){
498498

499499
try {
500500
await cli.connect();
501-
assert.equal(password, recPass);
501+
assert.equal(password, recPass.toString('utf8'));
502502

503503
} finally {
504504
cli.close();
@@ -586,6 +586,41 @@ describe('RPCServer', function(){
586586
close();
587587
}
588588
});
589+
590+
it('should recognise binary passwords', async () => {
591+
592+
const password = Buffer.from([
593+
0,1,2,3,4,5,6,7,8,9,
594+
65,66,67,68,69,
595+
251,252,253,254,255,
596+
]);
597+
598+
const {endpoint, close, server} = await createServer({}, {withClient: cli => {
599+
cli.handle('GetPassword', () => {
600+
return cli.session.pwd;
601+
});
602+
}});
603+
604+
server.auth((accept, reject, handshake) => {
605+
accept({pwd: handshake.password.toString('hex')});
606+
});
607+
608+
const cli = new RPCClient({
609+
endpoint,
610+
identity: 'X',
611+
password,
612+
});
613+
614+
try {
615+
await cli.connect();
616+
const pass = await cli.call('GetPassword');
617+
assert.equal(password.toString('hex'), pass);
618+
619+
} finally {
620+
cli.close();
621+
close();
622+
}
623+
});
589624

590625
});
591626

0 commit comments

Comments
 (0)