mirror of
https://github.com/brianc/node-postgres.git
synced 2025-12-08 20:16:25 +00:00
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.
This commit is contained in:
parent
2b469d01da
commit
5532ca51db
@ -2,13 +2,14 @@
|
||||
|
||||
var EventEmitter = require('events').EventEmitter
|
||||
var utils = require('./utils')
|
||||
var sasl = require('./sasl')
|
||||
var sasl = require('./crypto/sasl')
|
||||
var TypeOverrides = require('./type-overrides')
|
||||
|
||||
var ConnectionParameters = require('./connection-parameters')
|
||||
var Query = require('./query')
|
||||
var defaults = require('./defaults')
|
||||
var Connection = require('./connection')
|
||||
const crypto = require('./crypto/utils')
|
||||
|
||||
class Client extends EventEmitter {
|
||||
constructor(config) {
|
||||
@ -245,9 +246,13 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
_handleAuthMD5Password(msg) {
|
||||
this._checkPgPass(() => {
|
||||
const hashedPassword = utils.postgresMd5PasswordHash(this.user, this.password, msg.salt)
|
||||
this.connection.password(hashedPassword)
|
||||
this._checkPgPass(async () => {
|
||||
try {
|
||||
const hashedPassword = await crypto.postgresMd5PasswordHash(this.user, this.password, msg.salt)
|
||||
this.connection.password(hashedPassword)
|
||||
} catch (e) {
|
||||
this.emit('error', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -262,9 +267,9 @@ class Client extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
_handleAuthSASLContinue(msg) {
|
||||
async _handleAuthSASLContinue(msg) {
|
||||
try {
|
||||
sasl.continueSession(this.saslSession, this.password, msg.data)
|
||||
await sasl.continueSession(this.saslSession, this.password, msg.data)
|
||||
this.connection.sendSCRAMClientFinalMessage(this.saslSession.response)
|
||||
} catch (err) {
|
||||
this.connection.emit('error', err)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use strict'
|
||||
const crypto = require('crypto')
|
||||
const crypto = require('./utils')
|
||||
|
||||
function startSession(mechanisms) {
|
||||
if (mechanisms.indexOf('SCRAM-SHA-256') === -1) {
|
||||
@ -16,7 +16,7 @@ function startSession(mechanisms) {
|
||||
}
|
||||
}
|
||||
|
||||
function continueSession(session, password, serverData) {
|
||||
async function continueSession(session, password, serverData) {
|
||||
if (session.message !== 'SASLInitialResponse') {
|
||||
throw new Error('SASL: Last message was not SASLInitialResponse')
|
||||
}
|
||||
@ -38,29 +38,22 @@ function continueSession(session, password, serverData) {
|
||||
throw new Error('SASL: SCRAM-SERVER-FIRST-MESSAGE: server nonce is too short')
|
||||
}
|
||||
|
||||
var saltBytes = Buffer.from(sv.salt, 'base64')
|
||||
|
||||
var saltedPassword = crypto.pbkdf2Sync(password, saltBytes, sv.iteration, 32, 'sha256')
|
||||
|
||||
var clientKey = hmacSha256(saltedPassword, 'Client Key')
|
||||
var storedKey = sha256(clientKey)
|
||||
|
||||
var clientFirstMessageBare = 'n=*,r=' + session.clientNonce
|
||||
var serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration
|
||||
|
||||
var clientFinalMessageWithoutProof = 'c=biws,r=' + sv.nonce
|
||||
|
||||
var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
|
||||
|
||||
var clientSignature = hmacSha256(storedKey, authMessage)
|
||||
var clientProofBytes = xorBuffers(clientKey, clientSignature)
|
||||
var clientProof = clientProofBytes.toString('base64')
|
||||
|
||||
var serverKey = hmacSha256(saltedPassword, 'Server Key')
|
||||
var serverSignatureBytes = hmacSha256(serverKey, authMessage)
|
||||
var saltBytes = Buffer.from(sv.salt, 'base64')
|
||||
var saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration)
|
||||
var clientKey = await crypto.hmacSha256(saltedPassword, 'Client Key')
|
||||
var storedKey = await crypto.sha256(clientKey)
|
||||
var clientSignature = await crypto.hmacSha256(storedKey, authMessage)
|
||||
var clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString('base64')
|
||||
var serverKey = await crypto.hmacSha256(saltedPassword, 'Server Key')
|
||||
var serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage)
|
||||
|
||||
session.message = 'SASLResponse'
|
||||
session.serverSignature = serverSignatureBytes.toString('base64')
|
||||
session.serverSignature = Buffer.from(serverSignatureBytes).toString('base64')
|
||||
session.response = clientFinalMessageWithoutProof + ',p=' + clientProof
|
||||
}
|
||||
|
||||
@ -186,14 +179,6 @@ function xorBuffers(a, b) {
|
||||
return Buffer.from(a.map((_, i) => a[i] ^ b[i]))
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return crypto.createHash('sha256').update(text).digest()
|
||||
}
|
||||
|
||||
function hmacSha256(key, msg) {
|
||||
return crypto.createHmac('sha256', key).update(msg).digest()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startSession,
|
||||
continueSession,
|
||||
37
packages/pg/lib/crypto/utils-legacy.js
Normal file
37
packages/pg/lib/crypto/utils-legacy.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict'
|
||||
// This file contains crypto utility functions for versions of Node.js < 15.0.0,
|
||||
// which does not support the WebCrypto.subtle API.
|
||||
|
||||
const nodeCrypto = require('crypto')
|
||||
|
||||
function md5(string) {
|
||||
return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex')
|
||||
}
|
||||
|
||||
// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html
|
||||
function postgresMd5PasswordHash(user, password, salt) {
|
||||
var inner = md5(password + user)
|
||||
var outer = md5(Buffer.concat([Buffer.from(inner), salt]))
|
||||
return 'md5' + outer
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return nodeCrypto.createHash('sha256').update(text).digest()
|
||||
}
|
||||
|
||||
function hmacSha256(key, msg) {
|
||||
return nodeCrypto.createHmac('sha256', key).update(msg).digest()
|
||||
}
|
||||
|
||||
async function deriveKey(password, salt, iterations) {
|
||||
return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
postgresMd5PasswordHash,
|
||||
randomBytes: nodeCrypto.randomBytes,
|
||||
deriveKey,
|
||||
sha256,
|
||||
hmacSha256,
|
||||
md5,
|
||||
}
|
||||
92
packages/pg/lib/crypto/utils.js
Normal file
92
packages/pg/lib/crypto/utils.js
Normal file
@ -0,0 +1,92 @@
|
||||
'use strict'
|
||||
|
||||
const useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split('.')[0]) < 15
|
||||
if (useLegacyCrypto) {
|
||||
// We are on an old version of Node.js that requires legacy crypto utilities.
|
||||
module.exports = require('./utils-legacy')
|
||||
return
|
||||
}
|
||||
|
||||
const nodeCrypto = require('crypto')
|
||||
|
||||
module.exports = {
|
||||
postgresMd5PasswordHash,
|
||||
randomBytes,
|
||||
deriveKey,
|
||||
sha256,
|
||||
hmacSha256,
|
||||
md5,
|
||||
}
|
||||
|
||||
/**
|
||||
* The Web Crypto API - grabbed from the Node.js library or the global
|
||||
* @type Crypto
|
||||
*/
|
||||
const webCrypto = nodeCrypto.webcrypto || globalThis.crypto
|
||||
/**
|
||||
* The SubtleCrypto API for low level crypto operations.
|
||||
* @type SubtleCrypto
|
||||
*/
|
||||
const subtleCrypto = webCrypto.subtle
|
||||
const textEncoder = new TextEncoder()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} length
|
||||
* @returns
|
||||
*/
|
||||
function randomBytes(length) {
|
||||
return webCrypto.getRandomValues(Buffer.alloc(length))
|
||||
}
|
||||
|
||||
async function md5(string) {
|
||||
try {
|
||||
return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex')
|
||||
} catch (e) {
|
||||
// `createHash()` failed so we are probably not in Node.js, use the WebCrypto API instead.
|
||||
// Note that the MD5 algorithm on WebCrypto is not available in Node.js.
|
||||
// This is why we cannot just use WebCrypto in all environments.
|
||||
const data = typeof string === 'string' ? textEncoder.encode(string) : string
|
||||
const hash = await subtleCrypto.digest('MD5', data)
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html
|
||||
async function postgresMd5PasswordHash(user, password, salt) {
|
||||
var inner = await md5(password + user)
|
||||
var outer = await md5(Buffer.concat([Buffer.from(inner), salt]))
|
||||
return 'md5' + outer
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SHA-256 digest of the given data
|
||||
* @param {Buffer} data
|
||||
*/
|
||||
async function sha256(text) {
|
||||
return await subtleCrypto.digest('SHA-256', text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the message with the given key
|
||||
* @param {ArrayBuffer} keyBuffer
|
||||
* @param {string} msg
|
||||
*/
|
||||
async function hmacSha256(keyBuffer, msg) {
|
||||
const key = await subtleCrypto.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
|
||||
return await subtleCrypto.sign('HMAC', key, textEncoder.encode(msg))
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a key from the password and salt
|
||||
* @param {string} password
|
||||
* @param {Uint8Array} salt
|
||||
* @param {number} iterations
|
||||
*/
|
||||
async function deriveKey(password, salt, iterations) {
|
||||
const key = await subtleCrypto.importKey('raw', textEncoder.encode(password), 'PBKDF2', false, ['deriveBits'])
|
||||
const params = { name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations: iterations }
|
||||
return await subtleCrypto.deriveBits(params, key, 32 * 8, ['deriveBits'])
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
const defaults = require('./defaults')
|
||||
|
||||
function escapeElement(elementRepresentation) {
|
||||
@ -164,17 +162,6 @@ function normalizeQueryConfig(config, values, callback) {
|
||||
return config
|
||||
}
|
||||
|
||||
const md5 = function (string) {
|
||||
return crypto.createHash('md5').update(string, 'utf-8').digest('hex')
|
||||
}
|
||||
|
||||
// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html
|
||||
const postgresMd5PasswordHash = function (user, password, salt) {
|
||||
var inner = md5(password + user)
|
||||
var outer = md5(Buffer.concat([Buffer.from(inner), salt]))
|
||||
return 'md5' + outer
|
||||
}
|
||||
|
||||
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
|
||||
const escapeIdentifier = function (str) {
|
||||
return '"' + str.replace(/"/g, '""') + '"'
|
||||
@ -205,8 +192,6 @@ const escapeLiteral = function (str) {
|
||||
return escaped
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
prepareValue: function prepareValueWrapper(value) {
|
||||
// this ensures that extra arguments do not get passed into prepareValue
|
||||
@ -214,8 +199,6 @@ module.exports = {
|
||||
return prepareValue(value)
|
||||
},
|
||||
normalizeQueryConfig,
|
||||
postgresMd5PasswordHash,
|
||||
md5,
|
||||
escapeIdentifier,
|
||||
escapeLiteral
|
||||
escapeLiteral,
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ var net = require('net')
|
||||
var helper = require('../test-helper')
|
||||
var Connection = require('../../../lib/connection')
|
||||
var utils = require('../../../lib/utils')
|
||||
const crypto = require('../../../lib/crypto/utils')
|
||||
var connect = function (callback) {
|
||||
var username = helper.args.user
|
||||
var database = helper.args.database
|
||||
@ -20,8 +21,8 @@ var connect = function (callback) {
|
||||
con.once('authenticationCleartextPassword', function () {
|
||||
con.password(helper.args.password)
|
||||
})
|
||||
con.once('authenticationMD5Password', function (msg) {
|
||||
con.password(utils.postgresMd5PasswordHash(helper.args.user, helper.args.password, msg.salt))
|
||||
con.once('authenticationMD5Password', async function (msg) {
|
||||
con.password(await crypto.postgresMd5PasswordHash(helper.args.user, helper.args.password, msg.salt))
|
||||
})
|
||||
con.once('readyForQuery', function () {
|
||||
con.query('create temp table ids(id integer)')
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
'use strict'
|
||||
var helper = require('./test-helper')
|
||||
const BufferList = require('../../buffer-list')
|
||||
var utils = require('../../../lib/utils')
|
||||
var crypto = require('../../../lib/crypto/utils')
|
||||
|
||||
test('md5 authentication', function () {
|
||||
test('md5 authentication', async function () {
|
||||
var client = helper.createClient()
|
||||
client.password = '!'
|
||||
var salt = Buffer.from([1, 2, 3, 4])
|
||||
client.connection.emit('authenticationMD5Password', { salt: salt })
|
||||
await client.connection.emit('authenticationMD5Password', { salt: salt })
|
||||
|
||||
test('responds', function () {
|
||||
assert.lengthIs(client.connection.stream.packets, 1)
|
||||
test('should have correct encrypted data', function () {
|
||||
var password = utils.postgresMd5PasswordHash(client.user, client.password, salt)
|
||||
// how do we want to test this?
|
||||
assert.equalBuffers(client.connection.stream.packets[0], new BufferList().addCString(password).join(true, 'p'))
|
||||
setTimeout(() =>
|
||||
test('responds', function () {
|
||||
assert.lengthIs(client.connection.stream.packets, 1)
|
||||
test('should have correct encrypted data', async function () {
|
||||
var password = await crypto.postgresMd5PasswordHash(client.user, client.password, salt)
|
||||
// how do we want to test this?
|
||||
assert.equalBuffers(client.connection.stream.packets[0], new BufferList().addCString(password).join(true, 'p'))
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('md5 of utf-8 strings', function () {
|
||||
assert.equal(utils.md5('😊'), '5deda34cd95f304948d2bc1b4a62c11e')
|
||||
test('md5 of utf-8 strings', async function () {
|
||||
assert.equal(await crypto.md5('😊'), '5deda34cd95f304948d2bc1b4a62c11e')
|
||||
})
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
'use strict'
|
||||
require('./test-helper')
|
||||
const helper = require('./test-helper')
|
||||
|
||||
var sasl = require('../../../lib/sasl')
|
||||
var sasl = require('../../../lib/crypto/sasl')
|
||||
|
||||
test('sasl/scram', function () {
|
||||
test('startSession', function () {
|
||||
test('fails when mechanisms does not include SCRAM-SHA-256', function () {
|
||||
const suite = new helper.Suite()
|
||||
|
||||
suite.test('sasl/scram', function () {
|
||||
suite.test('startSession', function () {
|
||||
suite.test('fails when mechanisms does not include SCRAM-SHA-256', function () {
|
||||
assert.throws(
|
||||
function () {
|
||||
sasl.startSession([])
|
||||
@ -16,7 +18,7 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('returns expected session data', function () {
|
||||
suite.test('returns expected session data', function () {
|
||||
const session = sasl.startSession(['SCRAM-SHA-256'])
|
||||
|
||||
assert.equal(session.mechanism, 'SCRAM-SHA-256')
|
||||
@ -26,7 +28,7 @@ test('sasl/scram', function () {
|
||||
assert(session.response.match(/^n,,n=\*,r=.{24}/))
|
||||
})
|
||||
|
||||
test('creates random nonces', function () {
|
||||
suite.test('creates random nonces', function () {
|
||||
const session1 = sasl.startSession(['SCRAM-SHA-256'])
|
||||
const session2 = sasl.startSession(['SCRAM-SHA-256'])
|
||||
|
||||
@ -34,11 +36,11 @@ test('sasl/scram', function () {
|
||||
})
|
||||
})
|
||||
|
||||
test('continueSession', function () {
|
||||
test('fails when last session message was not SASLInitialResponse', function () {
|
||||
assert.throws(
|
||||
suite.test('continueSession', function () {
|
||||
suite.testAsync('fails when last session message was not SASLInitialResponse', async function () {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession({}, '', '')
|
||||
return sasl.continueSession({}, '', '')
|
||||
},
|
||||
{
|
||||
message: 'SASL: Last message was not SASLInitialResponse',
|
||||
@ -46,10 +48,10 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when nonce is missing in server message', function () {
|
||||
assert.throws(
|
||||
suite.testAsync('fails when nonce is missing in server message', function () {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession(
|
||||
return sasl.continueSession(
|
||||
{
|
||||
message: 'SASLInitialResponse',
|
||||
},
|
||||
@ -63,10 +65,10 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when salt is missing in server message', function () {
|
||||
assert.throws(
|
||||
suite.testAsync('fails when salt is missing in server message', function () {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession(
|
||||
return sasl.continueSession(
|
||||
{
|
||||
message: 'SASLInitialResponse',
|
||||
},
|
||||
@ -80,11 +82,11 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when client password is not a string', function () {
|
||||
for(const badPasswordValue of [null, undefined, 123, new Date(), {}]) {
|
||||
assert.throws(
|
||||
suite.testAsync('fails when client password is not a string', function () {
|
||||
for (const badPasswordValue of [null, undefined, 123, new Date(), {}]) {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession(
|
||||
return sasl.continueSession(
|
||||
{
|
||||
message: 'SASLInitialResponse',
|
||||
clientNonce: 'a',
|
||||
@ -100,10 +102,10 @@ test('sasl/scram', function () {
|
||||
}
|
||||
})
|
||||
|
||||
test('fails when client password is an empty string', function () {
|
||||
assert.throws(
|
||||
suite.testAsync('fails when client password is an empty string', function () {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession(
|
||||
return sasl.continueSession(
|
||||
{
|
||||
message: 'SASLInitialResponse',
|
||||
clientNonce: 'a',
|
||||
@ -118,10 +120,10 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when iteration is missing in server message', function () {
|
||||
assert.throws(
|
||||
suite.testAsync('fails when iteration is missing in server message', function () {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession(
|
||||
return sasl.continueSession(
|
||||
{
|
||||
message: 'SASLInitialResponse',
|
||||
},
|
||||
@ -135,10 +137,10 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when server nonce does not start with client nonce', function () {
|
||||
assert.throws(
|
||||
suite.testAsync('fails when server nonce does not start with client nonce', function () {
|
||||
assert.rejects(
|
||||
function () {
|
||||
sasl.continueSession(
|
||||
return sasl.continueSession(
|
||||
{
|
||||
message: 'SASLInitialResponse',
|
||||
clientNonce: '2',
|
||||
@ -153,13 +155,13 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('sets expected session data', function () {
|
||||
suite.testAsync('sets expected session data', async function () {
|
||||
const session = {
|
||||
message: 'SASLInitialResponse',
|
||||
clientNonce: 'a',
|
||||
}
|
||||
|
||||
sasl.continueSession(session, 'password', 'r=ab,s=abcd,i=1')
|
||||
await sasl.continueSession(session, 'password', 'r=ab,s=abcd,i=1')
|
||||
|
||||
assert.equal(session.message, 'SASLResponse')
|
||||
assert.equal(session.serverSignature, 'jwt97IHWFn7FEqHykPTxsoQrKGOMXJl/PJyJ1JXTBKc=')
|
||||
@ -168,8 +170,8 @@ test('sasl/scram', function () {
|
||||
})
|
||||
})
|
||||
|
||||
test('continueSession', function () {
|
||||
test('fails when last session message was not SASLResponse', function () {
|
||||
suite.test('finalizeSession', function () {
|
||||
suite.test('fails when last session message was not SASLResponse', function () {
|
||||
assert.throws(
|
||||
function () {
|
||||
sasl.finalizeSession({})
|
||||
@ -180,7 +182,7 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when server signature is not valid base64', function () {
|
||||
suite.test('fails when server signature is not valid base64', function () {
|
||||
assert.throws(
|
||||
function () {
|
||||
sasl.finalizeSession(
|
||||
@ -197,7 +199,7 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('fails when server signature does not match', function () {
|
||||
suite.test('fails when server signature does not match', function () {
|
||||
assert.throws(
|
||||
function () {
|
||||
sasl.finalizeSession(
|
||||
@ -214,7 +216,7 @@ test('sasl/scram', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test('does not fail when eveything is ok', function () {
|
||||
suite.test('does not fail when eveything is ok', function () {
|
||||
sasl.finalizeSession(
|
||||
{
|
||||
message: 'SASLResponse',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user