-
Notifications
You must be signed in to change notification settings - Fork 10
/
ep_hash_auth.js
170 lines (155 loc) · 6.45 KB
/
ep_hash_auth.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
161
162
163
164
165
166
167
168
169
170
'use strict';
// 2.x hash based authentication for etherpad
// 2014-2016 - István Király - [email protected]
// Contributions by Robin Schneider <[email protected]>
// Contributions by id01 <https://github.com/id01>
// Made on codepad :P
const fs = require('fs');
const settings = require('ep_etherpad-lite/node/utils/Settings');
const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
const crypto = require('crypto');
// npm install bcrypt/scrypt/argon2 (optional but recommended)
const optionalRequire = (library, name, npmLibrary) => {
try {
return require(library);
} catch (e) {
console.log(`Note: ${library} library could not be found. ${name} support will be disabled.`);
if (npmLibrary) {
console.log(`Run "npm install ${npmLibrary}" to enable ${name}`);
}
}
};
const bcrypt = optionalRequire('bcrypt', 'bcrypt', 'bcrypt');
const scrypt = optionalRequire('scrypt', 'scrypt', 'scrypt');
const argon2 = optionalRequire('argon2', 'argon2', 'argon2');
// ocrypt-relevant options
let hash_typ = 'sha512';
let hash_dig = 'hex';
// default dir to search for hash files
let hash_dir = '/var/etherpad/users';
// by default the extension is actually a file, so usernames are actually folders
let hash_ext = '/.hash';
// by default peple logged in that authenticated over a hash file, are admins?
let hash_adm = false;
// default filename containing the displayname of a user
let displayname_ext = '/.displayname';
if (settings.ep_hash_auth) {
if (settings.ep_hash_auth.hash_typ) hash_typ = settings.ep_hash_auth.hash_typ;
if (settings.ep_hash_auth.hash_dig) hash_dig = settings.ep_hash_auth.hash_dig;
if (settings.ep_hash_auth.hash_dir) hash_dir = settings.ep_hash_auth.hash_dir;
if (settings.ep_hash_auth.hash_ext) hash_ext = settings.ep_hash_auth.hash_ext;
if (settings.ep_hash_auth.hash_adm) hash_adm = settings.ep_hash_auth.hash_adm;
if (settings.ep_hash_auth.displayname_ext) {
displayname_ext = settings.ep_hash_auth.displayname_ext;
}
}
// Let's make a function to compare our hashes now that we have multiple comparisons required.
// This function calls callback(hashType) if authenticated, or callback(null) if not.
const compareHashes = async (password, hash, callback) => {
const cryptoHash = crypto.createHash(hash_typ).update(password).digest(hash_dig);
if (hash === cryptoHash) { // Check whether this is a crypto hash first
return callback('crypto');
// If not, check other hash types
} else if (hash[0] === '$') {
// This is an argon2 or bcrypt hash
if (hash.slice(0, 7) === '$argon2') {
// This is argon2
if (argon2) {
if (await argon2.verify(hash, password)) {
return callback('argon2');
} else {
return callback(null);
}
} else {
console.log('Warning: Could not verify argon2 hash due to missing dependency');
}
} else if (bcrypt) {
if (await bcrypt.compare(password, hash)) {
return callback('bcrypt');
} else {
return callback(null);
}
} else {
console.log('Warning: Could not verify bcrypt hash due to missing dependency');
}
} else if (scrypt) {
// This is a scrypt hash or a failed crypto hash
if (scrypt.verifyKdfSync(Buffer.from(hash, 'hex'), Buffer.from(password))) {
return callback('scrypt');
} else {
return callback(null);
}
} else {
console.log('Warning: Could not verify scrypt hash due to missing dependency');
}
return callback(null);
};
exports.authenticate = (hook_name, context, cb) => {
if (context.req.headers.authorization &&
context.req.headers.authorization.search('Basic ') === 0) {
const userpass = Buffer.from(
context.req.headers.authorization.split(' ')[1], 'base64').toString().split(':');
const username = userpass.shift();
const password = userpass.join(':');
// Authenticate user via settings.json
if (settings.users[username] !== undefined && settings.users[username].hash !== undefined) {
compareHashes(password, settings.users[username].hash, (hashType) => {
if (hashType) {
console.log(`Log: Authenticated (${hashType}) ${username}`);
settings.users[username].username = username;
context.req.session.user = settings.users[username];
// use displayname if available
if (settings.users[username].displayname !== undefined) {
context.req.session.user.displayname = settings.users[username].displayname;
} else {
console.log(`Log: displayname not found for user ${username}`);
}
return cb([true]);
} else { return cb([false]); }
});
} else {
// Authenticate user via hash_dir
const path = `${hash_dir}/${username}${hash_ext}`;
fs.readFile(path, 'utf8', (err, contents) => {
if (err) {
// file not found, or inaccessible
console.log(
`Error: Failed authentication attempt for ${username}: no authentication found`);
return cb([false]);
} else {
compareHashes(password, contents, (hashType) => {
if (hashType) {
console.log(`Log: Authenticated (${hashType}-file) ${username}`);
// read displayname if available
const displaynamepath = `${hash_dir}/${username}${displayname_ext}`;
fs.readFile(displaynamepath, 'utf8', (err, contents) => {
let displayname;
if (err) {
console.log(`Log: Could not load displayname for ${username}`);
} else {
displayname = contents;
}
settings.users[username] = {username, is_admin: hash_adm, displayname};
context.req.session.user = settings.users[username];
return cb([true]);
});
} else { return cb([false]); }
});
}
});
}
} else { return cb([false]); }
};
exports.handleMessage = (hook_name, context, cb) => {
// skip if we don't have any information to set
const session = context.client.client.request.session;
if (!session || !session.user || !session.user.displayname) return cb();
authorManager.getAuthor4Token(context.message.token).then((author) => {
authorManager.setAuthorName(author, context.client.client.request.session.user.displayname);
cb();
}).catch((error) => {
console.error(
'handleMessage: could not get authorid for token %s', context.message.token, error);
cb();
});
};