From b76cc1ee4c48b298701bf460b067172f57b61e03 Mon Sep 17 00:00:00 2001 From: Nazar Kovtun Date: Thu, 14 Aug 2025 15:54:13 +0300 Subject: [PATCH 1/2] HCK-12311: add private key file validation to key pair connection --- common/errorCodes.js | 1 + common/errorMessages.js | 2 ++ .../getKeyPairConnectionErrorMessageByCode.js | 1 + .../connectionSettingsModalConfig.json | 2 +- .../helpers/connections/keyPairConnection.js | 31 +++++++++++++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/common/errorCodes.js b/common/errorCodes.js index 071f4dd0..6ad74822 100644 --- a/common/errorCodes.js +++ b/common/errorCodes.js @@ -2,4 +2,5 @@ module.exports = { ERR_MISSING_PASSPHRASE: 'ERR_MISSING_PASSPHRASE', ERR_OSSL_BAD_DECRYPT: 'ERR_OSSL_BAD_DECRYPT', ERR_INVALID_USERNAME: '390144', + ERR_INVALID_KEY_FILE: 'ERR_INVALID_KEY_FILE', }; diff --git a/common/errorMessages.js b/common/errorMessages.js index f8fe38d2..13e8a06c 100644 --- a/common/errorMessages.js +++ b/common/errorMessages.js @@ -7,4 +7,6 @@ module.exports = { 'Native Okta auth doesn\'t support MFA. Please, use the "Identity Provider SSO (via external browser)" auth instead', KEY_PAIR_PASSPHRASE_ERROR: 'Please check that the passphrase is provided and matches the key.', KEY_PAIR_USERNAME_ERROR: 'Incorrect username was specified.', + KEY_PAIR_INVALID_FILE_ERROR: + "The selected file is not a valid key. Please choose a valid key file, check it's passphrase and try again.", }; diff --git a/common/getKeyPairConnectionErrorMessageByCode.js b/common/getKeyPairConnectionErrorMessageByCode.js index 8c26de0b..b5f553bf 100644 --- a/common/getKeyPairConnectionErrorMessageByCode.js +++ b/common/getKeyPairConnectionErrorMessageByCode.js @@ -5,6 +5,7 @@ const ERROR_CODE_TO_MESSAGE = { [errorCodes.ERR_MISSING_PASSPHRASE]: errorMessages.KEY_PAIR_PASSPHRASE_ERROR, [errorCodes.ERR_OSSL_BAD_DECRYPT]: errorMessages.KEY_PAIR_PASSPHRASE_ERROR, [errorCodes.ERR_INVALID_USERNAME]: errorMessages.KEY_PAIR_USERNAME_ERROR, + [errorCodes.ERR_INVALID_KEY_FILE]: errorMessages.KEY_PAIR_INVALID_FILE_ERROR, }; const getKeyPairConnectionErrorMessageByCode = code => ERROR_CODE_TO_MESSAGE[code]; diff --git a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json index 409224ac..eb2552af 100644 --- a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json +++ b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json @@ -171,7 +171,7 @@ "inputKeyword": "privateKeyPath", "inputType": "file", "inputPlaceholder": "Private Key", - "extensions": ["p8"], + "extensions": ["p8", "pem", "cert", "crt"], "dependency": { "key": "authType", "value": ["keyPair"] diff --git a/reverse_engineering/helpers/connections/keyPairConnection.js b/reverse_engineering/helpers/connections/keyPairConnection.js index 2bd3b278..e8dc0eec 100644 --- a/reverse_engineering/helpers/connections/keyPairConnection.js +++ b/reverse_engineering/helpers/connections/keyPairConnection.js @@ -1,6 +1,20 @@ +const fs = require('fs'); +const crypto = require('crypto'); const { connectWithTimeout } = require('./connection.js'); +const errorCodes = require('../../../common/errorCodes.js'); const authByKeyPair = ({ account, role, timeout, username, privateKeyPath, privateKeyPass }) => { + if ( + !isValidPrivateKey({ + privateKeyPath, + privateKeyPass, + }) + ) { + return Promise.reject({ + code: errorCodes.ERR_INVALID_KEY_FILE, + }); + } + return connectWithTimeout({ account, role, @@ -12,6 +26,23 @@ const authByKeyPair = ({ account, role, timeout, username, privateKeyPath, priva }); }; +const isValidPrivateKey = ({ privateKeyPath, privateKeyPass }) => { + const textFilePrivateKeyFormat = 'pem'; + const fileContent = fs.readFileSync(privateKeyPath, 'utf8'); + + try { + crypto.createPrivateKey({ + key: fileContent, + format: textFilePrivateKeyFormat, + passphrase: privateKeyPass, + }); + + return true; + } catch (error) { + return false; + } +}; + module.exports = { authByKeyPair, }; From 47c664b25a6bee3e6bb29cde684cbce6a45a1a8d Mon Sep 17 00:00:00 2001 From: Nazar Kovtun Date: Thu, 14 Aug 2025 16:19:51 +0300 Subject: [PATCH 2/2] HCK-12311: fixed remarks --- .../helpers/connections/connectionError.js | 12 ++++++++++++ .../helpers/connections/keyPairConnection.js | 12 +++++++----- reverse_engineering/helpers/snowflakeHelper.js | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 reverse_engineering/helpers/connections/connectionError.js diff --git a/reverse_engineering/helpers/connections/connectionError.js b/reverse_engineering/helpers/connections/connectionError.js new file mode 100644 index 00000000..46eeada0 --- /dev/null +++ b/reverse_engineering/helpers/connections/connectionError.js @@ -0,0 +1,12 @@ +class ConnectionError extends Error { + code; + + constructor(message, code) { + super(message); + this.code = code; + } +} + +module.exports = { + ConnectionError, +}; diff --git a/reverse_engineering/helpers/connections/keyPairConnection.js b/reverse_engineering/helpers/connections/keyPairConnection.js index e8dc0eec..45df4f65 100644 --- a/reverse_engineering/helpers/connections/keyPairConnection.js +++ b/reverse_engineering/helpers/connections/keyPairConnection.js @@ -2,17 +2,18 @@ const fs = require('fs'); const crypto = require('crypto'); const { connectWithTimeout } = require('./connection.js'); const errorCodes = require('../../../common/errorCodes.js'); +const errorMessages = require('../../../common/errorMessages.js'); +const { ConnectionError } = require('./connectionError.js'); -const authByKeyPair = ({ account, role, timeout, username, privateKeyPath, privateKeyPass }) => { +const authByKeyPair = ({ account, role, timeout, username, privateKeyPath, privateKeyPass, logger }) => { if ( !isValidPrivateKey({ privateKeyPath, privateKeyPass, + logger, }) ) { - return Promise.reject({ - code: errorCodes.ERR_INVALID_KEY_FILE, - }); + throw new ConnectionError(errorMessages.KEY_PAIR_INVALID_FILE_ERROR, errorCodes.ERR_INVALID_KEY_FILE); } return connectWithTimeout({ @@ -26,7 +27,7 @@ const authByKeyPair = ({ account, role, timeout, username, privateKeyPath, priva }); }; -const isValidPrivateKey = ({ privateKeyPath, privateKeyPass }) => { +const isValidPrivateKey = ({ privateKeyPath, privateKeyPass, logger }) => { const textFilePrivateKeyFormat = 'pem'; const fileContent = fs.readFileSync(privateKeyPath, 'utf8'); @@ -39,6 +40,7 @@ const isValidPrivateKey = ({ privateKeyPath, privateKeyPass }) => { return true; } catch (error) { + logger.log('error', { error }, 'Connection error'); return false; } }; diff --git a/reverse_engineering/helpers/snowflakeHelper.js b/reverse_engineering/helpers/snowflakeHelper.js index ca51d265..f611800b 100644 --- a/reverse_engineering/helpers/snowflakeHelper.js +++ b/reverse_engineering/helpers/snowflakeHelper.js @@ -129,7 +129,7 @@ const connect = async ( logger, }); } else if (authType === 'keyPair') { - authPromise = authByKeyPair({ account, role, timeout, username, privateKeyPath, privateKeyPass }); + authPromise = authByKeyPair({ account, role, timeout, username, privateKeyPath, privateKeyPass, logger }); } else { authPromise = authByCredentials({ account, username, password, role, warehouse, timeout }); }