@homebridge/dbus-native
Version:
D-bus protocol implementation in native javascript
134 lines (122 loc) • 4.23 kB
JavaScript
const Buffer = require('safe-buffer').Buffer;
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const constants = require('./constants');
const readLine = require('./readline');
function sha1(input) {
var shasum = crypto.createHash('sha1');
shasum.update(input);
return shasum.digest('hex');
}
function getUserHome() {
return process.env[process.platform.match(/$win/) ? 'USERPROFILE' : 'HOME'];
}
function getCookie(context, id, cb) {
// http://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha
var dirname = path.join(getUserHome(), '.dbus-keyrings');
// > There is a default context, "org_freedesktop_general" that's used by servers that do not specify otherwise.
if (context.length === 0) context = 'org_freedesktop_general';
var filename = path.join(dirname, context);
// check it's not writable by others and readable by user
fs.stat(dirname, function (err, stat) {
if (err) return cb(err);
if (stat.mode & 0o22)
return cb(
new Error(
'User keyrings directory is writeable by other users. Aborting authentication'
)
);
// eslint-disable-next-line no-prototype-builtins
if (process.hasOwnProperty('getuid') && stat.uid !== process.getuid())
return cb(
new Error(
'Keyrings directory is not owned by the current user. Aborting authentication!'
)
);
fs.readFile(filename, 'ascii', function (err, keyrings) {
if (err) return cb(err);
var lines = keyrings.split('\n');
for (var l = 0; l < lines.length; ++l) {
var data = lines[l].split(' ');
if (id === data[0]) return cb(null, data[2]);
}
return cb(new Error('cookie not found'));
});
});
}
function hexlify(input) {
return Buffer.from(input.toString(), 'ascii').toString('hex');
}
module.exports = function auth(stream, opts, cb) {
// filter used to make a copy so we don't accidently change opts data
var authMethods;
if (opts.authMethods) {
authMethods = opts.authMethods;
} else {
authMethods = constants.defaultAuthMethods;
}
stream.write('\0');
tryAuth(stream, authMethods.slice(), cb);
};
function tryAuth(stream, methods, cb) {
if (methods.length === 0) {
return cb(new Error('No authentication methods left to try'));
}
var authMethod = methods.shift();
// eslint-disable-next-line no-prototype-builtins
var uid = process.hasOwnProperty('getuid') ? process.getuid() : 0;
var id = hexlify(uid);
function beginOrNextAuth() {
readLine(stream, function (line) {
var ok = line.toString('ascii').match(/^([A-Za-z]+) (.*)/);
if (ok && ok[1] === 'OK') {
stream.write('BEGIN\r\n');
return cb(null, ok[2]); // ok[2] = guid. Do we need it?
} else {
// TODO: parse error!
if (!methods.empty) {
tryAuth(stream, methods, cb);
} else {
return cb(line);
}
}
});
}
switch (authMethod) {
case 'EXTERNAL':
stream.write(`AUTH ${authMethod} ${id}\r\n`);
beginOrNextAuth();
break;
case 'DBUS_COOKIE_SHA1':
stream.write(`AUTH ${authMethod} ${id}\r\n`);
readLine(stream, function (line) {
var data = Buffer.from(line.toString().split(' ')[1].trim(), 'hex')
.toString()
.split(' ');
var cookieContext = data[0];
var cookieId = data[1];
var serverChallenge = data[2];
// any random 16 bytes should work, sha1(rnd) to make it simplier
var clientChallenge = crypto.randomBytes(16).toString('hex');
getCookie(cookieContext, cookieId, function (err, cookie) {
if (err) return cb(err);
var response = sha1(
[serverChallenge, clientChallenge, cookie].join(':')
);
var reply = hexlify(clientChallenge + response);
stream.write(`DATA ${reply}\r\n`);
beginOrNextAuth();
});
});
break;
case 'ANONYMOUS':
stream.write('AUTH ANONYMOUS \r\n');
beginOrNextAuth();
break;
default:
console.error(`Unsupported auth method: ${authMethod}`);
beginOrNextAuth();
break;
}
}