pg 8.13.2 → 8.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +12 -2
- package/lib/crypto/cert-signatures.js +121 -0
- package/lib/crypto/sasl.js +33 -7
- package/lib/crypto/utils-legacy.js +6 -0
- package/lib/crypto/utils-webcrypto.js +5 -0
- package/lib/result.js +4 -2
- package/package.json +4 -4
package/lib/client.js
CHANGED
|
@@ -43,6 +43,7 @@ class Client extends EventEmitter {
|
|
|
43
43
|
this._connectionError = false
|
|
44
44
|
this._queryable = true
|
|
45
45
|
|
|
46
|
+
this.enableChannelBinding = Boolean(c.enableChannelBinding) // set true to use SCRAM-SHA-256-PLUS when offered
|
|
46
47
|
this.connection =
|
|
47
48
|
c.connection ||
|
|
48
49
|
new Connection({
|
|
@@ -104,6 +105,10 @@ class Client extends EventEmitter {
|
|
|
104
105
|
con._ending = true
|
|
105
106
|
con.stream.destroy(new Error('timeout expired'))
|
|
106
107
|
}, this._connectionTimeoutMillis)
|
|
108
|
+
|
|
109
|
+
if (this.connectionTimeoutHandle.unref) {
|
|
110
|
+
this.connectionTimeoutHandle.unref()
|
|
111
|
+
}
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
if (this.host && this.host.indexOf('/') === 0) {
|
|
@@ -258,7 +263,7 @@ class Client extends EventEmitter {
|
|
|
258
263
|
_handleAuthSASL(msg) {
|
|
259
264
|
this._checkPgPass(() => {
|
|
260
265
|
try {
|
|
261
|
-
this.saslSession = sasl.startSession(msg.mechanisms)
|
|
266
|
+
this.saslSession = sasl.startSession(msg.mechanisms, this.enableChannelBinding && this.connection.stream)
|
|
262
267
|
this.connection.sendSASLInitialResponseMessage(this.saslSession.mechanism, this.saslSession.response)
|
|
263
268
|
} catch (err) {
|
|
264
269
|
this.connection.emit('error', err)
|
|
@@ -268,7 +273,12 @@ class Client extends EventEmitter {
|
|
|
268
273
|
|
|
269
274
|
async _handleAuthSASLContinue(msg) {
|
|
270
275
|
try {
|
|
271
|
-
await sasl.continueSession(
|
|
276
|
+
await sasl.continueSession(
|
|
277
|
+
this.saslSession,
|
|
278
|
+
this.password,
|
|
279
|
+
msg.data,
|
|
280
|
+
this.enableChannelBinding && this.connection.stream
|
|
281
|
+
)
|
|
272
282
|
this.connection.sendSCRAMClientFinalMessage(this.saslSession.response)
|
|
273
283
|
} catch (err) {
|
|
274
284
|
this.connection.emit('error', err)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
function x509Error(msg, cert) {
|
|
2
|
+
throw new Error('SASL channel binding: ' + msg + ' when parsing public certificate ' + cert.toString('base64'))
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function readASN1Length(data, index) {
|
|
6
|
+
let length = data[index++]
|
|
7
|
+
if (length < 0x80) return { length, index }
|
|
8
|
+
|
|
9
|
+
const lengthBytes = length & 0x7f
|
|
10
|
+
if (lengthBytes > 4) x509Error('bad length', data)
|
|
11
|
+
|
|
12
|
+
length = 0
|
|
13
|
+
for (let i = 0; i < lengthBytes; i++) {
|
|
14
|
+
length = (length << 8) | data[index++]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { length, index }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readASN1OID(data, index) {
|
|
21
|
+
if (data[index++] !== 0x6) x509Error('non-OID data', data) // 6 = OID
|
|
22
|
+
|
|
23
|
+
const { length: OIDLength, index: indexAfterOIDLength } = readASN1Length(data, index)
|
|
24
|
+
index = indexAfterOIDLength
|
|
25
|
+
lastIndex = index + OIDLength
|
|
26
|
+
|
|
27
|
+
const byte1 = data[index++]
|
|
28
|
+
let oid = ((byte1 / 40) >> 0) + '.' + (byte1 % 40)
|
|
29
|
+
|
|
30
|
+
while (index < lastIndex) {
|
|
31
|
+
// loop over numbers in OID
|
|
32
|
+
let value = 0
|
|
33
|
+
while (index < lastIndex) {
|
|
34
|
+
// loop over bytes in number
|
|
35
|
+
const nextByte = data[index++]
|
|
36
|
+
value = (value << 7) | (nextByte & 0x7f)
|
|
37
|
+
if (nextByte < 0x80) break
|
|
38
|
+
}
|
|
39
|
+
oid += '.' + value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { oid, index }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function expectASN1Seq(data, index) {
|
|
46
|
+
if (data[index++] !== 0x30) x509Error('non-sequence data', data) // 30 = Sequence
|
|
47
|
+
return readASN1Length(data, index)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function signatureAlgorithmHashFromCertificate(data, index) {
|
|
51
|
+
// read this thread: https://www.postgresql.org/message-id/17760-b6c61e752ec07060%40postgresql.org
|
|
52
|
+
if (index === undefined) index = 0
|
|
53
|
+
index = expectASN1Seq(data, index).index
|
|
54
|
+
const { length: certInfoLength, index: indexAfterCertInfoLength } = expectASN1Seq(data, index)
|
|
55
|
+
index = indexAfterCertInfoLength + certInfoLength // skip over certificate info
|
|
56
|
+
index = expectASN1Seq(data, index).index // skip over signature length field
|
|
57
|
+
const { oid, index: indexAfterOID } = readASN1OID(data, index)
|
|
58
|
+
switch (oid) {
|
|
59
|
+
// RSA
|
|
60
|
+
case '1.2.840.113549.1.1.4':
|
|
61
|
+
return 'MD5'
|
|
62
|
+
case '1.2.840.113549.1.1.5':
|
|
63
|
+
return 'SHA-1'
|
|
64
|
+
case '1.2.840.113549.1.1.11':
|
|
65
|
+
return 'SHA-256'
|
|
66
|
+
case '1.2.840.113549.1.1.12':
|
|
67
|
+
return 'SHA-384'
|
|
68
|
+
case '1.2.840.113549.1.1.13':
|
|
69
|
+
return 'SHA-512'
|
|
70
|
+
case '1.2.840.113549.1.1.14':
|
|
71
|
+
return 'SHA-224'
|
|
72
|
+
case '1.2.840.113549.1.1.15':
|
|
73
|
+
return 'SHA512-224'
|
|
74
|
+
case '1.2.840.113549.1.1.16':
|
|
75
|
+
return 'SHA512-256'
|
|
76
|
+
// ECDSA
|
|
77
|
+
case '1.2.840.10045.4.1':
|
|
78
|
+
return 'SHA-1'
|
|
79
|
+
case '1.2.840.10045.4.3.1':
|
|
80
|
+
return 'SHA-224'
|
|
81
|
+
case '1.2.840.10045.4.3.2':
|
|
82
|
+
return 'SHA-256'
|
|
83
|
+
case '1.2.840.10045.4.3.3':
|
|
84
|
+
return 'SHA-384'
|
|
85
|
+
case '1.2.840.10045.4.3.4':
|
|
86
|
+
return 'SHA-512'
|
|
87
|
+
// RSASSA-PSS: hash is indicated separately
|
|
88
|
+
case '1.2.840.113549.1.1.10':
|
|
89
|
+
index = indexAfterOID
|
|
90
|
+
index = expectASN1Seq(data, index).index
|
|
91
|
+
if (data[index++] !== 0xa0) x509Error('non-tag data', data) // a0 = constructed tag 0
|
|
92
|
+
index = readASN1Length(data, index).index // skip over tag length field
|
|
93
|
+
index = expectASN1Seq(data, index).index // skip over sequence length field
|
|
94
|
+
const { oid: hashOID } = readASN1OID(data, index)
|
|
95
|
+
switch (hashOID) {
|
|
96
|
+
// standalone hash OIDs
|
|
97
|
+
case '1.2.840.113549.2.5':
|
|
98
|
+
return 'MD5'
|
|
99
|
+
case '1.3.14.3.2.26':
|
|
100
|
+
return 'SHA-1'
|
|
101
|
+
case '2.16.840.1.101.3.4.2.1':
|
|
102
|
+
return 'SHA-256'
|
|
103
|
+
case '2.16.840.1.101.3.4.2.2':
|
|
104
|
+
return 'SHA-384'
|
|
105
|
+
case '2.16.840.1.101.3.4.2.3':
|
|
106
|
+
return 'SHA-512'
|
|
107
|
+
}
|
|
108
|
+
x509Error('unknown hash OID ' + hashOID, data)
|
|
109
|
+
// Ed25519 -- see https: return//github.com/openssl/openssl/issues/15477
|
|
110
|
+
case '1.3.101.110':
|
|
111
|
+
case '1.3.101.112': // ph
|
|
112
|
+
return 'SHA-512'
|
|
113
|
+
// Ed448 -- still not in pg 17.2 (if supported, digest would be SHAKE256 x 64 bytes)
|
|
114
|
+
case '1.3.101.111':
|
|
115
|
+
case '1.3.101.113': // ph
|
|
116
|
+
x509Error('Ed448 certificate channel binding is not currently supported by Postgres')
|
|
117
|
+
}
|
|
118
|
+
x509Error('unknown OID ' + oid, data)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { signatureAlgorithmHashFromCertificate }
|
package/lib/crypto/sasl.js
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const crypto = require('./utils')
|
|
3
|
+
const { signatureAlgorithmHashFromCertificate } = require('./cert-signatures')
|
|
3
4
|
|
|
4
|
-
function startSession(mechanisms) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
function startSession(mechanisms, stream) {
|
|
6
|
+
const candidates = ['SCRAM-SHA-256']
|
|
7
|
+
if (stream) candidates.unshift('SCRAM-SHA-256-PLUS') // higher-priority, so placed first
|
|
8
|
+
|
|
9
|
+
const mechanism = candidates.find((candidate) => mechanisms.includes(candidate))
|
|
10
|
+
|
|
11
|
+
if (!mechanism) {
|
|
12
|
+
throw new Error('SASL: Only mechanism(s) ' + candidates.join(' and ') + ' are supported')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (mechanism === 'SCRAM-SHA-256-PLUS' && typeof stream.getPeerCertificate !== 'function') {
|
|
16
|
+
// this should never happen if we are really talking to a Postgres server
|
|
17
|
+
throw new Error('SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate')
|
|
7
18
|
}
|
|
8
19
|
|
|
9
20
|
const clientNonce = crypto.randomBytes(18).toString('base64')
|
|
21
|
+
const gs2Header = mechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : stream ? 'y' : 'n'
|
|
10
22
|
|
|
11
23
|
return {
|
|
12
|
-
mechanism
|
|
24
|
+
mechanism,
|
|
13
25
|
clientNonce,
|
|
14
|
-
response: '
|
|
26
|
+
response: gs2Header + ',,n=*,r=' + clientNonce,
|
|
15
27
|
message: 'SASLInitialResponse',
|
|
16
28
|
}
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
async function continueSession(session, password, serverData) {
|
|
31
|
+
async function continueSession(session, password, serverData, stream) {
|
|
20
32
|
if (session.message !== 'SASLInitialResponse') {
|
|
21
33
|
throw new Error('SASL: Last message was not SASLInitialResponse')
|
|
22
34
|
}
|
|
@@ -40,7 +52,21 @@ async function continueSession(session, password, serverData) {
|
|
|
40
52
|
|
|
41
53
|
var clientFirstMessageBare = 'n=*,r=' + session.clientNonce
|
|
42
54
|
var serverFirstMessage = 'r=' + sv.nonce + ',s=' + sv.salt + ',i=' + sv.iteration
|
|
43
|
-
|
|
55
|
+
|
|
56
|
+
// without channel binding:
|
|
57
|
+
let channelBinding = stream ? 'eSws' : 'biws' // 'y,,' or 'n,,', base64-encoded
|
|
58
|
+
|
|
59
|
+
// override if channel binding is in use:
|
|
60
|
+
if (session.mechanism === 'SCRAM-SHA-256-PLUS') {
|
|
61
|
+
const peerCert = stream.getPeerCertificate().raw
|
|
62
|
+
let hashName = signatureAlgorithmHashFromCertificate(peerCert)
|
|
63
|
+
if (hashName === 'MD5' || hashName === 'SHA-1') hashName = 'SHA-256'
|
|
64
|
+
const certHash = await crypto.hashByName(hashName, peerCert)
|
|
65
|
+
const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)])
|
|
66
|
+
channelBinding = bindingData.toString('base64')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var clientFinalMessageWithoutProof = 'c=' + channelBinding + ',r=' + sv.nonce
|
|
44
70
|
var authMessage = clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof
|
|
45
71
|
|
|
46
72
|
var saltBytes = Buffer.from(sv.salt, 'base64')
|
|
@@ -19,6 +19,11 @@ function sha256(text) {
|
|
|
19
19
|
return nodeCrypto.createHash('sha256').update(text).digest()
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function hashByName(hashName, text) {
|
|
23
|
+
hashName = hashName.replace(/(\D)-/, '$1') // e.g. SHA-256 -> SHA256
|
|
24
|
+
return nodeCrypto.createHash(hashName).update(text).digest()
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
function hmacSha256(key, msg) {
|
|
23
28
|
return nodeCrypto.createHmac('sha256', key).update(msg).digest()
|
|
24
29
|
}
|
|
@@ -32,6 +37,7 @@ module.exports = {
|
|
|
32
37
|
randomBytes: nodeCrypto.randomBytes,
|
|
33
38
|
deriveKey,
|
|
34
39
|
sha256,
|
|
40
|
+
hashByName,
|
|
35
41
|
hmacSha256,
|
|
36
42
|
md5,
|
|
37
43
|
}
|
|
@@ -5,6 +5,7 @@ module.exports = {
|
|
|
5
5
|
randomBytes,
|
|
6
6
|
deriveKey,
|
|
7
7
|
sha256,
|
|
8
|
+
hashByName,
|
|
8
9
|
hmacSha256,
|
|
9
10
|
md5,
|
|
10
11
|
}
|
|
@@ -60,6 +61,10 @@ async function sha256(text) {
|
|
|
60
61
|
return await subtleCrypto.digest('SHA-256', text)
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
async function hashByName(hashName, text) {
|
|
65
|
+
return await subtleCrypto.digest(hashName, text)
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
/**
|
|
64
69
|
* Sign the message with the given key
|
|
65
70
|
* @param {ArrayBuffer} keyBuffer
|
package/lib/result.js
CHANGED
|
@@ -88,11 +88,11 @@ class Result {
|
|
|
88
88
|
this._parsers = new Array(fieldDescriptions.length)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
var row = {}
|
|
92
92
|
|
|
93
93
|
for (var i = 0; i < fieldDescriptions.length; i++) {
|
|
94
94
|
var desc = fieldDescriptions[i]
|
|
95
|
-
|
|
95
|
+
row[desc.name] = null
|
|
96
96
|
|
|
97
97
|
if (this._types) {
|
|
98
98
|
this._parsers[i] = this._types.getTypeParser(desc.dataTypeID, desc.format || 'text')
|
|
@@ -100,6 +100,8 @@ class Result {
|
|
|
100
100
|
this._parsers[i] = types.getTypeParser(desc.dataTypeID, desc.format || 'text')
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
this._prebuiltEmptyResultObject = { ...row }
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pg",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.0",
|
|
4
4
|
"description": "PostgreSQL client - pure javascript & libpq with the same API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"database",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"main": "./lib",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"pg-connection-string": "^2.7.0",
|
|
24
|
-
"pg-pool": "^3.
|
|
25
|
-
"pg-protocol": "^1.
|
|
24
|
+
"pg-pool": "^3.8.0",
|
|
25
|
+
"pg-protocol": "^1.8.0",
|
|
26
26
|
"pg-types": "^2.1.0",
|
|
27
27
|
"pgpass": "1.x"
|
|
28
28
|
},
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 8.0.0"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "f7c92e487c6a9c9600585f9de14cb17e7a65e76e"
|
|
62
62
|
}
|