@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
377 lines • 17.5 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
*/
/* 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
;