UNPKG

@salesforce/core

Version:

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

500 lines 21.6 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 */ 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.keyChainImpl = exports.GenericWindowsKeychainAccess = exports.GenericUnixKeychainAccess = exports.GenericKeychainAccess = exports.KeychainAccess = void 0; const childProcess = __importStar(require("node:child_process")); const nodeFs = __importStar(require("node:fs")); const fs = __importStar(require("node:fs")); const os = __importStar(require("node:os")); const node_os_1 = require("node:os"); const path = __importStar(require("node:path")); const ts_types_1 = require("@salesforce/ts-types"); const kit_1 = require("@salesforce/kit"); const global_1 = require("../global"); const messages_1 = require("../messages"); ; 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 GET_PASSWORD_RETRY_COUNT = 3; /** * Helper to reduce an array of cli args down to a presentable string for logging. * * @param optionsArray CLI command args. */ const optionsToString = (optionsArray) => optionsArray.join(' '); /** * Helper to determine if a program is executable. Returns `true` if the program is executable for the user. For * Windows true is always returned. * * @param mode Stats mode. * @param gid Unix group id. * @param uid Unix user id. */ const isExe = (mode, gid, uid) => { if (process.platform === 'win32') { return true; } return Boolean(mode & parseInt('0001', 8) || Boolean(mode & parseInt('0010', 8) && process.getgid && gid === process.getgid()) || (mode & parseInt('0100', 8) && process.getuid && uid === process.getuid())); }; /** * Private helper to validate that a program exists on the file system and is executable. * * **Throws** *{@link SfError}{ name: 'MissingCredentialProgramError' }* When the OS credential program isn't found. * * **Throws** *{@link SfError}{ name: 'CredentialProgramAccessError' }* When the OS credential program isn't accessible. * * @param programPath The absolute path of the program. * @param fsIfc The file system interface. * @param isExeIfc Executable validation function. */ // eslint-disable-next-line no-underscore-dangle const _validateProgram = async (programPath, fsIfc, isExeIfc // eslint-disable-next-line @typescript-eslint/require-await ) => { let noPermission; try { const stats = fsIfc.statSync(programPath); noPermission = !isExeIfc(stats.mode, stats.gid, stats.uid); } catch (e) { throw messages.createError('missingCredentialProgramError', [programPath]); } if (noPermission) { throw messages.createError('credentialProgramAccessError', [programPath]); } }; /** * @private */ class KeychainAccess { osImpl; fsIfc; /** * Abstract prototype for general cross platform keychain interaction. * * @param osImpl The platform impl for (linux, darwin, windows). * @param fsIfc The file system interface. */ constructor(osImpl, fsIfc) { this.osImpl = osImpl; this.fsIfc = fsIfc; } /** * Validates the os level program is executable. */ async validateProgram() { await _validateProgram(this.osImpl.getProgram(), this.fsIfc, isExe); } /** * Returns a password using the native program for credential management. * * @param opts Options for the credential lookup. * @param fn Callback function (err, password). * @param retryCount Used internally to track the number of retries for getting a password out of the keychain. */ async getPassword(opts, fn, retryCount = 0) { if (opts.service == null) { fn(messages.createError('keyChainServiceRequiredError')); return; } if (opts.account == null) { fn(messages.createError('keyChainAccountRequiredError')); return; } await this.validateProgram(); const credManager = this.osImpl.getCommandFunc(opts, childProcess.spawn); let stdout = ''; let stderr = ''; if (credManager.stdout) { credManager.stdout.on('data', (data) => { stdout += data; }); } if (credManager.stderr) { credManager.stderr.on('data', (data) => { stderr += data; }); } // eslint-disable-next-line @typescript-eslint/no-misused-promises credManager.on('close', async (code) => { try { return await this.osImpl.onGetCommandClose(code, stdout, stderr, opts, fn); } catch (e) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (e.retry) { if (retryCount >= GET_PASSWORD_RETRY_COUNT) { throw messages.createError('passwordRetryError', [GET_PASSWORD_RETRY_COUNT]); } return this.getPassword(opts, fn, retryCount + 1); } else { // if retry throw e; } } }); if (credManager.stdin) { credManager.stdin.end(); } } /** * Sets a password using the native program for credential management. * * @param opts Options for the credential lookup. * @param fn Callback function (err, ConfigContents). */ async setPassword(opts, fn) { if (opts.service == null) { fn(messages.createError('keyChainServiceRequiredError')); return; } if (opts.account == null) { fn(messages.createError('keyChainAccountRequiredError')); return; } if (opts.password == null) { fn(messages.createError('passwordRequiredError')); return; } await _validateProgram(this.osImpl.getProgram(), this.fsIfc, isExe); const credManager = this.osImpl.setCommandFunc(opts, childProcess.spawn); let stdout = ''; let stderr = ''; if (credManager.stdout) { credManager.stdout.on('data', (data) => { stdout += data; }); } if (credManager.stderr) { credManager.stderr.on('data', (data) => { stderr += data; }); } credManager.on('close', // eslint-disable-next-line @typescript-eslint/no-misused-promises async (code) => this.osImpl.onSetCommandClose(code, stdout, stderr, opts, fn)); if (credManager.stdin) { credManager.stdin.end(); } } } exports.KeychainAccess = KeychainAccess; /** * Linux implementation. * * Uses libsecret. */ const linuxImpl = { getProgram() { return process.env.SFDX_SECRET_TOOL_PATH ?? path.join(path.sep, 'usr', 'bin', 'secret-tool'); }, getProgramOptions(opts) { return ['lookup', 'user', opts.account, 'domain', opts.service]; }, getCommandFunc(opts, fn) { return fn(linuxImpl.getProgram(), linuxImpl.getProgramOptions(opts)); }, // eslint-disable-next-line @typescript-eslint/require-await async onGetCommandClose(code, stdout, stderr, opts, fn) { if (code === 1) { const command = `${linuxImpl.getProgram()} ${optionsToString(linuxImpl.getProgramOptions(opts))}`; const error = messages.createError('passwordNotFoundError', [], [command]); // This is a workaround for linux. // Calling secret-tool too fast can cause it to return an unexpected error. (below) if (stderr?.includes('invalid or unencryptable secret')) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore TODO: make an error subclass with this field error.retry = true; // Throwing here allows us to perform a retry in KeychainAccess throw error; } // All other issues we will report back to the handler. fn(error); } else { fn(null, stdout.trim()); } }, setProgramOptions(opts) { return ['store', "--label='salesforce.com'", 'user', opts.account, 'domain', opts.service]; }, setCommandFunc(opts, fn) { const secretTool = fn(linuxImpl.getProgram(), linuxImpl.setProgramOptions(opts)); if (secretTool.stdin) { secretTool.stdin.write(`${opts.password}\n`); } return secretTool; }, // eslint-disable-next-line @typescript-eslint/require-await async onSetCommandClose(code, stdout, stderr, opts, fn) { if (code !== 0) { const command = `${linuxImpl.getProgram()} ${optionsToString(linuxImpl.setProgramOptions(opts))}`; fn(messages.createError('setCredentialError', [`${stdout} - ${stderr}`], [os.userInfo().username, command])); } else { fn(null); } }, }; /** * OSX implementation. * * /usr/bin/security is a cli front end for OSX keychain. */ const darwinImpl = { getProgram() { return path.join(path.sep, 'usr', 'bin', 'security'); }, getProgramOptions(opts) { return ['find-generic-password', '-a', opts.account, '-s', opts.service, '-g']; }, getCommandFunc(opts, fn) { return fn(darwinImpl.getProgram(), darwinImpl.getProgramOptions(opts)); }, // eslint-disable-next-line @typescript-eslint/require-await async onGetCommandClose(code, stdout, stderr, opts, fn) { let err; if (code !== 0) { switch (code) { case 128: { err = messages.createError('keyChainUserCanceledError'); break; } default: { const command = `${darwinImpl.getProgram()} ${optionsToString(darwinImpl.getProgramOptions(opts))}`; err = messages.createError('passwordNotFoundError', [`${stdout} - ${stderr}`], [command]); } } fn(err); return; } // For better or worse, the last line (containing the actual password) is actually written to stderr instead of // stdout. Reference: http://blog.macromates.com/2006/keychain-access-from-shell/ if (stderr.includes('password')) { const match = RegExp(/"(.*)"/).exec(stderr); if (!match?.[1]) { fn(messages.createError('passwordNotFoundError', [`${stdout} - ${stderr}`])); } else { fn(null, match[1]); } } else { const command = `${darwinImpl.getProgram()} ${optionsToString(darwinImpl.getProgramOptions(opts))}`; fn(messages.createError('passwordNotFoundError', [`${stdout} - ${stderr}`], [command])); } }, setProgramOptions(opts) { const result = ['add-generic-password', '-a', opts.account, '-s', opts.service]; if (opts.password) { result.push('-w', opts.password); } return result; }, setCommandFunc(opts, fn) { return fn(darwinImpl.getProgram(), darwinImpl.setProgramOptions(opts)); }, // eslint-disable-next-line @typescript-eslint/require-await async onSetCommandClose(code, stdout, stderr, opts, fn) { if (code !== 0) { const command = `${darwinImpl.getProgram()} ${optionsToString(darwinImpl.setProgramOptions(opts))}`; fn(messages.createError('setCredentialError', [`${stdout} - ${stderr}`], [os.userInfo().username, command])); } else { fn(null); } }, }; const getSecretFile = () => path.join(global_1.Global.DIR, 'key.json'); var SecretField; (function (SecretField) { SecretField["SERVICE"] = "service"; SecretField["ACCOUNT"] = "account"; SecretField["KEY"] = "key"; })(SecretField || (SecretField = {})); async function writeFile(opts, fn) { try { const contents = { [SecretField.ACCOUNT]: opts.account, [SecretField.KEY]: opts.password, [SecretField.SERVICE]: opts.service, }; const secretFile = getSecretFile(); await fs.promises.mkdir(path.dirname(secretFile), { recursive: true }); await fs.promises.writeFile(secretFile, JSON.stringify(contents, null, 4), { mode: '600' }); fn(null, contents); } catch (err) { fn(err); } } async function readFile() { // The file and access is validated before this method is called const fileContents = (0, kit_1.parseJsonMap)(await fs.promises.readFile(getSecretFile(), 'utf8')); return { account: (0, ts_types_1.ensureString)(fileContents[SecretField.ACCOUNT]), password: (0, ts_types_1.asString)(fileContents[SecretField.KEY]), service: (0, ts_types_1.ensureString)(fileContents[SecretField.SERVICE]), }; } // istanbul ignore next - getPassword/setPassword is always mocked out /** * @@ignore */ class GenericKeychainAccess { async getPassword(opts, fn) { // validate the file in .sfdx await this.isValidFileAccess(async (fileAccessError) => { // the file checks out. if (fileAccessError == null) { // read it's contents try { const { service, account, password } = await readFile(); // validate service name and account just because if (opts.service === service && opts.account === account) { fn(null, password); } else { // if the service and account names don't match then maybe someone or something is editing // that file. #donotallow fn(messages.createError('genericKeychainServiceError', [getSecretFile()])); } } catch (readJsonErr) { fn(readJsonErr); } } else if (fileAccessError.code === 'ENOENT') { fn(messages.createError('passwordNotFoundError')); } else { fn(fileAccessError); } }); } async setPassword(opts, fn) { // validate the file in .sfdx await this.isValidFileAccess(async (fileAccessError) => { // if there is a validation error if (fileAccessError != null) { // file not found if (fileAccessError.code === 'ENOENT') { // create the file await writeFile.call(this, opts, fn); } else { fn(fileAccessError); } } else { // the existing file validated. we can write the updated key await writeFile.call(this, opts, fn); } }); } // eslint-disable-next-line class-methods-use-this async isValidFileAccess(cb) { try { const root = (0, node_os_1.homedir)(); await fs.promises.access(path.join(root, global_1.Global.SFDX_STATE_FOLDER), fs.constants.R_OK | fs.constants.X_OK | fs.constants.W_OK); await cb(null); } catch (err) { await cb(err); } } } exports.GenericKeychainAccess = GenericKeychainAccess; /** * @ignore */ // istanbul ignore next - getPassword/setPassword is always mocked out class GenericUnixKeychainAccess extends GenericKeychainAccess { async isValidFileAccess(cb) { await super.isValidFileAccess(async (err) => { if (err != null) { await cb(err); } else { const secretFile = getSecretFile(); const stats = await fs.promises.stat(secretFile); const octalModeStr = (stats.mode & 0o777).toString(8); const EXPECTED_OCTAL_PERM_VALUE = '600'; if (octalModeStr === EXPECTED_OCTAL_PERM_VALUE) { await cb(null); } else { // eslint-disable-next-line @typescript-eslint/no-floating-promises cb(messages.createError('genericKeychainInvalidPermsError', [secretFile], [secretFile, EXPECTED_OCTAL_PERM_VALUE])); } } }); } } exports.GenericUnixKeychainAccess = GenericUnixKeychainAccess; /** * @ignore */ class GenericWindowsKeychainAccess extends GenericKeychainAccess { async isValidFileAccess(cb) { await super.isValidFileAccess(async (err) => { if (err != null) { await cb(err); } else { try { await fs.promises.access(getSecretFile(), fs.constants.R_OK | fs.constants.W_OK); await cb(null); } catch (e) { await cb(e); } } }); } } exports.GenericWindowsKeychainAccess = GenericWindowsKeychainAccess; /** * @ignore */ exports.keyChainImpl = { // eslint-disable-next-line camelcase generic_unix: new GenericUnixKeychainAccess(), // eslint-disable-next-line camelcase generic_windows: new GenericWindowsKeychainAccess(), darwin: new KeychainAccess(darwinImpl, nodeFs), linux: new KeychainAccess(linuxImpl, nodeFs), validateProgram: _validateProgram, }; //# sourceMappingURL=keyChainImpl.js.map