@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
224 lines • 8.39 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 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Crypto = void 0;
const crypto = require("crypto");
const os = require("os");
const path_1 = require("path");
const ts_types_1 = require("@salesforce/ts-types");
const kit_1 = require("@salesforce/kit");
const logger_1 = require("../logger");
const messages_1 = require("../messages");
const cache_1 = require("../util/cache");
const global_1 = require("../global");
const keyChain_1 = require("./keyChain");
const secureBuffer_1 = require("./secureBuffer");
const TAG_DELIMITER = ':';
const BYTE_COUNT_FOR_IV = 6;
const ALGO = 'aes-256-gcm';
const AUTH_TAG_LENGTH = 32;
const ENCRYPTED_CHARS = /[a-f0-9]/;
const KEY_NAME = 'sfdx';
const ACCOUNT = 'local';
messages_1.Messages.importMessagesDirectory((0, path_1.join)(__dirname));
const messages = messages_1.Messages.load('@salesforce/core', 'encryption', [
'keychainPasswordCreationError',
'invalidEncryptedFormatError',
'authDecryptError',
'macKeychainOutOfSync',
]);
const makeSecureBuffer = (password) => {
const newSb = new secureBuffer_1.SecureBuffer();
newSb.consume(Buffer.from((0, ts_types_1.ensure)(password), 'utf8'));
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) => {
return _keychain.getPassword({ service, account }, (err, password) => {
if (err)
return reject(err);
cache_1.Cache.set(cacheKey, makeSecureBuffer(password));
return resolve({ username: account, password: (0, ts_types_1.ensure)(password) });
});
});
}
else {
const pw = sb.value((buffer) => buffer.toString('utf8'));
cache_1.Cache.set(cacheKey, makeSecureBuffer(pw));
return new Promise((resolve) => {
return resolve({ username: account, password: (0, ts_types_1.ensure)(pw) });
});
}
},
/**
* 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 {
/**
* 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.key = new secureBuffer_1.SecureBuffer();
this.options = options || {};
}
encrypt(text) {
if (text == null) {
return;
}
if (this.key == null) {
throw messages.createError('keychainPasswordCreationError');
}
const iv = crypto.randomBytes(BYTE_COUNT_FOR_IV).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}`;
});
}
decrypt(text) {
if (text == null) {
return;
}
const tokens = text.split(TAG_DELIMITER);
if (tokens.length !== 2) {
throw messages.createError('invalidEncryptedFormatError');
}
const tag = tokens[1];
const iv = tokens[0].substring(0, BYTE_COUNT_FOR_IV * 2);
const secret = tokens[0].substring(BYTE_COUNT_FOR_IV * 2, tokens[0].length);
return this.key.value((buffer) => {
const decipher = crypto.createDecipheriv(ALGO, buffer.toString('utf8'), iv);
let dec;
try {
decipher.setAuthTag(Buffer.from(tag, 'hex'));
dec = decipher.update(secret, 'hex', 'utf8');
dec += decipher.final('utf8');
}
catch (err) {
const error = messages.createError('authDecryptError', [err.message], [], err);
const useGenericUnixKeychain = kit_1.env.getBoolean('SFDX_USE_GENERIC_UNIX_KEYCHAIN') || kit_1.env.getBoolean('USE_GENERIC_UNIX_KEYCHAIN');
if (os.platform() === 'darwin' && !useGenericUnixKeychain) {
error.actions = [messages.getMessage('macKeychainOutOfSync')];
}
throw error;
}
return dec;
});
}
/**
* 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.
*/
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 >= BYTE_COUNT_FOR_IV &&
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();
}
}
/**
* Initialize async components.
*/
async init() {
const logger = await logger_1.Logger.child('crypto');
if (!this.options.platform) {
this.options.platform = os.platform();
}
logger.debug(`retryStatus: ${this.options.retryStatus}`);
this.noResetOnClose = !!this.options.noResetOnClose;
try {
this.key.consume(Buffer.from((await keychainPromises.getPassword(await this.getKeyChain(this.options.platform), KEY_NAME, ACCOUNT))
.password, 'utf8'));
}
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') {
logger.debug('a key was set but the retry to get the password failed.');
throw err;
}
else {
logger.debug('password not found in keychain attempting to created one and re-init.');
}
const key = crypto.randomBytes(Math.ceil(16)).toString('hex');
// Create a 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;
}
}
}
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
;