Skip to content

Commit 5532ca5

Browse files
Use WebCrypto APIs where possible
The only place we are stuck with node's original crypto API is for generating md5 hashes, which are not supported by WebCrypto.
1 parent 2b469d0 commit 5532ca5

File tree

8 files changed

+208
-101
lines changed

8 files changed

+208
-101
lines changed

packages/pg/lib/client.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
var EventEmitter = require('events').EventEmitter
44
var utils = require('./utils')
5-
var sasl = require('./sasl')
5+
var sasl = require('./crypto/sasl')
66
var TypeOverrides = require('./type-overrides')
77

88
var ConnectionParameters = require('./connection-parameters')
99
var Query = require('./query')
1010
var defaults = require('./defaults')
1111
var Connection = require('./connection')
12+
const crypto = require('./crypto/utils')
1213

1314
class Client extends EventEmitter {
1415
constructor(config) {
@@ -245,9 +246,13 @@ class Client extends EventEmitter {
245246
}
246247

247248
_handleAuthMD5Password(msg) {
248-
this._checkPgPass(() => {
249-
const hashedPassword = utils.postgresMd5PasswordHash(this.user, this.password, msg.salt)
250-
this.connection.password(hashedPassword)
249+
this._checkPgPass(async () => {
250+
try {
251+
const hashedPassword = await crypto.postgresMd5PasswordHash(this.user, this.password, msg.salt)
252+
this.connection.password(hashedPassword)
253+
} catch (e) {
254+
this.emit('error', e)
255+
}
251256
})
252257
}
253258

@@ -262,9 +267,9 @@ class Client extends EventEmitter {
262267
})
263268
}
264269

265-
_handleAuthSASLContinue(msg) {
270+
async _handleAuthSASLContinue(msg) {
266271
try {
267-
sasl.continueSession(this.saslSession, this.password, msg.data)
272+
await sasl.continueSession(this.saslSession, this.password, msg.data)
268273
this.connection.sendSCRAMClientFinalMessage(this.saslSession.response)
269274
} catch (err) {
270275
this.connection.emit('error', err)

packages/pg/lib/sasl.js renamed to packages/pg/lib/crypto/sasl.js

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict'
2-
const crypto = require('crypto')
2+
const crypto = require('./utils')
33

44
function startSession(mechanisms) {
55
if (mechanisms.indexOf('SCRAM-SHA-256') === -1) {
@@ -16,7 +16,7 @@ function startSession(mechanisms) {
1616
}
1717
}
1818

19-
function continueSession(session, password, serverData) {
19+
async function continueSession(session, password, serverData) {
2020
if (session.message !== 'SASLInitialResponse') {
2121
throw new Error('SASL: Last message was not SASLInitialResponse')
2222
}
@@ -38,29 +38,22 @@ function continueSession(session, password, serverData) {
3838
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short')
3939
}
4040

41-
var saltBytes = Buffer.from(sv.salt, 'base64')
42-
43-
var saltedPassword = crypto.pbkdf2Sync(password, saltBytes, sv.iteration, 32, 'sha256')
44-
45-
var clientKey = hmacSha256(saltedPassword, 'Client Key')
46-
var storedKey = sha256(clientKey)
47-
4841
var clientFirstMessageBare = 'n=*,r=' + session.clientNonce
4942
var serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration
50-
5143
var clientFinalMessageWithoutProof = 'c=biws,r=' + sv.nonce
52-
5344
var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
5445

55-
var clientSignature = hmacSha256(storedKey, authMessage)
56-
var clientProofBytes = xorBuffers(clientKey, clientSignature)
57-
var clientProof = clientProofBytes.toString('base64')
58-
59-
var serverKey = hmacSha256(saltedPassword, 'Server Key')
60-
var serverSignatureBytes = hmacSha256(serverKey, authMessage)
46+
var saltBytes = Buffer.from(sv.salt, 'base64')
47+
var saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration)
48+
var clientKey = await crypto.hmacSha256(saltedPassword, 'Client Key')
49+
var storedKey = await crypto.sha256(clientKey)
50+
var clientSignature = await crypto.hmacSha256(storedKey, authMessage)
51+
var clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString('base64')
52+
var serverKey = await crypto.hmacSha256(saltedPassword, 'Server Key')
53+
var serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage)
6154

6255
session.message = 'SASLResponse'
63-
session.serverSignature = serverSignatureBytes.toString('base64')
56+
session.serverSignature = Buffer.from(serverSignatureBytes).toString('base64')
6457
session.response = clientFinalMessageWithoutProof + ',p=' + clientProof
6558
}
6659

@@ -186,14 +179,6 @@ function xorBuffers(a, b) {
186179
return Buffer.from(a.map((_, i) => a[i] ^ b[i]))
187180
}
188181

189-
function sha256(text) {
190-
return crypto.createHash('sha256').update(text).digest()
191-
}
192-
193-
function hmacSha256(key, msg) {
194-
return crypto.createHmac('sha256', key).update(msg).digest()
195-
}
196-
197182
module.exports = {
198183
startSession,
199184
continueSession,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
var inner = md5(password + user)
14+
var 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+
function hmacSha256(key, msg) {
23+
return nodeCrypto.createHmac('sha256', key).update(msg).digest()
24+
}
25+
26+
async function deriveKey(password, salt, iterations) {
27+
return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256')
28+
}
29+
30+
module.exports = {
31+
postgresMd5PasswordHash,
32+
randomBytes: nodeCrypto.randomBytes,
33+
deriveKey,
34+
sha256,
35+
hmacSha256,
36+
md5,
37+
}

packages/pg/lib/crypto/utils.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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('./utils-legacy')
7+
return
8+
}
9+
10+
const nodeCrypto = require('crypto')
11+
12+
module.exports = {
13+
postgresMd5PasswordHash,
14+
randomBytes,
15+
deriveKey,
16+
sha256,
17+
hmacSha256,
18+
md5,
19+
}
20+
21+
/**
22+
* The Web Crypto API - grabbed from the Node.js library or the global
23+
* @type Crypto
24+
*/
25+
const webCrypto = nodeCrypto.webcrypto || globalThis.crypto
26+
/**
27+
* The SubtleCrypto API for low level crypto operations.
28+
* @type SubtleCrypto
29+
*/
30+
const subtleCrypto = webCrypto.subtle
31+
const textEncoder = new TextEncoder()
32+
33+
/**
34+
*
35+
* @param {*} length
36+
* @returns
37+
*/
38+
function randomBytes(length) {
39+
return webCrypto.getRandomValues(Buffer.alloc(length))
40+
}
41+
42+
async function md5(string) {
43+
try {
44+
return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex')
45+
} catch (e) {
46+
// `createHash()` failed so we are probably not in Node.js, use the WebCrypto API instead.
47+
// Note that the MD5 algorithm on WebCrypto is not available in Node.js.
48+
// This is why we cannot just use WebCrypto in all environments.
49+
const data = typeof string === 'string' ? textEncoder.encode(string) : string
50+
const hash = await subtleCrypto.digest('MD5', data)
51+
return Array.from(new Uint8Array(hash))
52+
.map((b) => b.toString(16).padStart(2, '0'))
53+
.join('')
54+
}
55+
}
56+
57+
// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html
58+
async function postgresMd5PasswordHash(user, password, salt) {
59+
var inner = await md5(password + user)
60+
var outer = await md5(Buffer.concat([Buffer.from(inner), salt]))
61+
return 'md5' + outer
62+
}
63+
64+
/**
65+
* Create a SHA-256 digest of the given data
66+
* @param {Buffer} data
67+
*/
68+
async function sha256(text) {
69+
return await subtleCrypto.digest('SHA-256', text)
70+
}
71+
72+
/**
73+
* Sign the message with the given key
74+
* @param {ArrayBuffer} keyBuffer
75+
* @param {string} msg
76+
*/
77+
async function hmacSha256(keyBuffer, msg) {
78+
const key = await subtleCrypto.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
79+
return await subtleCrypto.sign('HMAC', key, textEncoder.encode(msg))
80+
}
81+
82+
/**
83+
* Derive a key from the password and salt
84+
* @param {string} password
85+
* @param {Uint8Array} salt
86+
* @param {number} iterations
87+
*/
88+
async function deriveKey(password, salt, iterations) {
89+
const key = await subtleCrypto.importKey('raw', textEncoder.encode(password), 'PBKDF2', false, ['deriveBits'])
90+
const params = { name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations: iterations }
91+
return await subtleCrypto.deriveBits(params, key, 32 * 8, ['deriveBits'])
92+
}

packages/pg/lib/utils.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
'use strict'
22

3-
const crypto = require('crypto')
4-
53
const defaults = require('./defaults')
64

75
function escapeElement(elementRepresentation) {
@@ -164,17 +162,6 @@ function normalizeQueryConfig(config, values, callback) {
164162
return config
165163
}
166164

167-
const md5 = function (string) {
168-
return crypto.createHash('md5').update(string, 'utf-8').digest('hex')
169-
}
170-
171-
// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html
172-
const postgresMd5PasswordHash = function (user, password, salt) {
173-
var inner = md5(password + user)
174-
var outer = md5(Buffer.concat([Buffer.from(inner), salt]))
175-
return 'md5' + outer
176-
}
177-
178165
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
179166
const escapeIdentifier = function (str) {
180167
return '"' + str.replace(/"/g, '""') + '"'
@@ -205,17 +192,13 @@ const escapeLiteral = function (str) {
205192
return escaped
206193
}
207194

208-
209-
210195
module.exports = {
211196
prepareValue: function prepareValueWrapper(value) {
212197
// this ensures that extra arguments do not get passed into prepareValue
213198
// by accident, eg: from calling values.map(utils.prepareValue)
214199
return prepareValue(value)
215200
},
216201
normalizeQueryConfig,
217-
postgresMd5PasswordHash,
218-
md5,
219202
escapeIdentifier,
220-
escapeLiteral
203+
escapeLiteral,
221204
}

packages/pg/test/integration/connection/test-helper.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var net = require('net')
33
var helper = require('../test-helper')
44
var Connection = require('../../../lib/connection')
55
var utils = require('../../../lib/utils')
6+
const crypto = require('../../../lib/crypto/utils')
67
var connect = function (callback) {
78
var username = helper.args.user
89
var database = helper.args.database
@@ -20,8 +21,8 @@ var connect = function (callback) {
2021
con.once('authenticationCleartextPassword', function () {
2122
con.password(helper.args.password)
2223
})
23-
con.once('authenticationMD5Password', function (msg) {
24-
con.password(utils.postgresMd5PasswordHash(helper.args.user, helper.args.password, msg.salt))
24+
con.once('authenticationMD5Password', async function (msg) {
25+
con.password(await crypto.postgresMd5PasswordHash(helper.args.user, helper.args.password, msg.salt))
2526
})
2627
con.once('readyForQuery', function () {
2728
con.query('create temp table ids(id integer)')
Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
'use strict'
22
var helper = require('./test-helper')
33
const BufferList = require('../../buffer-list')
4-
var utils = require('../../../lib/utils')
4+
var crypto = require('../../../lib/crypto/utils')
55

6-
test('md5 authentication', function () {
6+
test('md5 authentication', async function () {
77
var client = helper.createClient()
88
client.password = '!'
99
var salt = Buffer.from([1, 2, 3, 4])
10-
client.connection.emit('authenticationMD5Password', { salt: salt })
10+
await client.connection.emit('authenticationMD5Password', { salt: salt })
1111

12-
test('responds', function () {
13-
assert.lengthIs(client.connection.stream.packets, 1)
14-
test('should have correct encrypted data', function () {
15-
var password = utils.postgresMd5PasswordHash(client.user, client.password, salt)
16-
// how do we want to test this?
17-
assert.equalBuffers(client.connection.stream.packets[0], new BufferList().addCString(password).join(true, 'p'))
12+
setTimeout(() =>
13+
test('responds', function () {
14+
assert.lengthIs(client.connection.stream.packets, 1)
15+
test('should have correct encrypted data', async function () {
16+
var password = await crypto.postgresMd5PasswordHash(client.user, client.password, salt)
17+
// how do we want to test this?
18+
assert.equalBuffers(client.connection.stream.packets[0], new BufferList().addCString(password).join(true, 'p'))
19+
})
1820
})
19-
})
21+
)
2022
})
2123

22-
test('md5 of utf-8 strings', function () {
23-
assert.equal(utils.md5('😊'), '5deda34cd95f304948d2bc1b4a62c11e')
24+
test('md5 of utf-8 strings', async function () {
25+
assert.equal(await crypto.md5('😊'), '5deda34cd95f304948d2bc1b4a62c11e')
2426
})

0 commit comments

Comments
 (0)