Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SCRAM-SHA-256-PLUS support #1008

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions cf/src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
, delay = 0
, rows = 0
, serverSignature = null
, saslMechanism = null
, nextWriteTimer = null
, terminated = false
, incomings = null
Expand Down Expand Up @@ -680,11 +681,25 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
)
}

async function SASL() {
async function SASL(x) {
const length = x.readUInt32BE(1)
const mechanisms = x.subarray(9, length - 1).toString('utf8').split('\x00') // `length - 1` excludes 2 terminal nulls (string and list)

for (const m of mechanisms) {
if (m === 'SCRAM-SHA-256-PLUS' && socket instanceof tls.TLSSocket) {
saslMechanism = m
break
}
if (m === 'SCRAM-SHA-256') saslMechanism = m
}
if (!saslMechanism) errored(Errors.generic('SASL_MECHANISMS_UNSUPPORTED', 'No supported SASL mechanism was offered'))

const gs2Header = saslMechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : 'y'
nonce = (await crypto.randomBytes(18)).toString('base64')
b().p().str('SCRAM-SHA-256' + b.N)

b().p().str(saslMechanism + b.N)
const i = b.i
write(b.inc(4).str('n,,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
write(b.inc(4).str(gs2Header + ',,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
}

async function SASLContinue(x) {
Expand All @@ -699,13 +714,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose

const clientKey = await hmac(saltedPassword, 'Client Key')

let channelBinding = 'eSws' // 'y,,' base64-encoded
if (saslMechanism === 'SCRAM-SHA-256-PLUS') {
const peerCert = socket.getPeerCertificate().raw
const x509 = await import('@peculiar/x509')
const parsedCert = new x509.X509Certificate(peerCert)
const sigAlgo = parsedCert.signatureAlgorithm
if (!sigAlgo || !sigAlgo.hash || !sigAlgo.hash.name) errored(Errors.generic('SASL_CERT_ERROR', 'Unable to identify certificate digest type for channel binding'))
let hashName = sigAlgo.hash.name;
if (/^(md5)|(sha-?1)$/i.test(hashName)) hashName = 'sha256' // for MD5 and SHA-1, we substitute SHA-256
const certHash = await namedDigest(hashName, peerCert)
const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)])
channelBinding = bindingData.toString('base64')
}

const auth = 'n=*,r=' + nonce + ','
+ 'r=' + res.r + ',s=' + res.s + ',i=' + res.i
+ ',c=biws,r=' + res.r
+ ',c=' + channelBinding + ',r=' + res.r

serverSignature = (await hmac(await hmac(saltedPassword, 'Server Key'), auth)).toString('base64')

const payload = 'c=biws,r=' + res.r + ',p=' + xor(
const payload = 'c=' + channelBinding + ',r=' + res.r + ',p=' + xor(
clientKey, Buffer.from(await hmac(await sha256(clientKey), auth))
).toString('base64')

Expand Down Expand Up @@ -1007,6 +1036,10 @@ function sha256(x) {
return crypto.createHash('sha256').update(x).digest()
}

function namedDigest(name, x) {
return crypto.createHash(name).update(x).digest()
}

function xor(a, b) {
const length = Math.max(a.length, b.length)
const buffer = Buffer.allocUnsafe(length)
Expand Down
2 changes: 1 addition & 1 deletion cf/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ function parseOptions(a, b) {
{}
),
connection : {
application_name: 'postgres.js',
application_name: env.PGAPPNAME || 'postgres.js',
...o.connection,
...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {})
},
Expand Down
43 changes: 38 additions & 5 deletions cjs/src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
, delay = 0
, rows = 0
, serverSignature = null
, saslMechanism = null
, nextWriteTimer = null
, terminated = false
, incomings = null
Expand Down Expand Up @@ -678,11 +679,25 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
)
}

async function SASL() {
async function SASL(x) {
const length = x.readUInt32BE(1)
const mechanisms = x.subarray(9, length - 1).toString('utf8').split('\x00') // `length - 1` excludes 2 terminal nulls (string and list)

for (const m of mechanisms) {
if (m === 'SCRAM-SHA-256-PLUS' && socket instanceof tls.TLSSocket) {
saslMechanism = m
break
}
if (m === 'SCRAM-SHA-256') saslMechanism = m
}
if (!saslMechanism) errored(Errors.generic('SASL_MECHANISMS_UNSUPPORTED', 'No supported SASL mechanism was offered'))

const gs2Header = saslMechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : 'y'
nonce = (await crypto.randomBytes(18)).toString('base64')
b().p().str('SCRAM-SHA-256' + b.N)

b().p().str(saslMechanism + b.N)
const i = b.i
write(b.inc(4).str('n,,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
write(b.inc(4).str(gs2Header + ',,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
}

async function SASLContinue(x) {
Expand All @@ -697,13 +712,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose

const clientKey = await hmac(saltedPassword, 'Client Key')

let channelBinding = 'eSws' // 'y,,' base64-encoded
if (saslMechanism === 'SCRAM-SHA-256-PLUS') {
const peerCert = socket.getPeerCertificate().raw
const x509 = await import('@peculiar/x509')
const parsedCert = new x509.X509Certificate(peerCert)
const sigAlgo = parsedCert.signatureAlgorithm
if (!sigAlgo || !sigAlgo.hash || !sigAlgo.hash.name) errored(Errors.generic('SASL_CERT_ERROR', 'Unable to identify certificate digest type for channel binding'))
let hashName = sigAlgo.hash.name;
if (/^(md5)|(sha-?1)$/i.test(hashName)) hashName = 'sha256' // for MD5 and SHA-1, we substitute SHA-256
const certHash = await namedDigest(hashName, peerCert)
const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)])
channelBinding = bindingData.toString('base64')
}

const auth = 'n=*,r=' + nonce + ','
+ 'r=' + res.r + ',s=' + res.s + ',i=' + res.i
+ ',c=biws,r=' + res.r
+ ',c=' + channelBinding + ',r=' + res.r

serverSignature = (await hmac(await hmac(saltedPassword, 'Server Key'), auth)).toString('base64')

const payload = 'c=biws,r=' + res.r + ',p=' + xor(
const payload = 'c=' + channelBinding + ',r=' + res.r + ',p=' + xor(
clientKey, Buffer.from(await hmac(await sha256(clientKey), auth))
).toString('base64')

Expand Down Expand Up @@ -1005,6 +1034,10 @@ function sha256(x) {
return crypto.createHash('sha256').update(x).digest()
}

function namedDigest(name, x) {
return crypto.createHash(name).update(x).digest()
}

function xor(a, b) {
const length = Math.max(a.length, b.length)
const buffer = Buffer.allocUnsafe(length)
Expand Down
2 changes: 1 addition & 1 deletion cjs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ function parseOptions(a, b) {
{}
),
connection : {
application_name: 'postgres.js',
application_name: env.PGAPPNAME || 'postgres.js',
...o.connection,
...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {})
},
Expand Down
23 changes: 14 additions & 9 deletions deno/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1121,20 +1121,25 @@ It is also possible to connect to the database without a connection string or an
const sql = postgres()
```

| Option | Environment Variables |
| ----------------- | ------------------------ |
| `host` | `PGHOST` |
| `port` | `PGPORT` |
| `database` | `PGDATABASE` |
| `username` | `PGUSERNAME` or `PGUSER` |
| `password` | `PGPASSWORD` |
| `idle_timeout` | `PGIDLE_TIMEOUT` |
| `connect_timeout` | `PGCONNECT_TIMEOUT` |
| Option | Environment Variables |
| ------------------ | ------------------------ |
| `host` | `PGHOST` |
| `port` | `PGPORT` |
| `database` | `PGDATABASE` |
| `username` | `PGUSERNAME` or `PGUSER` |
| `password` | `PGPASSWORD` |
| `application_name` | `PGAPPNAME` |
| `idle_timeout` | `PGIDLE_TIMEOUT` |
| `connect_timeout` | `PGCONNECT_TIMEOUT` |

### Prepared statements

Prepared statements will automatically be created for any queries where it can be inferred that the query is static. This can be disabled by using the `prepare: false` option. For instance — this is useful when [using PGBouncer in `transaction mode`](https://github.com/porsager/postgres/issues/93#issuecomment-656290493).

**update**: [since 1.21.0](https://www.pgbouncer.org/2023/10/pgbouncer-1-21-0)
PGBouncer supports protocol-level named prepared statements when [configured
properly](https://www.pgbouncer.org/config.html#max_prepared_statements)

## Custom Types

You can add ergonomic support for custom types, or simply use `sql.typed(value, type)` inline, where type is the PostgreSQL `oid` for the type and the correctly serialized string. _(`oid` values for types can be found in the `pg_catalog.pg_type` table.)_
Expand Down
43 changes: 38 additions & 5 deletions deno/src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
, delay = 0
, rows = 0
, serverSignature = null
, saslMechanism = null
, nextWriteTimer = null
, terminated = false
, incomings = null
Expand Down Expand Up @@ -681,11 +682,25 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
)
}

async function SASL() {
async function SASL(x) {
const length = x.readUInt32BE(1)
const mechanisms = x.subarray(9, length - 1).toString('utf8').split('\x00') // `length - 1` excludes 2 terminal nulls (string and list)

for (const m of mechanisms) {
if (m === 'SCRAM-SHA-256-PLUS' && socket instanceof tls.TLSSocket) {
saslMechanism = m
break
}
if (m === 'SCRAM-SHA-256') saslMechanism = m
}
if (!saslMechanism) errored(Errors.generic('SASL_MECHANISMS_UNSUPPORTED', 'No supported SASL mechanism was offered'))

const gs2Header = saslMechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : 'y'
nonce = (await crypto.randomBytes(18)).toString('base64')
b().p().str('SCRAM-SHA-256' + b.N)

b().p().str(saslMechanism + b.N)
const i = b.i
write(b.inc(4).str('n,,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
write(b.inc(4).str(gs2Header + ',,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
}

async function SASLContinue(x) {
Expand All @@ -700,13 +715,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose

const clientKey = await hmac(saltedPassword, 'Client Key')

let channelBinding = 'eSws' // 'y,,' base64-encoded
if (saslMechanism === 'SCRAM-SHA-256-PLUS') {
const peerCert = socket.getPeerCertificate().raw
const x509 = await import('@peculiar/x509')
const parsedCert = new x509.X509Certificate(peerCert)
const sigAlgo = parsedCert.signatureAlgorithm
if (!sigAlgo || !sigAlgo.hash || !sigAlgo.hash.name) errored(Errors.generic('SASL_CERT_ERROR', 'Unable to identify certificate digest type for channel binding'))
let hashName = sigAlgo.hash.name;
if (/^(md5)|(sha-?1)$/i.test(hashName)) hashName = 'sha256' // for MD5 and SHA-1, we substitute SHA-256
const certHash = await namedDigest(hashName, peerCert)
const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)])
channelBinding = bindingData.toString('base64')
}

const auth = 'n=*,r=' + nonce + ','
+ 'r=' + res.r + ',s=' + res.s + ',i=' + res.i
+ ',c=biws,r=' + res.r
+ ',c=' + channelBinding + ',r=' + res.r

serverSignature = (await hmac(await hmac(saltedPassword, 'Server Key'), auth)).toString('base64')

const payload = 'c=biws,r=' + res.r + ',p=' + xor(
const payload = 'c=' + channelBinding + ',r=' + res.r + ',p=' + xor(
clientKey, Buffer.from(await hmac(await sha256(clientKey), auth))
).toString('base64')

Expand Down Expand Up @@ -1008,6 +1037,10 @@ function sha256(x) {
return crypto.createHash('sha256').update(x).digest()
}

function namedDigest(name, x) {
return crypto.createHash(name).update(x).digest()
}

function xor(a, b) {
const length = Math.max(a.length, b.length)
const buffer = Buffer.allocUnsafe(length)
Expand Down
2 changes: 1 addition & 1 deletion deno/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ function parseOptions(a, b) {
{}
),
connection : {
application_name: 'postgres.js',
application_name: env.PGAPPNAME || 'postgres.js',
...o.connection,
...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {})
},
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@
"db",
"pg",
"database"
]
],
"dependencies": {
"@peculiar/x509": "^1.12.3"
}
}
42 changes: 37 additions & 5 deletions src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
, delay = 0
, rows = 0
, serverSignature = null
, saslMechanism = null
, nextWriteTimer = null
, terminated = false
, incomings = null
Expand Down Expand Up @@ -678,11 +679,24 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
)
}

async function SASL() {
async function SASL(x) {
const length = x.readUInt32BE(1)
const mechanisms = x.subarray(9, length - 1).toString('utf8').split('\x00') // `length - 1` excludes 2 terminal nulls (string and list)

for (const m of mechanisms) {
if (m === 'SCRAM-SHA-256-PLUS' && socket instanceof tls.TLSSocket) {
saslMechanism = m
break
}
if (m === 'SCRAM-SHA-256') saslMechanism = m
}
if (!saslMechanism) errored(Errors.generic('SASL_MECHANISMS_UNSUPPORTED', 'No supported SASL mechanism was offered'))

const gs2Header = saslMechanism === 'SCRAM-SHA-256-PLUS' ? 'p=tls-server-end-point' : 'y'
nonce = (await crypto.randomBytes(18)).toString('base64')
b().p().str('SCRAM-SHA-256' + b.N)
b().p().str(saslMechanism + b.N)
const i = b.i
write(b.inc(4).str('n,,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
write(b.inc(4).str(gs2Header + ',,n=*,r=' + nonce).i32(b.i - i - 4, i).end())
}

async function SASLContinue(x) {
Expand All @@ -697,13 +711,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose

const clientKey = await hmac(saltedPassword, 'Client Key')

let channelBinding = 'eSws' // 'y,,' base64-encoded
if (saslMechanism === 'SCRAM-SHA-256-PLUS') {
const peerCert = socket.getPeerCertificate().raw
const x509 = await import('@peculiar/x509')
const parsedCert = new x509.X509Certificate(peerCert)
const sigAlgo = parsedCert.signatureAlgorithm
if (!sigAlgo || !sigAlgo.hash || !sigAlgo.hash.name) errored(Errors.generic('SASL_CERT_ERROR', 'Unable to identify certificate digest type for channel binding'))
let hashName = sigAlgo.hash.name;
if (/^(md5)|(sha-?1)$/i.test(hashName)) hashName = 'sha256' // for MD5 and SHA-1, we substitute SHA-256
const certHash = await namedDigest(hashName, peerCert)
const bindingData = Buffer.concat([Buffer.from('p=tls-server-end-point,,'), Buffer.from(certHash)])
channelBinding = bindingData.toString('base64')
}

const auth = 'n=*,r=' + nonce + ','
+ 'r=' + res.r + ',s=' + res.s + ',i=' + res.i
+ ',c=biws,r=' + res.r
+ ',c=' + channelBinding + ',r=' + res.r

serverSignature = (await hmac(await hmac(saltedPassword, 'Server Key'), auth)).toString('base64')

const payload = 'c=biws,r=' + res.r + ',p=' + xor(
const payload = 'c=' + channelBinding + ',r=' + res.r + ',p=' + xor(
clientKey, Buffer.from(await hmac(await sha256(clientKey), auth))
).toString('base64')

Expand Down Expand Up @@ -1005,6 +1033,10 @@ function sha256(x) {
return crypto.createHash('sha256').update(x).digest()
}

function namedDigest(name, x) {
return crypto.createHash(name).update(x).digest()
}

function xor(a, b) {
const length = Math.max(a.length, b.length)
const buffer = Buffer.allocUnsafe(length)
Expand Down
Loading