diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c0c61a68..6e2004090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,16 +56,30 @@ jobs: PGHOST: localhost PGDATABASE: ci_db_test PGTESTNOSSL: 'true' - # SCRAM_TEST_PGUSER: scram_test - # SCRAM_TEST_PGPASSWORD: test4scram + SHA256_TEST_PGUSER: sha256_test + SHA256_TEST_PGPASSWORD: test4@scram steps: - name: Show OS run: | uname -a - # - run: | - # psql \ - # -c "SET password_encryption = 'scram-sha-256'" \ - # -c "CREATE ROLE scram_test LOGIN PASSWORD 'test4scram'" + - name: Wait for GaussDB to be ready + run: | + timeout 60 bash -c 'until pg_isready -h localhost -p 5432; do sleep 2; done' + - name: Setup SHA256 authentication + run: | + # Wait for database to be fully started + sleep 15 + + # Get container ID + CONTAINER_ID=$(docker ps --filter "ancestor=opengauss/opengauss" --format "{{.ID}}") + docker exec $CONTAINER_ID su - omm -c "gs_guc set -D /var/lib/opengauss/data/ -c 'password_encryption_type = 2'" + + # Add SHA256 authentication rule to pg_hba.conf + docker exec $CONTAINER_ID su - omm -c "gs_guc set -D /var/lib/opengauss/data/ -h 'host all sha256_test 0.0.0.0/0 sha256'" + docker exec $CONTAINER_ID su - omm -c "gs_ctl reload -D /var/lib/opengauss/data/" + sleep 5 + + PGPASSWORD=openGauss@123 psql -h localhost -U ci_user -d ci_db_test -c "CREATE ROLE sha256_test login password 'test4@scram';" - uses: actions/checkout@v4 with: persist-credentials: false diff --git a/packages/pg-protocol/src/inbound-parser.test.ts b/packages/pg-protocol/src/inbound-parser.test.ts index 0575993df..cf8418d54 100644 --- a/packages/pg-protocol/src/inbound-parser.test.ts +++ b/packages/pg-protocol/src/inbound-parser.test.ts @@ -194,9 +194,10 @@ const testForMessage = function (buffer: Buffer, expectedMessage: any) { const plainPasswordBuffer = buffers.authenticationCleartextPassword() const md5PasswordBuffer = buffers.authenticationMD5Password() -const SASLBuffer = buffers.authenticationSASL() -const SASLContinueBuffer = buffers.authenticationSASLContinue() -const SASLFinalBuffer = buffers.authenticationSASLFinal() +// SASL authentication is no longer supported +// const SASLBuffer = buffers.authenticationSASL() +// const SASLContinueBuffer = buffers.authenticationSASLContinue() +// const SASLFinalBuffer = buffers.authenticationSASLFinal() const expectedPlainPasswordMessage = { name: 'authenticationCleartextPassword', @@ -207,6 +208,8 @@ const expectedMD5PasswordMessage = { salt: Buffer.from([1, 2, 3, 4]), } +// SASL authentication is no longer supported +/* const expectedSASLMessage = { name: 'authenticationSASL', mechanisms: ['SCRAM-SHA-256'], @@ -221,6 +224,7 @@ const expectedSASLFinalMessage = { name: 'authenticationSASLFinal', data: 'data', } +*/ const notificationResponseBuffer = buffers.notification(4, 'hi', 'boom') const expectedNotificationResponseMessage = { @@ -245,6 +249,9 @@ describe('PgPacketStream', function () { testForMessage(authOkBuffer, expectedAuthenticationOkayMessage) testForMessage(plainPasswordBuffer, expectedPlainPasswordMessage) testForMessage(md5PasswordBuffer, expectedMD5PasswordMessage) + + // SASL authentication tests are commented out as SASL is no longer supported + /* testForMessage(SASLBuffer, expectedSASLMessage) testForMessage(SASLContinueBuffer, expectedSASLContinueMessage) @@ -261,6 +268,7 @@ describe('PgPacketStream', function () { // and adds a test which is deterministic, rather than relying on network packet chunking const extendedSASLFinalBuffer = Buffer.concat([SASLFinalBuffer, Buffer.from([1, 2, 4, 5])]) testForMessage(extendedSASLFinalBuffer, expectedSASLFinalMessage) + */ testForMessage(paramStatusBuffer, expectedParameterStatusMessage) testForMessage(backendKeyDataBuffer, expectedBackendKeyDataMessage) diff --git a/packages/pg-protocol/src/outbound-serializer.test.ts b/packages/pg-protocol/src/outbound-serializer.test.ts index 48dc25b20..f3aa6c502 100644 --- a/packages/pg-protocol/src/outbound-serializer.test.ts +++ b/packages/pg-protocol/src/outbound-serializer.test.ts @@ -12,7 +12,7 @@ describe('serializer', () => { actual, new BufferList() .addInt16(3) - .addInt16(0) + .addInt16(0x33) // this is for gaussdb .addCString('user') .addCString('brian') .addCString('database') diff --git a/packages/pg-protocol/src/parser.ts b/packages/pg-protocol/src/parser.ts index f548726a8..0665270e6 100644 --- a/packages/pg-protocol/src/parser.ts +++ b/packages/pg-protocol/src/parser.ts @@ -24,7 +24,8 @@ import { BackendMessage, MessageName, AuthenticationMD5Password, - AuthenticationSHA256Password, + // AuthenticationSHA256Password is imported but not used - temporarily commented + // AuthenticationSHA256Password, NoticeMessage, } from './messages' import { BufferReader } from './buffer-reader' @@ -340,7 +341,7 @@ export class Parser { // } while (mechanism) // } // break - case 10: // AuthenticationSHA256Password + case 10: // AuthenticationSHA256Password { message.name = 'authenticationSHA256Password' message.data = this.reader.bytes(length - 8) diff --git a/packages/pg-protocol/src/testing/test-buffers.ts b/packages/pg-protocol/src/testing/test-buffers.ts index 1f0d71f2d..e4b2acdd5 100644 --- a/packages/pg-protocol/src/testing/test-buffers.ts +++ b/packages/pg-protocol/src/testing/test-buffers.ts @@ -21,6 +21,8 @@ const buffers = { .join(true, 'R') }, + // SASL authentication is no longer supported - commented out + /* authenticationSASL: function () { return new BufferList().addInt32(10).addCString('SCRAM-SHA-256').addCString('').join(true, 'R') }, @@ -32,6 +34,7 @@ const buffers = { authenticationSASLFinal: function () { return new BufferList().addInt32(12).addString('data').join(true, 'R') }, + */ parameterStatus: function (name: string, value: string) { return new BufferList().addCString(name).addCString(value).join(true, 'S') diff --git a/packages/pg/lib/crypto/rfc5802.js b/packages/pg/lib/crypto/rfc5802.js index c20ef250e..97f5ae6ae 100644 --- a/packages/pg/lib/crypto/rfc5802.js +++ b/packages/pg/lib/crypto/rfc5802.js @@ -4,7 +4,7 @@ const crypto = require('crypto') /** * RFC5802 algorithm implementation * reference: https://github.com/jackc/pgx/commit/3d247719df5910a2adcf935a5038efa26fde58e2#diff-e7952278d2dec72dcda153feed6236482904c063027f34c6474302b112e3d1c9 - * + * * @param {string} password - Password * @param {string} random64code - 64-bit random code * @param {string} token - 8-bit token @@ -14,165 +14,159 @@ const crypto = require('crypto') * @returns {Buffer} - Result byte array */ function RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) { - const k = generateKFromPBKDF2(password, random64code, serverIteration) - const serverKey = getKeyFromHmac(k, Buffer.from('Sever Key')) - const clientKey = getKeyFromHmac(k, Buffer.from('Client Key')) - - let storedKey - if (method.toLowerCase() === 'sha256') { - storedKey = getSha256(clientKey) - } else { - throw new Error('Only sha256 method is supported') - } - - const tokenByte = hexStringToBytes(token) - const clientSignature = getKeyFromHmac(serverKey, tokenByte) - - if (serverSignature && serverSignature !== bytesToHexString(clientSignature)) { - return Buffer.from('') - } - - const hmacResult = getKeyFromHmac(storedKey, tokenByte) - const h = xorBetweenPassword(hmacResult, clientKey, clientKey.length) + const k = generateKFromPBKDF2(password, random64code, serverIteration) + const serverKey = getKeyFromHmac(k, Buffer.from('Sever Key')) + const clientKey = getKeyFromHmac(k, Buffer.from('Client Key')) + + let storedKey + if (method.toLowerCase() === 'sha256') { + storedKey = getSha256(clientKey) + } else { + throw new Error('Only sha256 method is supported') + } + + const tokenByte = hexStringToBytes(token) + const clientSignature = getKeyFromHmac(serverKey, tokenByte) - return bytesToHex(h) + if (serverSignature && serverSignature !== bytesToHexString(clientSignature)) { + return Buffer.from('') + } + + const hmacResult = getKeyFromHmac(storedKey, tokenByte) + const h = xorBetweenPassword(hmacResult, clientKey, clientKey.length) + + return bytesToHex(h) } /** * Generate key using PBKDF2 - * + * * @param {string} password - Password * @param {string} random64code - 64-bit random code * @param {number} serverIteration - Server iteration count * @returns {Buffer} - Generated key */ function generateKFromPBKDF2(password, random64code, serverIteration) { - const random32code = hexStringToBytes(random64code) - return crypto.pbkdf2Sync( - password, - random32code, - serverIteration, - 32, - 'sha1' - ) + const random32code = hexStringToBytes(random64code) + return crypto.pbkdf2Sync(password, random32code, serverIteration, 32, 'sha1') } /** * Convert hex string to byte array - * + * * @param {string} hexString - Hex string * @returns {Buffer} - Byte array */ function hexStringToBytes(hexString) { - if (hexString === '') { - return Buffer.from('') - } - - const upperString = hexString.toUpperCase() - const bytesLen = Math.floor(upperString.length / 2) - const array = Buffer.alloc(bytesLen) - - for (let i = 0; i < bytesLen; i++) { - const pos = i * 2 - array[i] = (charToByte(upperString.charAt(pos)) << 4) | charToByte(upperString.charAt(pos + 1)) - } - - return array + if (hexString === '') { + return Buffer.from('') + } + + const upperString = hexString.toUpperCase() + const bytesLen = Math.floor(upperString.length / 2) + const array = Buffer.alloc(bytesLen) + + for (let i = 0; i < bytesLen; i++) { + const pos = i * 2 + array[i] = (charToByte(upperString.charAt(pos)) << 4) | charToByte(upperString.charAt(pos + 1)) + } + + return array } /** * Convert hex character to byte - * + * * @param {string} c - Hex character * @returns {number} - Byte value */ function charToByte(c) { - return '0123456789ABCDEF'.indexOf(c) + return '0123456789ABCDEF'.indexOf(c) } /** * Convert byte array to hex string - * + * * @param {Buffer} src - Byte array * @returns {string} - Hex string */ function bytesToHexString(src) { - let s = '' - for (let i = 0; i < src.length; i++) { - const v = src[i] & 0xFF - const hv = v.toString(16) - if (hv.length < 2) { - s += '0' + hv - } else { - s += hv - } + let s = '' + for (let i = 0; i < src.length; i++) { + const v = src[i] & 0xff + const hv = v.toString(16) + if (hv.length < 2) { + s += '0' + hv + } else { + s += hv } - return s + } + return s } /** * Calculate HMAC with key and data - * + * * @param {Buffer} key - Key * @param {Buffer} data - Data * @returns {Buffer} - HMAC result */ function getKeyFromHmac(key, data) { - const hmac = crypto.createHmac('sha256', key) - hmac.update(data) - return hmac.digest() + const hmac = crypto.createHmac('sha256', key) + hmac.update(data) + return hmac.digest() } /** * Calculate SHA256 hash - * + * * @param {Buffer} message - Message * @returns {Buffer} - SHA256 hash */ function getSha256(message) { - const hash = crypto.createHash('sha256') - hash.update(message) - return hash.digest() + const hash = crypto.createHash('sha256') + hash.update(message) + return hash.digest() } /** * Perform XOR operation on two passwords - * + * * @param {Buffer} password1 - First password * @param {Buffer} password2 - Second password * @param {number} length - Length * @returns {Buffer} - XOR result */ function xorBetweenPassword(password1, password2, length) { - const array = Buffer.alloc(length) - for (let i = 0; i < length; i++) { - array[i] = password1[i] ^ password2[i] - } - return array + const array = Buffer.alloc(length) + for (let i = 0; i < length; i++) { + array[i] = password1[i] ^ password2[i] + } + return array } /** * Convert byte array to hex bytes - * + * * @param {Buffer} bytes - Byte array * @returns {Buffer} - Hex bytes */ function bytesToHex(bytes) { - const lookup = Buffer.from('0123456789abcdef') - const result = Buffer.alloc(bytes.length * 2) - let pos = 0 - - for (let i = 0; i < bytes.length; i++) { - const c = bytes[i] & 0xFF - const j = c >> 4 - result[pos] = lookup[j] - pos++ - const k = c & 0xF - result[pos] = lookup[k] - pos++ - } - - return result + const lookup = Buffer.from('0123456789abcdef') + const result = Buffer.alloc(bytes.length * 2) + let pos = 0 + + for (let i = 0; i < bytes.length; i++) { + const c = bytes[i] & 0xff + const j = c >> 4 + result[pos] = lookup[j] + pos++ + const k = c & 0xf + result[pos] = lookup[k] + pos++ + } + + return result } module.exports = { RFC5802Algorithm } diff --git a/packages/pg/lib/crypto/utils-legacy.js b/packages/pg/lib/crypto/utils-legacy.js index e4216337e..9b5819274 100644 --- a/packages/pg/lib/crypto/utils-legacy.js +++ b/packages/pg/lib/crypto/utils-legacy.js @@ -17,8 +17,14 @@ function postgresMd5PasswordHash(user, password, salt) { // See AuthenticationSHA256Password (based on similar approach to MD5) function postgresSha256PasswordHash(user, password, salt) { - const inner = nodeCrypto.createHash('sha256').update(password + user, 'utf-8').digest('hex') - const outer = nodeCrypto.createHash('sha256').update(Buffer.concat([Buffer.from(inner), salt])).digest('hex') + const inner = nodeCrypto + .createHash('sha256') + .update(password + user, 'utf-8') + .digest('hex') + const outer = nodeCrypto + .createHash('sha256') + .update(Buffer.concat([Buffer.from(inner), salt])) + .digest('hex') return 'sha256' + outer } diff --git a/packages/pg/lib/crypto/utils-webcrypto.js b/packages/pg/lib/crypto/utils-webcrypto.js index d9f170e7c..f13b32d08 100644 --- a/packages/pg/lib/crypto/utils-webcrypto.js +++ b/packages/pg/lib/crypto/utils-webcrypto.js @@ -65,8 +65,10 @@ async function postgresSha256PasswordHash(user, password, data) { const ITERATION_SIZE = 4 const dataBuffer = Buffer.from(data) - const passwordStoredMethod = dataBuffer.readInt32BE(PASSWORD_METHOD_OFFSET) - + // Password method is stored at the beginning but not used in this implementation + // We ignore the stored method as we're using SHA256 here + dataBuffer.readInt32BE(PASSWORD_METHOD_OFFSET) + // Extract 64-byte random code starting from offset 4 const randomCode = dataBuffer.slice(PASSWORD_METHOD_SIZE, PASSWORD_METHOD_SIZE + RANDOM_CODE_SIZE).toString('ascii') diff --git a/packages/pg/test/integration/client/sha256-password-tests.js b/packages/pg/test/integration/client/sha256-password-tests.js new file mode 100644 index 000000000..4bf802f77 --- /dev/null +++ b/packages/pg/test/integration/client/sha256-password-tests.js @@ -0,0 +1,102 @@ +'use strict' +const helper = require('./../test-helper') +const pg = helper.pg +const suite = new helper.Suite() +const { native } = helper.args +const assert = require('assert') + +/** + * This test only executes if the env variables SHA256_TEST_PGUSER and + * SHA256_TEST_PGPASSWORD are defined. You can override additional values + * for the host, port and database with other SHA256_TEST_ prefixed vars. + * If the variables are not defined the test will be skipped. + * + * SQL to create test role: + * + * SET password_encryption_type = 2; + * CREATE ROLE sha256_test login password 'test4@scram'; + * + * Add the following entries to pg_hba.conf: + * + * host all sha256_test ::1/128 sha256 + * host all sha256_test 0.0.0.0/0 sha256 + * + * Then run this file with after exporting: + * + * SHA256_TEST_PGUSER=sha256_test + * SHA256_TEST_PGPASSWORD=test4@scram + */ + +// Base config for SHA256 tests +const config = { + user: process.env.SHA256_TEST_PGUSER, + password: process.env.SHA256_TEST_PGPASSWORD, + host: process.env.SHA256_TEST_PGHOST || 'localhost', + port: process.env.SHA256_TEST_PGPORT || 5432, + database: process.env.SHA256_TEST_PGDATABASE || 'ci_db_test', +} + +if (native) { + suite.testAsync('skipping SHA256 tests (on native)', () => {}) + return +} +if (!config.user || !config.password) { + suite.testAsync('skipping SHA256 tests (missing env)', () => {}) + return +} + +suite.testAsync('can connect using sha256 password authentication', async () => { + const client = new pg.Client(config) + let usingSha256 = false + client.connection.once('authenticationSHA256Password', () => { + usingSha256 = true + }) + await client.connect() + assert.ok(usingSha256, 'Should be using SHA256 for authentication') + + // Test basic query execution + const { rows } = await client.query('SELECT NOW()') + assert.strictEqual(rows.length, 1) + + await client.end() +}) + +suite.testAsync('sha256 authentication fails when password is wrong', async () => { + const client = new pg.Client({ + ...config, + password: config.password + 'append-something-to-make-it-bad', + }) + let usingSha256 = false + client.connection.once('authenticationSHA256Password', () => { + usingSha256 = true + }) + await assert.rejects( + () => client.connect(), + { + code: '28P01', + }, + 'Error code should be for a password error' + ) + assert.ok(usingSha256, 'Should be using SHA256 for authentication') +}) + +suite.testAsync('sha256 authentication fails when password is empty', async () => { + const client = new pg.Client({ + ...config, + // use a password function to simulate empty password + password: () => '', + }) + let usingSha256 = false + client.connection.once('authenticationSHA256Password', () => { + usingSha256 = true + }) + await assert.rejects( + () => client.connect(), + (err) => { + // Should fail with authentication error + return err.code === '28P01' || err.message.includes('password') + }, + 'Should fail with password-related error' + ) + assert.ok(usingSha256, 'Should be using SHA256 for authentication') +}) diff --git a/packages/pg/test/test-buffers.js b/packages/pg/test/test-buffers.js index 576d22f7e..3d1ca5945 100644 --- a/packages/pg/test/test-buffers.js +++ b/packages/pg/test/test-buffers.js @@ -23,6 +23,8 @@ buffers.authenticationMD5Password = function () { .join(true, 'R') } +// SASL authentication is no longer supported - commented out +/* buffers.authenticationSASL = function () { return new BufferList().addInt32(10).addCString('SCRAM-SHA-256').addCString('').join(true, 'R') } @@ -34,6 +36,7 @@ buffers.authenticationSASLContinue = function () { buffers.authenticationSASLFinal = function () { return new BufferList().addInt32(12).addString('data').join(true, 'R') } +*/ buffers.parameterStatus = function (name, value) { return new BufferList().addCString(name).addCString(value).join(true, 'S') diff --git a/packages/pg/test/unit/client/sasl-scram-tests.js b/packages/pg/test/unit/client/sasl-scram-tests.js index 07d15f660..60544da4d 100644 --- a/packages/pg/test/unit/client/sasl-scram-tests.js +++ b/packages/pg/test/unit/client/sasl-scram-tests.js @@ -1,3 +1,7 @@ +// SASL authentication is no longer supported in this repository +// All SASL tests have been commented out + +/* 'use strict' const helper = require('./test-helper') const assert = require('assert') @@ -312,3 +316,4 @@ suite.test('sasl/scram', function () { }) }) }) +*/ diff --git a/packages/pg/test/unit/client/sha256-password-tests.js b/packages/pg/test/unit/client/sha256-password-tests.js new file mode 100644 index 000000000..bf256c200 --- /dev/null +++ b/packages/pg/test/unit/client/sha256-password-tests.js @@ -0,0 +1,84 @@ +'use strict' +const helper = require('./test-helper') +const BufferList = require('../../buffer-list') +const crypto = require('../../../lib/crypto/utils') +const assert = require('assert') +const suite = new helper.Suite() +const test = suite.test.bind(suite) + +test('sha256 authentication', async function () { + const client = helper.createClient() + client.password = '!' + // Mock SHA256 authentication data following the format expected by postgresSha256PasswordHash, similar to the MD5 test + // Structure: [4 bytes method][64 bytes random code][8 bytes token][4 bytes iteration] + const data = Buffer.alloc(80) + data.writeInt32BE(1, 0) // password method. in fact this data is not used in the hashing + data.write('A'.repeat(64), 4, 'ascii') // 64-byte random code + data.write('B'.repeat(8), 68, 'ascii') // 8-byte token + data.writeInt32BE(1000, 76) // iteration count + + await client.connection.emit('authenticationSHA256Password', { data: data }) + + setTimeout(() => + test('responds', function () { + assert.lengthIs(client.connection.stream.packets, 1) + test('should have correct encrypted data', async function () { + const hashedPassword = await crypto.postgresSha256PasswordHash(client.user, client.password, data) + assert.equalBuffers( + client.connection.stream.packets[0], + new BufferList().addCString(hashedPassword).join(true, 'p') + ) + }) + }) + ) +}) + +test('sha256 authentication with empty password', async function () { + const client = helper.createClient() + client.password = '' + const data = Buffer.alloc(80) + data.writeInt32BE(1, 0) + data.write('A'.repeat(64), 4, 'ascii') + data.write('B'.repeat(8), 68, 'ascii') + data.writeInt32BE(1000, 76) + + await client.connection.emit('authenticationSHA256Password', { data: data }) + + setTimeout(() => + test('responds with empty password', function () { + assert.lengthIs(client.connection.stream.packets, 1) + test('should have correct encrypted data for empty password', async function () { + const hashedPassword = await crypto.postgresSha256PasswordHash(client.user, client.password, data) + assert.equalBuffers( + client.connection.stream.packets[0], + new BufferList().addCString(hashedPassword).join(true, 'p') + ) + }) + }) + ) +}) + +test('sha256 authentication with utf-8 password', async function () { + const client = helper.createClient() + client.password = 'test_password_123' + const data = Buffer.alloc(80) + data.writeInt32BE(1, 0) + data.write('A'.repeat(64), 4, 'ascii') + data.write('B'.repeat(8), 68, 'ascii') + data.writeInt32BE(1000, 76) + + await client.connection.emit('authenticationSHA256Password', { data: data }) + + setTimeout(() => + test('responds with utf-8 password', function () { + assert.lengthIs(client.connection.stream.packets, 1) + test('should have correct encrypted data for utf-8 password', async function () { + const hashedPassword = await crypto.postgresSha256PasswordHash(client.user, client.password, data) + assert.equalBuffers( + client.connection.stream.packets[0], + new BufferList().addCString(hashedPassword).join(true, 'p') + ) + }) + }) + ) +})