Skip to content

Commit a03c024

Browse files
committed
polyfill crypto lib
1 parent 0e08207 commit a03c024

File tree

11 files changed

+248
-55
lines changed

11 files changed

+248
-55
lines changed

lib/auth_41.js

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,7 @@
2424
server stores sha1(sha1(password)) ( hash_stag2)
2525
*/
2626

27-
const crypto = require('crypto');
28-
29-
function sha1(msg, msg1, msg2) {
30-
const hash = crypto.createHash('sha1');
31-
hash.update(msg);
32-
if (msg1) {
33-
hash.update(msg1);
34-
}
35-
36-
if (msg2) {
37-
hash.update(msg2);
38-
}
39-
40-
return hash.digest();
41-
}
27+
const crypto = require('./utils/crypto');
4228

4329
function xor(a, b) {
4430
const result = Buffer.allocUnsafe(a.length);
@@ -50,37 +36,37 @@ function xor(a, b) {
5036

5137
exports.xor = xor;
5238

53-
function token(password, scramble1, scramble2) {
39+
async function token(password, scramble1, scramble2) {
5440
if (!password) {
5541
return Buffer.alloc(0);
5642
}
57-
const stage1 = sha1(password);
58-
return exports.calculateTokenFromPasswordSha(stage1, scramble1, scramble2);
43+
const stage1 = await crypto.sha1(password);
44+
return await exports.calculateTokenFromPasswordSha(stage1, scramble1, scramble2);
5945
}
6046

61-
exports.calculateTokenFromPasswordSha = function(
47+
exports.calculateTokenFromPasswordSha = async function(
6248
passwordSha,
6349
scramble1,
6450
scramble2
6551
) {
6652
// we use AUTH 41 here, and we need only the bytes we just need.
6753
const authPluginData1 = scramble1.slice(0, 8);
6854
const authPluginData2 = scramble2.slice(0, 12);
69-
const stage2 = sha1(passwordSha);
70-
const stage3 = sha1(authPluginData1, authPluginData2, stage2);
55+
const stage2 = await crypto.sha1(passwordSha);
56+
const stage3 = await crypto.sha1(authPluginData1, authPluginData2, stage2);
7157
return xor(stage3, passwordSha);
7258
};
7359

7460
exports.calculateToken = token;
7561

76-
exports.verifyToken = function(publicSeed1, publicSeed2, token, doubleSha) {
77-
const hashStage1 = xor(token, sha1(publicSeed1, publicSeed2, doubleSha));
78-
const candidateHash2 = sha1(hashStage1);
62+
exports.verifyToken = async function(publicSeed1, publicSeed2, token, doubleSha) {
63+
const hashStage1 = xor(token, await crypto.sha1(publicSeed1, publicSeed2, doubleSha));
64+
const candidateHash2 = await crypto.sha1(hashStage1);
7965
return candidateHash2.compare(doubleSha) === 0;
8066
};
8167

82-
exports.doubleSha1 = function(password) {
83-
return sha1(sha1(password));
68+
exports.doubleSha1 = async function(password) {
69+
return await crypto.sha1(await crypto.sha1(password));
8470
};
8571

8672
function xorRotating(a, seed) {

lib/auth_plugins/mysql_native_password.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ module.exports = pluginOptions => ({ connection, command }) => {
1010
command.passwordSha1 ||
1111
pluginOptions.passwordSha1 ||
1212
connection.config.passwordSha1;
13-
return data => {
13+
return async data => {
1414
const authPluginData1 = data.slice(0, 8);
1515
const authPluginData2 = data.slice(8, 20);
1616
let authToken;
1717
if (passwordSha1) {
18-
authToken = auth41.calculateTokenFromPasswordSha(
18+
authToken = await auth41.calculateTokenFromPasswordSha(
1919
passwordSha1,
2020
authPluginData1,
2121
authPluginData2
2222
);
2323
} else {
24-
authToken = auth41.calculateToken(
24+
authToken = await auth41.calculateToken(
2525
password,
2626
authPluginData1,
2727
authPluginData2

lib/commands/change_user.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ class ChangeUser extends Command {
4646
connection.clientEncoding = CharsetToEncoding[this.charsetNumber];
4747
// clear prepared statements cache as all statements become invalid after changeUser
4848
connection._statements.clear();
49-
connection.writePacket(newPacket.toPacket());
49+
newPacket.toPacket().then(packet => {
50+
connection.writePacket(packet);
51+
}).catch(err => {
52+
this.onResult(err);
53+
});
5054
// check if the server supports multi-factor authentication
5155
const multiFactorAuthentication = connection.serverCapabilityFlags & ClientConstants.MULTI_FACTOR_AUTHENTICATION;
5256
if (multiFactorAuthentication) {

lib/commands/client_handshake.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ClientHandshake extends Command {
4646
connection.writePacket(sslRequest.toPacket());
4747
}
4848

49-
sendCredentials(connection) {
49+
async sendCredentials(connection) {
5050
if (connection.config.debug) {
5151
// eslint-disable-next-line
5252
console.log(
@@ -80,22 +80,22 @@ class ClientHandshake extends Command {
8080
compress: connection.config.compress,
8181
connectAttributes: connection.config.connectAttributes
8282
});
83-
connection.writePacket(handshakeResponse.toPacket());
83+
connection.writePacket(await handshakeResponse.toPacket());
8484
}
8585

86-
calculateNativePasswordAuthToken(authPluginData) {
86+
async calculateNativePasswordAuthToken(authPluginData) {
8787
// TODO: dont split into authPluginData1 and authPluginData2, instead join when 1 & 2 received
8888
const authPluginData1 = authPluginData.slice(0, 8);
8989
const authPluginData2 = authPluginData.slice(8, 20);
9090
let authToken;
9191
if (this.passwordSha1) {
92-
authToken = auth41.calculateTokenFromPasswordSha(
92+
authToken = await auth41.calculateTokenFromPasswordSha(
9393
this.passwordSha1,
9494
authPluginData1,
9595
authPluginData2
9696
);
9797
} else {
98-
authToken = auth41.calculateToken(
98+
authToken = await auth41.calculateToken(
9999
this.password,
100100
authPluginData1,
101101
authPluginData2
@@ -156,10 +156,14 @@ class ClientHandshake extends Command {
156156
return;
157157
}
158158
// rest of communication is encrypted
159-
this.sendCredentials(connection);
159+
this.sendCredentials(connection).catch(err => {
160+
this.emit('error', err);
161+
});
160162
});
161163
} else {
162-
this.sendCredentials(connection);
164+
this.sendCredentials(connection).catch(err => {
165+
this.emit('error', err);
166+
});
163167
}
164168
if (multiFactorAuthentication) {
165169
// if the server supports multi-factor authentication, we enable it in

lib/connection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Connection extends EventEmitter {
4343
// TODO: use `/usr/local/mysql/bin/mysql_config --socket` output? as default socketPath
4444
// if there is no host/port and no socketPath parameters?
4545
if (!opts.config.stream) {
46-
this.stream = getStream(opts.config.ssl);
46+
this.stream = getStream(!!opts.config.ssl);
4747
if (opts.config.socketPath) {
4848
// FIXME
4949
this.stream.connect(opts.config.socketPath);

lib/packets/change_user.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,33 @@ class ChangeUser {
1717
this.authPluginData1 = opts.authPluginData1;
1818
this.authPluginData2 = opts.authPluginData2;
1919
this.connectAttributes = opts.connectAttrinutes || {};
20+
this.authTokenReadly = this.initAuthToken();
21+
this.charsetNumber = opts.charsetNumber;
22+
}
23+
24+
async initAuthToken() {
2025
let authToken;
2126
if (this.passwordSha1) {
22-
authToken = auth41.calculateTokenFromPasswordSha(
27+
authToken = await auth41.calculateTokenFromPasswordSha(
2328
this.passwordSha1,
2429
this.authPluginData1,
2530
this.authPluginData2
2631
);
2732
} else {
28-
authToken = auth41.calculateToken(
33+
authToken = await auth41.calculateToken(
2934
this.password,
3035
this.authPluginData1,
3136
this.authPluginData2
3237
);
3338
}
3439
this.authToken = authToken;
35-
this.charsetNumber = opts.charsetNumber;
3640
}
3741

3842
// TODO
3943
// ChangeUser.fromPacket = function(packet)
4044
// };
41-
serializeToBuffer(buffer) {
45+
async serializeToBuffer(buffer) {
46+
await this.authTokenReadly;
4247
const isSet = flag => this.flags & ClientConstants[flag];
4348
const packet = new Packet(0, buffer, 0, buffer.length);
4449
packet.offset = 4;
@@ -81,16 +86,17 @@ class ChangeUser {
8186
return packet;
8287
}
8388

84-
toPacket() {
89+
async toPacket() {
90+
await this.authTokenReadly;
8591
if (typeof this.user !== 'string') {
8692
throw new Error('"user" connection config property must be a string');
8793
}
8894
if (typeof this.database !== 'string') {
8995
throw new Error('"database" connection config property must be a string');
9096
}
9197
// dry run: calculate resulting packet length
92-
const p = this.serializeToBuffer(Packet.MockBuffer());
93-
return this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
98+
const p = await this.serializeToBuffer(Packet.MockBuffer());
99+
return await this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
94100
}
95101
}
96102

lib/packets/handshake_response.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,34 @@ class HandshakeResponse {
1616
this.authPluginData2 = handshake.authPluginData2;
1717
this.compress = handshake.compress;
1818
this.clientFlags = handshake.flags;
19+
this.authTokenReady = this.initAuthToken();
20+
this.charsetNumber = handshake.charsetNumber;
21+
this.encoding = CharsetToEncoding[handshake.charsetNumber];
22+
this.connectAttributes = handshake.connectAttributes;
23+
}
24+
25+
async initAuthToken() {
1926
// TODO: pre-4.1 auth support
2027
let authToken;
2128
if (this.passwordSha1) {
22-
authToken = auth41.calculateTokenFromPasswordSha(
29+
authToken = await auth41.calculateTokenFromPasswordSha(
2330
this.passwordSha1,
2431
this.authPluginData1,
2532
this.authPluginData2
2633
);
2734
} else {
28-
authToken = auth41.calculateToken(
35+
authToken = await auth41.calculateToken(
2936
this.password,
3037
this.authPluginData1,
3138
this.authPluginData2
3239
);
3340
}
3441
this.authToken = authToken;
35-
this.charsetNumber = handshake.charsetNumber;
36-
this.encoding = CharsetToEncoding[handshake.charsetNumber];
37-
this.connectAttributes = handshake.connectAttributes;
3842
}
3943

40-
serializeResponse(buffer) {
44+
async serializeResponse(buffer) {
45+
await this.authTokenReady;
46+
4147
const isSet = flag => this.clientFlags & ClientConstants[flag];
4248
const packet = new Packet(0, buffer, 0, buffer.length);
4349
packet.offset = 4;
@@ -88,17 +94,19 @@ class HandshakeResponse {
8894
return packet;
8995
}
9096

91-
toPacket() {
97+
async toPacket() {
98+
await this.authTokenReady;
9299
if (typeof this.user !== 'string') {
93100
throw new Error('"user" connection config property must be a string');
94101
}
95102
if (typeof this.database !== 'string') {
96103
throw new Error('"database" connection config property must be a string');
97104
}
98105
// dry run: calculate resulting packet length
99-
const p = this.serializeResponse(Packet.MockBuffer());
100-
return this.serializeResponse(Buffer.alloc(p.offset));
106+
const p = await this.serializeResponse(Packet.MockBuffer());
107+
return await this.serializeResponse(Buffer.alloc(p.offset));
101108
}
109+
102110
static fromPacket(packet) {
103111
const args = {};
104112
args.clientFlags = packet.readInt32();

lib/stream.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
module.exports.getStream = function getStream(ssl = false) {
88
const net = require('net')
99
if (typeof net.Socket === 'function') {
10-
return net.Socket
10+
console.log('using node socket');
11+
return net.Socket()
1112
}
1213
const { CloudflareSocket } = require('pg-cloudflare')
14+
console.log('using cloudflare socket');
1315
return new CloudflareSocket(ssl);
1416
}

lib/utils/crypto.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict'
2+
3+
const useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split('.')[0]) < 15
4+
if (useLegacyCrypto) {
5+
// We are on an old version of Node.js that requires legacy crypto utilities.
6+
module.exports = require('./nodecrypto')
7+
} else {
8+
module.exports = require('./webcrypto');
9+
}

lib/utils/nodecrypto.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use strict'
2+
// This file contains crypto utility functions for versions of Node.js < 15.0.0,
3+
// which does not support the WebCrypto.subtle API.
4+
5+
const nodeCrypto = require('crypto')
6+
7+
function md5(string) {
8+
return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex')
9+
}
10+
11+
// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html
12+
function postgresMd5PasswordHash(user, password, salt) {
13+
const inner = md5(password + user);
14+
const outer = md5(Buffer.concat([Buffer.from(inner), salt]));
15+
return `md5${outer}`
16+
}
17+
18+
function sha256(text) {
19+
return nodeCrypto.createHash('sha256').update(text).digest()
20+
}
21+
22+
async function sha1(msg,msg1,msg2) {
23+
const hash = nodeCrypto.createHash('sha1');
24+
hash.update(msg);
25+
if (msg1) {
26+
hash.update(msg1);
27+
}
28+
if (msg2) {
29+
hash.update(msg2);
30+
}
31+
return hash.digest();
32+
}
33+
34+
function xorRotating(a, seed) {
35+
const result = Buffer.allocUnsafe(a.length);
36+
const seedLen = seed.length;
37+
38+
for (let i = 0; i < a.length; i++) {
39+
result[i] = a[i] ^ seed[i % seedLen];
40+
}
41+
return result;
42+
}
43+
44+
function encrypt(password, scramble, key) {
45+
const stage1 = xorRotating(
46+
Buffer.from(`${password}\0`, 'utf8'),
47+
scramble
48+
);
49+
return nodeCrypto.publicEncrypt(key, stage1);
50+
}
51+
52+
function hmacSha256(key, msg) {
53+
return nodeCrypto.createHmac('sha256', key).update(msg).digest()
54+
}
55+
56+
function deriveKey(password, salt, iterations) {
57+
return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256')
58+
}
59+
60+
module.exports = {
61+
postgresMd5PasswordHash,
62+
randomBytes: nodeCrypto.randomBytes,
63+
deriveKey,
64+
sha256,
65+
hmacSha256,
66+
md5,
67+
sha1,
68+
encrypt,
69+
}

0 commit comments

Comments
 (0)