UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

377 lines 17.5 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ /* eslint-disable @typescript-eslint/ban-types */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Crypto = void 0; const crypto = __importStar(require("node:crypto")); const os = __importStar(require("node:os")); const node_path_1 = require("node:path"); const ts_types_1 = require("@salesforce/ts-types"); const kit_1 = require("@salesforce/kit"); const logger_1 = require("../logger/logger"); const lifecycleEvents_1 = require("../lifecycleEvents"); const messages_1 = require("../messages"); const cache_1 = require("../util/cache"); const global_1 = require("../global"); const sfError_1 = require("../sfError"); const keyChain_1 = require("./keyChain"); const secureBuffer_1 = require("./secureBuffer"); const TAG_DELIMITER = ':'; const IV_BYTES = { v1: 6, v2: 12, }; const ENCODING = { v1: 'utf8', v2: 'hex', }; const KEY_SIZE = { v1: 16, v2: 32, }; const ALGO = 'aes-256-gcm'; const AUTH_TAG_LENGTH = 32; const ENCRYPTED_CHARS = /[a-f0-9]/; const KEY_NAME = 'sfdx'; const ACCOUNT = 'local'; let cryptoLogger; const getCryptoLogger = () => { cryptoLogger ??= logger_1.Logger.childFromRoot('crypto'); return cryptoLogger; }; const getCryptoV2EnvVar = () => { let sfCryptoV2 = process.env.SF_CRYPTO_V2?.toLowerCase(); if (sfCryptoV2 !== undefined) { getCryptoLogger().debug(`SF_CRYPTO_V2=${sfCryptoV2}`); // normalize all values that aren't "true" to be "false" if (sfCryptoV2 !== 'true') { sfCryptoV2 = 'false'; } } return sfCryptoV2; }; let cryptoVersion; const getCryptoVersion = () => { if (!cryptoVersion) { // This only happens when generating a new key, so use the env var // and (for now) default to 'v1'. cryptoVersion = getCryptoV2EnvVar() === 'true' ? 'v2' : 'v1'; } return cryptoVersion; }; // Detect the crypto version based on the password (key) length. // This happens once per process. const detectCryptoVersion = (pwd) => { if (!cryptoVersion) { // check the env var to see if it's set const sfCryptoV2 = getCryptoV2EnvVar(); // Password length of 64 is v2 crypto and uses hex encoding. // Password length of 32 is v1 crypto and uses utf8 encoding. if (pwd?.length === KEY_SIZE.v2 * 2) { cryptoVersion = 'v2'; getCryptoLogger().debug('Using v2 crypto'); if (sfCryptoV2 === 'false') { getCryptoLogger().warn(messages.getMessage('v1CryptoWithV2KeyWarning')); } } else if (pwd?.length === KEY_SIZE.v1 * 2) { cryptoVersion = 'v1'; getCryptoLogger().debug('Using v1 crypto'); if (sfCryptoV2 === 'true') { getCryptoLogger().warn(messages.getMessage('v2CryptoWithV1KeyWarning')); } } else { getCryptoLogger().debug("crypto key doesn't match v1 or v2. using SF_CRYPTO_V2."); getCryptoVersion(); } void lifecycleEvents_1.Lifecycle.getInstance().emitTelemetry({ eventName: 'crypto_version', library: 'sfdx-core', function: 'detectCryptoVersion', cryptoVersion, // 'v1' or 'v2' cryptoEnvVar: sfCryptoV2, // 'true' or 'false' or 'undefined' }); } }; ; const messages = new messages_1.Messages('@salesforce/core', 'encryption', new Map([["invalidEncryptedFormatError", "The encrypted data is not properly formatted."], ["invalidEncryptedFormatError.actions", ["If attempting to create a scratch org then re-authorize. Otherwise create a new scratch org."]], ["authDecryptError", "Failed to decipher auth data. reason: %s."], ["unsupportedOperatingSystemError", "Unsupported Operating System: %s"], ["missingCredentialProgramError", "Unable to find required security software: %s"], ["credentialProgramAccessError", "Unable to execute security software: %s"], ["passwordRetryError", "Failed to get the password after %i retries."], ["passwordRequiredError", "A password is required."], ["keyChainServiceRequiredError", "Unable to get or set a keychain value without a service name."], ["keyChainAccountRequiredError", "Unable to get or set a keychain value without an account name."], ["keyChainUserCanceledError", "User canceled authentication."], ["keychainPasswordCreationError", "Failed to create a password in the keychain."], ["genericKeychainServiceError", "The service and account specified in %s do not match the version of the toolbelt."], ["genericKeychainServiceError.actions", ["Check your toolbelt version and re-auth."]], ["genericKeychainInvalidPermsError", "Invalid file permissions for secret file: %s"], ["genericKeychainInvalidPermsError.actions", ["Ensure the file %s has the file permission octal value of %s."]], ["passwordNotFoundError", "Could not find password.\n%s"], ["passwordNotFoundError.actions", ["Ensure a valid password is returned with the following command: [%s]"]], ["setCredentialError", "Command failed with response:\n%s"], ["setCredentialError.actions", ["Determine why this command failed to set an encryption key for user %s: [%s]."]], ["macKeychainOutOfSync", "We\u2019ve encountered an error with the Mac keychain being out of sync with your `sfdx` credentials. To fix the problem, sync your credentials by authenticating into your org again using the auth commands."], ["v1CryptoWithV2KeyWarning", "The SF_CRYPTO_V2 environment variable was set to \"false\" but a v2 crypto key was detected. v1 crypto can only be used with a v1 key. Unset the SF_CRYPTO_V2 environment variable."], ["v2CryptoWithV1KeyWarning", "SF_CRYPTO_V2 was set to \"true\" but a v1 crypto key was detected. v2 crypto can only be used with a v2 key. To generate a v2 key:\n\n1. Logout of all orgs: `sf org logout --all`\n2. Delete the sfdx keychain entry (account: local, service: sfdx). If `SF_USE_GENERIC_UNIX_KEYCHAIN=true` env var is set, you can delete the `key.json` file.\n3. Set `SF_CRYPTO_V2=true` env var.\n4. Re-Authenticate with your orgs using the CLI org login commands."]])); const makeSecureBuffer = (password, encoding) => { const newSb = new secureBuffer_1.SecureBuffer(); newSb.consume(Buffer.from(password, encoding)); return newSb; }; /** * osxKeyChain promise wrapper. */ const keychainPromises = { /** * Gets a password item. * * @param _keychain * @param service The keychain service name. * @param account The keychain account name. */ getPassword(_keychain, service, account) { const cacheKey = `${global_1.Global.DIR}:${service}:${account}`; const sb = cache_1.Cache.get(cacheKey); if (!sb) { return new Promise((resolve, reject) => _keychain.getPassword({ service, account }, (err, password) => { if (err) return reject(err); const pwd = (0, ts_types_1.ensure)(password, 'Expected the keychain password to be set'); detectCryptoVersion(pwd); cache_1.Cache.set(cacheKey, makeSecureBuffer(pwd, ENCODING[getCryptoVersion()])); return resolve({ username: account, password: pwd }); })); } else { // If the password is cached, we know the crypto version and encoding because it was // detected by the non-cache code path just above this. const encoding = ENCODING[getCryptoVersion()]; const pwd = (0, ts_types_1.ensure)(sb.value((buffer) => buffer.toString(encoding)), 'Expected the keychain password to be set'); cache_1.Cache.set(cacheKey, makeSecureBuffer(pwd, encoding)); return new Promise((resolve) => resolve({ username: account, password: pwd })); } }, /** * Sets a generic password item in OSX keychain. * * @param _keychain * @param service The keychain service name. * @param account The keychain account name. * @param password The password for the keychain item. */ setPassword(_keychain, service, account, password) { return new Promise((resolve, reject) => _keychain.setPassword({ service, account, password }, (err) => { if (err) return reject(err); return resolve({ username: account, password }); })); }, }; /** * Class for managing encrypting and decrypting private auth information. */ class Crypto extends kit_1.AsyncOptionalCreatable { key = new secureBuffer_1.SecureBuffer(); options; noResetOnClose; /** * Constructor * **Do not directly construct instances of this class -- use {@link Crypto.create} instead.** * * @param options The options for the class instance. * @ignore */ constructor(options) { super(options); this.options = options ?? {}; } // @ts-expect-error only for test access // eslint-disable-next-line class-methods-use-this static unsetCryptoVersion() { cryptoVersion = undefined; } encrypt(text) { if (text == null) { return; } if (this.key == null) { throw messages.createError('keychainPasswordCreationError'); } // When everything is v2, we can remove the else if (this.isV2Crypto()) { return this.encryptV2(text); } else { return this.encryptV1(text); } } decrypt(text) { if (text == null) { return; } const tokens = text.split(TAG_DELIMITER); if (tokens.length !== 2) { throw messages.createError('invalidEncryptedFormatError'); } // When everything is v2, we can remove the else if (this.isV2Crypto()) { return this.decryptV2(tokens); } else { return this.decryptV1(tokens); } } /** * Takes a best guess if the value provided was encrypted by {@link Crypto.encrypt} by * checking the delimiter, tag length, and valid characters. * * @param text The text * @returns true if the text is encrypted, false otherwise. */ // eslint-disable-next-line class-methods-use-this isEncrypted(text) { if (text == null) { return false; } const tokens = text.split(TAG_DELIMITER); if (tokens.length !== 2) { return false; } const tag = tokens[1]; const value = tokens[0]; return (tag.length === AUTH_TAG_LENGTH && value.length >= IV_BYTES[getCryptoVersion()] && ENCRYPTED_CHARS.test(tag) && ENCRYPTED_CHARS.test(tokens[0])); } /** * Clears the crypto state. This should be called in a finally block. */ close() { if (!this.noResetOnClose) { this.key.clear(); } } // eslint-disable-next-line class-methods-use-this isV2Crypto() { return getCryptoVersion() === 'v2'; } /** * Initialize async components. */ async init() { if (!this.options.platform) { this.options.platform = os.platform(); } this.noResetOnClose = !!this.options.noResetOnClose; try { const keyChain = await this.getKeyChain(this.options.platform); const pwd = (await keychainPromises.getPassword(keyChain, KEY_NAME, ACCOUNT)).password; // The above line ensures the crypto version is detected and set so we can rely on it now. this.key.consume(Buffer.from(pwd, ENCODING[getCryptoVersion()])); } catch (err) { // No password found if (err.name === 'PasswordNotFoundError') { // If we already tried to create a new key then bail. if (this.options.retryStatus === 'KEY_SET') { getCryptoLogger().debug('a key was set but the retry to get the password failed.'); throw err; } else { getCryptoLogger().debug(`password not found in keychain. Creating new one (Crypto ${getCryptoVersion()}) and re-init.`); } // 2/6/2024: This generates a new key using the crypto version based on the SF_CRYPTO_V2 env var. // Sometime in the future we could hardcode this to be `KEY_SIZE.v2` so that it becomes the default. const key = crypto.randomBytes(KEY_SIZE[getCryptoVersion()]).toString('hex'); // Set the new password in the KeyChain. await keychainPromises.setPassword((0, ts_types_1.ensure)(this.options.keychain), KEY_NAME, ACCOUNT, key); return this.init(); } else { throw err; } } } encryptV1(text) { const iv = crypto.randomBytes(IV_BYTES.v1).toString('hex'); return this.key.value((buffer) => { const cipher = crypto.createCipheriv(ALGO, buffer.toString('utf8'), iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const tag = cipher.getAuthTag().toString('hex'); return `${iv}${encrypted}${TAG_DELIMITER}${tag}`; }); } encryptV2(text) { const iv = crypto.randomBytes(IV_BYTES.v2); return this.key.value((buffer) => { const cipher = crypto.createCipheriv(ALGO, buffer, iv); const ivHex = iv.toString('hex'); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const tag = cipher.getAuthTag().toString('hex'); return `${ivHex}${encrypted}${TAG_DELIMITER}${tag}`; }); } decryptV1(tokens) { const tag = tokens[1]; const iv = tokens[0].substring(0, IV_BYTES.v1 * 2); const secret = tokens[0].substring(IV_BYTES.v1 * 2, tokens[0].length); return this.key.value((buffer) => { const decipher = crypto.createDecipheriv(ALGO, buffer.toString('utf8'), iv); try { decipher.setAuthTag(Buffer.from(tag, 'hex')); return `${decipher.update(secret, 'hex', 'utf8')}${decipher.final('utf8')}`; } catch (err) { const error = messages.createError('authDecryptError', [err.message], [], err); const useGenericUnixKeychain = kit_1.env.getBoolean('SF_USE_GENERIC_UNIX_KEYCHAIN') || kit_1.env.getBoolean('USE_GENERIC_UNIX_KEYCHAIN'); if (os.platform() === 'darwin' && !useGenericUnixKeychain) { error.actions = [messages.getMessage('macKeychainOutOfSync')]; } throw error; } }); } decryptV2(tokens) { const tag = tokens[1]; const iv = tokens[0].substring(0, IV_BYTES.v2 * 2); const secret = tokens[0].substring(IV_BYTES.v2 * 2, tokens[0].length); return this.key.value((buffer) => { const decipher = crypto.createDecipheriv(ALGO, buffer, Buffer.from(iv, 'hex')); try { decipher.setAuthTag(Buffer.from(tag, 'hex')); return `${decipher.update(secret, 'hex', 'utf8')}${decipher.final('utf8')}`; } catch (_err) { const err = ((0, ts_types_1.isString)(_err) ? sfError_1.SfError.wrap(_err) : _err); const error = messages.createError('authDecryptError', [err.message], [], err); const useGenericUnixKeychain = kit_1.env.getBoolean('SF_USE_GENERIC_UNIX_KEYCHAIN') || kit_1.env.getBoolean('USE_GENERIC_UNIX_KEYCHAIN'); if (os.platform() === 'darwin' && !useGenericUnixKeychain) { error.actions = [messages.getMessage('macKeychainOutOfSync')]; } throw error; } }); } async getKeyChain(platform) { if (!this.options.keychain) { this.options.keychain = await (0, keyChain_1.retrieveKeychain)(platform); } return this.options.keychain; } } exports.Crypto = Crypto; //# sourceMappingURL=crypto.js.map