UNPKG

@salesforce/core

Version:

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

224 lines 8.39 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 */ 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