UNPKG

8.35 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2020, salesforce.com, inc.
4 * All rights reserved.
5 * Licensed under the BSD 3-Clause license.
6 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8/* eslint-disable @typescript-eslint/ban-types */
9Object.defineProperty(exports, "__esModule", { value: true });
10exports.Crypto = void 0;
11const crypto = require("crypto");
12const os = require("os");
13const path_1 = require("path");
14const ts_types_1 = require("@salesforce/ts-types");
15const kit_1 = require("@salesforce/kit");
16const logger_1 = require("../logger");
17const messages_1 = require("../messages");
18const cache_1 = require("../util/cache");
19const global_1 = require("../global");
20const keyChain_1 = require("./keyChain");
21const secureBuffer_1 = require("./secureBuffer");
22const TAG_DELIMITER = ':';
23const BYTE_COUNT_FOR_IV = 6;
24const ALGO = 'aes-256-gcm';
25const AUTH_TAG_LENGTH = 32;
26const ENCRYPTED_CHARS = /[a-f0-9]/;
27const KEY_NAME = 'sfdx';
28const ACCOUNT = 'local';
29messages_1.Messages.importMessagesDirectory((0, path_1.join)(__dirname));
30const messages = messages_1.Messages.load('@salesforce/core', 'encryption', [
31 'keychainPasswordCreationError',
32 'invalidEncryptedFormatError',
33 'authDecryptError',
34 'macKeychainOutOfSync',
35]);
36const makeSecureBuffer = (password) => {
37 const newSb = new secureBuffer_1.SecureBuffer();
38 newSb.consume(Buffer.from((0, ts_types_1.ensure)(password), 'utf8'));
39 return newSb;
40};
41/**
42 * osxKeyChain promise wrapper.
43 */
44const keychainPromises = {
45 /**
46 * Gets a password item.
47 *
48 * @param _keychain
49 * @param service The keychain service name.
50 * @param account The keychain account name.
51 */
52 getPassword(_keychain, service, account) {
53 const cacheKey = `${global_1.Global.DIR}:${service}:${account}`;
54 const sb = cache_1.Cache.get(cacheKey);
55 if (!sb) {
56 return new Promise((resolve, reject) => _keychain.getPassword({ service, account }, (err, password) => {
57 if (err)
58 return reject(err);
59 cache_1.Cache.set(cacheKey, makeSecureBuffer(password));
60 return resolve({ username: account, password: (0, ts_types_1.ensure)(password) });
61 }));
62 }
63 else {
64 const pw = sb.value((buffer) => buffer.toString('utf8'));
65 cache_1.Cache.set(cacheKey, makeSecureBuffer(pw));
66 return new Promise((resolve) => resolve({ username: account, password: (0, ts_types_1.ensure)(pw) }));
67 }
68 },
69 /**
70 * Sets a generic password item in OSX keychain.
71 *
72 * @param _keychain
73 * @param service The keychain service name.
74 * @param account The keychain account name.
75 * @param password The password for the keychain item.
76 */
77 setPassword(_keychain, service, account, password) {
78 return new Promise((resolve, reject) => _keychain.setPassword({ service, account, password }, (err) => {
79 if (err)
80 return reject(err);
81 return resolve({ username: account, password });
82 }));
83 },
84};
85/**
86 * Class for managing encrypting and decrypting private auth information.
87 */
88class Crypto extends kit_1.AsyncOptionalCreatable {
89 /**
90 * Constructor
91 * **Do not directly construct instances of this class -- use {@link Crypto.create} instead.**
92 *
93 * @param options The options for the class instance.
94 * @ignore
95 */
96 constructor(options) {
97 super(options);
98 this.key = new secureBuffer_1.SecureBuffer();
99 this.options = options ?? {};
100 }
101 encrypt(text) {
102 if (text == null) {
103 return;
104 }
105 if (this.key == null) {
106 throw messages.createError('keychainPasswordCreationError');
107 }
108 const iv = crypto.randomBytes(BYTE_COUNT_FOR_IV).toString('hex');
109 return this.key.value((buffer) => {
110 const cipher = crypto.createCipheriv(ALGO, buffer.toString('utf8'), iv);
111 let encrypted = cipher.update(text, 'utf8', 'hex');
112 encrypted += cipher.final('hex');
113 const tag = cipher.getAuthTag().toString('hex');
114 return `${iv}${encrypted}${TAG_DELIMITER}${tag}`;
115 });
116 }
117 decrypt(text) {
118 if (text == null) {
119 return;
120 }
121 const tokens = text.split(TAG_DELIMITER);
122 if (tokens.length !== 2) {
123 throw messages.createError('invalidEncryptedFormatError');
124 }
125 const tag = tokens[1];
126 const iv = tokens[0].substring(0, BYTE_COUNT_FOR_IV * 2);
127 const secret = tokens[0].substring(BYTE_COUNT_FOR_IV * 2, tokens[0].length);
128 return this.key.value((buffer) => {
129 const decipher = crypto.createDecipheriv(ALGO, buffer.toString('utf8'), iv);
130 let dec;
131 try {
132 decipher.setAuthTag(Buffer.from(tag, 'hex'));
133 dec = decipher.update(secret, 'hex', 'utf8');
134 dec += decipher.final('utf8');
135 }
136 catch (err) {
137 const error = messages.createError('authDecryptError', [err.message], [], err);
138 const useGenericUnixKeychain = kit_1.env.getBoolean('SFDX_USE_GENERIC_UNIX_KEYCHAIN') || kit_1.env.getBoolean('USE_GENERIC_UNIX_KEYCHAIN');
139 if (os.platform() === 'darwin' && !useGenericUnixKeychain) {
140 error.actions = [messages.getMessage('macKeychainOutOfSync')];
141 }
142 throw error;
143 }
144 return dec;
145 });
146 }
147 /**
148 * Takes a best guess if the value provided was encrypted by {@link Crypto.encrypt} by
149 * checking the delimiter, tag length, and valid characters.
150 *
151 * @param text The text
152 * @returns true if the text is encrypted, false otherwise.
153 */
154 // eslint-disable-next-line class-methods-use-this
155 isEncrypted(text) {
156 if (text == null) {
157 return false;
158 }
159 const tokens = text.split(TAG_DELIMITER);
160 if (tokens.length !== 2) {
161 return false;
162 }
163 const tag = tokens[1];
164 const value = tokens[0];
165 return (tag.length === AUTH_TAG_LENGTH &&
166 value.length >= BYTE_COUNT_FOR_IV &&
167 ENCRYPTED_CHARS.test(tag) &&
168 ENCRYPTED_CHARS.test(tokens[0]));
169 }
170 /**
171 * Clears the crypto state. This should be called in a finally block.
172 */
173 close() {
174 if (!this.noResetOnClose) {
175 this.key.clear();
176 }
177 }
178 /**
179 * Initialize async components.
180 */
181 async init() {
182 const logger = await logger_1.Logger.child('crypto');
183 if (!this.options.platform) {
184 this.options.platform = os.platform();
185 }
186 logger.debug(`retryStatus: ${this.options.retryStatus}`);
187 this.noResetOnClose = !!this.options.noResetOnClose;
188 try {
189 this.key.consume(Buffer.from((await keychainPromises.getPassword(await this.getKeyChain(this.options.platform), KEY_NAME, ACCOUNT))
190 .password, 'utf8'));
191 }
192 catch (err) {
193 // No password found
194 if (err.name === 'PasswordNotFoundError') {
195 // If we already tried to create a new key then bail.
196 if (this.options.retryStatus === 'KEY_SET') {
197 logger.debug('a key was set but the retry to get the password failed.');
198 throw err;
199 }
200 else {
201 logger.debug('password not found in keychain attempting to created one and re-init.');
202 }
203 const key = crypto.randomBytes(Math.ceil(16)).toString('hex');
204 // Create a new password in the KeyChain.
205 await keychainPromises.setPassword((0, ts_types_1.ensure)(this.options.keychain), KEY_NAME, ACCOUNT, key);
206 return this.init();
207 }
208 else {
209 throw err;
210 }
211 }
212 }
213 async getKeyChain(platform) {
214 if (!this.options.keychain) {
215 this.options.keychain = await (0, keyChain_1.retrieveKeychain)(platform);
216 }
217 return this.options.keychain;
218 }
219}
220exports.Crypto = Crypto;
221//# sourceMappingURL=crypto.js.map
\No newline at end of file