mirror of
https://github.com/mariadb-corporation/mariadb-connector-nodejs.git
synced 2025-07-25 16:02:44 +00:00
336 lines
12 KiB
JavaScript
336 lines
12 KiB
JavaScript
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
// Copyright (c) 2015-2025 MariaDB Corporation Ab
|
|
|
|
'use strict';
|
|
|
|
const Command = require('../command');
|
|
const Errors = require('../../misc/errors');
|
|
const Capabilities = require('../../const/capabilities');
|
|
const Handshake = require('./auth/handshake');
|
|
const ServerStatus = require('../../const/server-status');
|
|
const StateChange = require('../../const/state-change');
|
|
const Collations = require('../../const/collations');
|
|
const Crypto = require('crypto');
|
|
const utils = require('../../misc/utils');
|
|
const tls = require('tls');
|
|
const authenticationPlugins = {
|
|
mysql_native_password: require('./auth/native-password-auth.js'),
|
|
mysql_clear_password: require('./auth/clear-password-auth'),
|
|
client_ed25519: require('./auth/ed25519-password-auth'),
|
|
parsec: require('./auth/parsec-auth'),
|
|
dialog: require('./auth/pam-password-auth'),
|
|
sha256_password: require('./auth/sha256-password-auth'),
|
|
caching_sha2_password: require('./auth/caching-sha2-password-auth')
|
|
};
|
|
|
|
/**
|
|
* Handle handshake.
|
|
* see https://mariadb.com/kb/en/library/1-connecting-connecting/
|
|
*/
|
|
class Authentication extends Command {
|
|
constructor(cmdParam, resolve, reject, _createSecureContext, getSocket) {
|
|
super(cmdParam, resolve, reject);
|
|
this.cmdParam = cmdParam;
|
|
this._createSecureContext = _createSecureContext;
|
|
this.getSocket = getSocket;
|
|
this.plugin = new Handshake(this, getSocket, this.handshakeResult, reject);
|
|
}
|
|
|
|
onPacketReceive(packet, out, opts, info) {
|
|
this.plugin.sequenceNo = this.sequenceNo;
|
|
this.plugin.compressSequenceNo = this.compressSequenceNo;
|
|
this.plugin.onPacketReceive(packet, out, opts, info);
|
|
}
|
|
|
|
/**
|
|
* Fast-path handshake results :
|
|
* - if plugin was the one expected by server, server will send OK_Packet / ERR_Packet.
|
|
* - if not, server send an AuthSwitchRequest packet, indicating the specific PLUGIN to use with this user.
|
|
* dispatching to plugin handler then.
|
|
*
|
|
* @param packet current packet
|
|
* @param out output buffer
|
|
* @param opts options
|
|
* @param info connection info
|
|
* @returns {*} return null if authentication succeed, depending on plugin conversation if not finished
|
|
*/
|
|
handshakeResult(packet, out, opts, info) {
|
|
const marker = packet.peek();
|
|
switch (marker) {
|
|
//*********************************************************************************************************
|
|
//* AuthSwitchRequest packet
|
|
//*********************************************************************************************************
|
|
case 0xfe:
|
|
this.dispatchAuthSwitchRequest(packet, out, opts, info);
|
|
return;
|
|
|
|
//*********************************************************************************************************
|
|
//* OK_Packet - authentication succeeded
|
|
//*********************************************************************************************************
|
|
case 0x00:
|
|
this.plugin.onPacketReceive = null;
|
|
packet.skip(1); //skip header
|
|
packet.skipLengthCodedNumber(); //skip affected rows
|
|
packet.skipLengthCodedNumber(); //skip last insert id
|
|
info.status = packet.readUInt16();
|
|
|
|
if (info.requireValidCert) {
|
|
if (info.selfSignedCertificate) {
|
|
// TLS was forced to trust, and certificate validation is required
|
|
packet.skip(2); //skip warning count
|
|
if (packet.remaining()) {
|
|
const validationHash = packet.readBufferLengthEncoded();
|
|
if (validationHash.length > 0) {
|
|
if (!this.plugin.permitHash() || !Boolean(this.cmdParam.opts.password)) {
|
|
return this.throwNewError(
|
|
'Self signed certificates. Either set `ssl: { rejectUnauthorized: false }` (trust mode) or provide server certificate to client',
|
|
true,
|
|
info,
|
|
'08000',
|
|
Errors.ER_SELF_SIGNED_NO_PWD
|
|
);
|
|
}
|
|
if (this.validateFingerPrint(validationHash, info)) {
|
|
return this.successEnd();
|
|
}
|
|
}
|
|
}
|
|
return this.throwNewError('self-signed certificate', true, info, '08000', Errors.ER_SELF_SIGNED);
|
|
} else {
|
|
// certificate is not self signed, validate server identity
|
|
const validationFunction =
|
|
opts.ssl === true || typeof opts.ssl.checkServerIdentity !== 'function'
|
|
? tls.checkServerIdentity
|
|
: opts.ssl.checkServerIdentity;
|
|
const identityError = validationFunction(
|
|
typeof opts.ssl === 'object' && opts.ssl.servername ? opts.ssl.servername : opts.host,
|
|
info.tlsCert
|
|
);
|
|
if (identityError) {
|
|
return this.throwNewError(
|
|
'certificate identify Error: ' + identityError.message,
|
|
true,
|
|
info,
|
|
'08000',
|
|
Errors.ER_TLS_IDENTITY_ERROR
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mustRedirect = false;
|
|
if (info.status & ServerStatus.SESSION_STATE_CHANGED) {
|
|
packet.skip(2); //skip warning count
|
|
packet.skipLengthCodedNumber();
|
|
while (packet.remaining()) {
|
|
const len = packet.readUnsignedLength();
|
|
if (len > 0) {
|
|
const subPacket = packet.subPacketLengthEncoded(len);
|
|
while (subPacket.remaining()) {
|
|
const type = subPacket.readUInt8();
|
|
switch (type) {
|
|
case StateChange.SESSION_TRACK_SYSTEM_VARIABLES:
|
|
let subSubPacket;
|
|
do {
|
|
subSubPacket = subPacket.subPacketLengthEncoded(subPacket.readUnsignedLength());
|
|
const variable = subSubPacket.readStringLengthEncoded();
|
|
const value = subSubPacket.readStringLengthEncoded();
|
|
|
|
switch (variable) {
|
|
case 'character_set_client':
|
|
info.collation = Collations.fromCharset(value);
|
|
if (info.collation === undefined) {
|
|
this.throwError(new Error("unknown charset : '" + value + "'"), info);
|
|
return;
|
|
}
|
|
opts.emit('collation', info.collation);
|
|
break;
|
|
|
|
case 'redirect_url':
|
|
if (value !== '') {
|
|
mustRedirect = true;
|
|
info.redirect(value, this.successEnd.bind(this));
|
|
}
|
|
break;
|
|
|
|
case 'maxscale':
|
|
info.maxscaleVersion = value;
|
|
break;
|
|
|
|
case 'connection_id':
|
|
info.threadId = parseInt(value);
|
|
break;
|
|
|
|
default:
|
|
//variable not used by driver
|
|
}
|
|
} while (subSubPacket.remaining() > 0);
|
|
break;
|
|
|
|
case StateChange.SESSION_TRACK_SCHEMA:
|
|
const subSubPacket2 = subPacket.subPacketLengthEncoded(subPacket.readUnsignedLength());
|
|
info.database = subSubPacket2.readStringLengthEncoded();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!mustRedirect) this.successEnd();
|
|
return;
|
|
|
|
//*********************************************************************************************************
|
|
//* ERR_Packet
|
|
//*********************************************************************************************************
|
|
case 0xff:
|
|
this.plugin.onPacketReceive = null;
|
|
const authErr = packet.readError(info, this.displaySql(), undefined);
|
|
authErr.fatal = true;
|
|
if (info.requireValidCert && info.selfSignedCertificate) {
|
|
// TLS was forced to trust, and certificate validation is required
|
|
return this.plugin.throwNewError(
|
|
'Self signed certificates. Either set `ssl: { rejectUnauthorized: false }` (trust mode) or provide server certificate to client',
|
|
true,
|
|
info,
|
|
'08000',
|
|
Errors.ER_SELF_SIGNED_NO_PWD
|
|
);
|
|
}
|
|
return this.plugin.throwError(authErr, info);
|
|
|
|
//*********************************************************************************************************
|
|
//* unexpected
|
|
//*********************************************************************************************************
|
|
default:
|
|
this.throwNewError(
|
|
`Unexpected type of packet during handshake phase : ${marker}`,
|
|
true,
|
|
info,
|
|
'42000',
|
|
Errors.ER_AUTHENTICATION_BAD_PACKET
|
|
);
|
|
}
|
|
}
|
|
|
|
validateFingerPrint(validationHash, info) {
|
|
if (validationHash.length === 0 || !info.tlsFingerprint) return false;
|
|
|
|
// 0x01 = SHA256 encryption
|
|
if (validationHash[0] !== 0x01) {
|
|
const err = Errors.createFatalError(
|
|
`Unexpected hash format for fingerprint hash encoding`,
|
|
Errors.ER_UNEXPECTED_PACKET,
|
|
this.info
|
|
);
|
|
if (this.opts.logger.error) this.opts.logger.error(err);
|
|
return false;
|
|
}
|
|
|
|
const pwdHash = this.plugin.hash(this.cmdParam.opts);
|
|
|
|
let hash = Crypto.createHash('sha256');
|
|
let digest = hash.update(pwdHash).update(info.seed).update(Buffer.from(info.tlsFingerprint, 'hex')).digest();
|
|
const hashHex = utils.toHexString(digest);
|
|
const serverValidationHex = validationHash.toString('ascii', 1, validationHash.length).toLowerCase();
|
|
return hashHex === serverValidationHex;
|
|
}
|
|
|
|
/**
|
|
* Handle authentication switch request : dispatch to plugin handler.
|
|
*
|
|
* @param packet packet
|
|
* @param out output writer
|
|
* @param opts options
|
|
* @param info connection information
|
|
*/
|
|
dispatchAuthSwitchRequest(packet, out, opts, info) {
|
|
let pluginName, pluginData;
|
|
if (info.clientCapabilities & Capabilities.PLUGIN_AUTH) {
|
|
packet.skip(1); //header
|
|
if (packet.remaining()) {
|
|
//AuthSwitchRequest packet.
|
|
pluginName = packet.readStringNullEnded();
|
|
pluginData = packet.readBufferRemaining();
|
|
} else {
|
|
//OldAuthSwitchRequest
|
|
pluginName = 'mysql_old_password';
|
|
pluginData = info.seed.subarray(0, 8);
|
|
}
|
|
} else {
|
|
pluginName = packet.readStringNullEnded('ascii');
|
|
pluginData = packet.readBufferRemaining();
|
|
}
|
|
|
|
if (
|
|
info.requireValidCert &&
|
|
info.selfSignedCertificate &&
|
|
Boolean(this.cmdParam.opts.password) &&
|
|
!this.plugin.permitHash()
|
|
) {
|
|
return this.throwNewError(
|
|
`Unsupported authentication plugin ${pluginName} with Self signed certificates. Either set 'ssl: { rejectUnauthorized: false }' (trust mode) or provide server certificate to client`,
|
|
true,
|
|
info,
|
|
'08000',
|
|
Errors.ER_SELF_SIGNED_BAD_PLUGIN
|
|
);
|
|
}
|
|
|
|
if (opts.restrictedAuth && !opts.restrictedAuth.includes(pluginName)) {
|
|
this.throwNewError(
|
|
`Unsupported authentication plugin ${pluginName}. Authorized plugin: ${opts.restrictedAuth.toString()}`,
|
|
true,
|
|
info,
|
|
'42000',
|
|
Errors.ER_NOT_SUPPORTED_AUTH_PLUGIN
|
|
);
|
|
return;
|
|
}
|
|
try {
|
|
this.plugin.emit('end');
|
|
this.plugin.onPacketReceive = null;
|
|
this.plugin = Authentication.pluginHandler(
|
|
pluginName,
|
|
this.plugin.sequenceNo,
|
|
this.plugin.compressSequenceNo,
|
|
pluginData,
|
|
info,
|
|
opts,
|
|
out,
|
|
this.cmdParam,
|
|
this.reject,
|
|
this.handshakeResult.bind(this)
|
|
);
|
|
this.plugin.start(out, opts, info);
|
|
} catch (err) {
|
|
this.reject(err);
|
|
}
|
|
}
|
|
|
|
static pluginHandler(
|
|
pluginName,
|
|
packSeq,
|
|
compressPackSeq,
|
|
pluginData,
|
|
info,
|
|
opts,
|
|
out,
|
|
cmdParam,
|
|
authReject,
|
|
multiAuthResolver
|
|
) {
|
|
let pluginAuth = authenticationPlugins[pluginName];
|
|
if (!pluginAuth) {
|
|
throw Errors.createFatalError(
|
|
`Client does not support authentication protocol '${pluginName}' requested by server.`,
|
|
Errors.ER_AUTHENTICATION_PLUGIN_NOT_SUPPORTED,
|
|
info,
|
|
'08004'
|
|
);
|
|
}
|
|
return new pluginAuth(packSeq, compressPackSeq, pluginData, cmdParam, authReject, multiAuthResolver);
|
|
}
|
|
}
|
|
|
|
module.exports = Authentication;
|