@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
464 lines • 19.1 kB
JavaScript
;
/*
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.keyChainImpl = exports.GenericWindowsKeychainAccess = exports.GenericUnixKeychainAccess = exports.GenericKeychainAccess = exports.KeychainAccess = void 0;
const childProcess = require("child_process");
const nodeFs = require("fs");
const os = require("os");
const path = require("path");
const ts_types_1 = require("@salesforce/ts-types");
const configFile_1 = require("./config/configFile");
const keychainConfig_1 = require("./config/keychainConfig");
const global_1 = require("./global");
const sfdxError_1 = require("./sfdxError");
const fs_1 = require("./util/fs");
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.
*/
function _optionsToString(optionsArray) {
return optionsArray.reduce((accum, element) => `${accum} ${element}`);
}
/**
* 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) ||
(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 SfdxError}{ name: 'MissingCredentialProgramError' }* When the OS credential program isn't found.
*
* **Throws** *{@link SfdxError}{ 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.
*/
const _validateProgram = async (programPath, fsIfc, isExeIfc) => {
let noPermission;
try {
const stats = fsIfc.statSync(programPath);
noPermission = !isExeIfc(stats.mode, stats.gid, stats.uid);
}
catch (e) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'MissingCredentialProgramError', [programPath]);
}
if (noPermission) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'CredentialProgramAccessError', [programPath]);
}
};
/**
* @private
*/
class KeychainAccess {
/**
* 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(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainServiceRequiredError'));
return;
}
if (opts.account == null) {
fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', '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) {
if (e.retry) {
if (retryCount >= GET_PASSWORD_RETRY_COUNT) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', '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(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainServiceRequiredError'));
return;
}
if (opts.account == null) {
fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainAccountRequiredError'));
return;
}
if (opts.password == null) {
fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', '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) => await 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));
},
async onGetCommandClose(code, stdout, stderr, opts, fn) {
if (code === 1) {
const command = `${_linuxImpl.getProgram()} ${_optionsToString(_linuxImpl.getProgramOptions(opts))}`;
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
const error = sfdxError_1.SfdxError.create(errorConfig);
// This is a workaround for linux.
// Calling secret-tool too fast can cause it to return an unexpected error. (below)
if (stderr != null && stderr.includes('invalid or unencryptable secret')) {
// @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;
},
async onSetCommandClose(code, stdout, stderr, opts, fn) {
if (code !== 0) {
const command = `${_linuxImpl.getProgram()} ${_optionsToString(_linuxImpl.setProgramOptions(opts))}`;
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'SetCredentialError', [`\n${stdout} - ${stderr}`], 'SetCredentialErrorAction', [os.userInfo().username, command]);
fn(sfdxError_1.SfdxError.create(errorConfig));
}
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));
},
async onGetCommandClose(code, stdout, stderr, opts, fn) {
let err;
if (code !== 0) {
switch (code) {
case 128: {
err = sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainUserCanceledError');
break;
}
default: {
const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.getProgramOptions(opts))}`;
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
err = sfdxError_1.SfdxError.create(errorConfig);
}
}
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 || !match[1]) {
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction');
fn(sfdxError_1.SfdxError.create(errorConfig));
}
else {
fn(null, match[1]);
}
}
else {
const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.getProgramOptions(opts))}`;
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
fn(sfdxError_1.SfdxError.create(errorConfig));
}
},
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));
},
async onSetCommandClose(code, stdout, stderr, opts, fn) {
if (code !== 0) {
const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.setProgramOptions(opts))}`;
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'SetCredentialError', [`\n${stdout} - ${stderr}`], 'SetCredentialErrorAction', [os.userInfo().username, command]);
fn(sfdxError_1.SfdxError.create(errorConfig));
}
else {
fn(null);
}
},
};
async function _writeFile(opts, fn) {
try {
const config = await keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions());
config.set(SecretField.ACCOUNT, opts.account);
config.set(SecretField.KEY, opts.password || '');
config.set(SecretField.SERVICE, opts.service);
await config.write();
fn(null, config.getContents());
}
catch (err) {
fn(err);
}
}
var SecretField;
(function (SecretField) {
SecretField["SERVICE"] = "service";
SecretField["ACCOUNT"] = "account";
SecretField["KEY"] = "key";
})(SecretField || (SecretField = {}));
// 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
return keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions())
.then((config) => {
// validate service name and account just because
if (opts.service === config.get(SecretField.SERVICE) && opts.account === config.get(SecretField.ACCOUNT)) {
const key = config.get(SecretField.KEY);
fn(null, ts_types_1.asString(key));
}
else {
// if the service and account names don't match then maybe someone or something is editing
// that file. #donotallow
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'GenericKeychainServiceError', [keychainConfig_1.KeychainConfig.getFileName()], 'GenericKeychainServiceErrorAction');
const err = sfdxError_1.SfdxError.create(errorConfig);
fn(err);
}
})
.catch((readJsonErr) => {
fn(readJsonErr);
});
}
else {
if (fileAccessError.code === 'ENOENT') {
fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', '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);
}
});
}
async isValidFileAccess(cb) {
try {
const root = await configFile_1.ConfigFile.resolveRootFolder(true);
await fs_1.fs.access(path.join(root, global_1.Global.STATE_FOLDER), fs_1.fs.constants.R_OK | fs_1.fs.constants.X_OK | fs_1.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) {
const secretFile = path.join(await configFile_1.ConfigFile.resolveRootFolder(true), global_1.Global.STATE_FOLDER, ts_types_1.ensure(keychainConfig_1.KeychainConfig.getDefaultOptions().filename));
await super.isValidFileAccess(async (err) => {
if (err != null) {
await cb(err);
}
else {
const keyFile = await keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions());
const stats = await keyFile.stat();
const octalModeStr = (stats.mode & 0o777).toString(8);
const EXPECTED_OCTAL_PERM_VALUE = '600';
if (octalModeStr === EXPECTED_OCTAL_PERM_VALUE) {
await cb(null);
}
else {
const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'GenericKeychainInvalidPermsError', undefined, 'GenericKeychainInvalidPermsErrorAction', [secretFile, EXPECTED_OCTAL_PERM_VALUE]);
await cb(sfdxError_1.SfdxError.create(errorConfig));
}
}
});
}
}
exports.GenericUnixKeychainAccess = GenericUnixKeychainAccess;
/**
* @ignore
*/
class GenericWindowsKeychainAccess extends GenericKeychainAccess {
async isValidFileAccess(cb) {
await super.isValidFileAccess(async (err) => {
if (err != null) {
await cb(err);
}
else {
try {
const secretFile = path.join(await configFile_1.ConfigFile.resolveRootFolder(true), global_1.Global.STATE_FOLDER, ts_types_1.ensure(keychainConfig_1.KeychainConfig.getDefaultOptions().filename));
await fs_1.fs.access(secretFile, fs_1.fs.constants.R_OK | fs_1.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