forked from JellyBrick/node-dbus-next
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandshake.js
160 lines (149 loc) Β· 4.65 KB
/
handshake.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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) {
const 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
const 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';
const 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'
)
);
}
if ('getuid' in process && 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);
const lines = keyrings.split('\n');
for (let l = 0; l < lines.length; ++l) {
const 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
let 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'));
}
const authMethod = methods.shift();
const uid = 'getuid' in process ? process.getuid() : 0;
const id = hexlify(uid);
let guid = '';
function beginOrNextAuth () {
readLine(stream, function (line) {
const ok = line.toString('ascii').match(/^([A-Za-z]+) (.*)/);
if (ok && ok[1] === 'OK') {
guid = ok[2]; // ok[2] = guid. Do we need it?
if (stream.supportsUnixFd) {
negotiateUnixFd();
} else {
stream.write('BEGIN\r\n');
return cb(null, guid);
}
} else {
// TODO: parse error!
if (!methods.empty) {
tryAuth(stream, methods, cb);
} else {
return cb(line);
}
}
});
}
function negotiateUnixFd () {
stream.write('NEGOTIATE_UNIX_FD\r\n');
readLine(stream, function (line) {
const res = line.toString('ascii').trim();
if (res === 'AGREE_UNIX_FD') {
// ok
} else if (res === 'ERROR') {
stream.supportsUnixFd = false;
} else {
return cb(line);
}
stream.write('BEGIN\r\n');
return cb(null, guid);
});
}
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) {
const data = Buffer.from(
line
.toString()
.split(' ')[1]
.trim(),
'hex'
)
.toString()
.split(' ');
const cookieContext = data[0];
const cookieId = data[1];
const serverChallenge = data[2];
// any random 16 bytes should work, sha1(rnd) to make it simplier
const clientChallenge = crypto.randomBytes(16).toString('hex');
getCookie(cookieContext, cookieId, function (err, cookie) {
if (err) return cb(err);
const response = sha1(
[serverChallenge, clientChallenge, cookie].join(':')
);
const 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;
}
}