UNPKG

17.6 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2018, salesforce.com, inc.
4 * All rights reserved.
5 * SPDX-License-Identifier: BSD-3-Clause
6 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9const ts_types_1 = require("@salesforce/ts-types");
10const childProcess = require("child_process");
11const nodeFs = require("fs");
12const os = require("os");
13const path = require("path");
14const configFile_1 = require("./config/configFile");
15const keychainConfig_1 = require("./config/keychainConfig");
16const global_1 = require("./global");
17const sfdxError_1 = require("./sfdxError");
18const fs_1 = require("./util/fs");
19/* tslint:disable: no-bitwise */
20const GET_PASSWORD_RETRY_COUNT = 3;
21/**
22 * Helper to reduce an array of cli args down to a presentable string for logging.
23 * @param optionsArray CLI command args.
24 */
25function _optionsToString(optionsArray) {
26 return optionsArray.reduce((accum, element) => `${accum} ${element}`);
27}
28/**
29 * Helper to determine if a program is executable. Returns `true` if the program is executable for the user. For
30 * Windows true is always returned.
31 * @param mode Stats mode.
32 * @param gid Unix group id.
33 * @param uid Unix user id.
34 */
35const _isExe = (mode, gid, uid) => {
36 if (process.platform === 'win32') {
37 return true;
38 }
39 return Boolean(mode & parseInt('0001', 8) ||
40 (mode & parseInt('0010', 8) && process.getgid && gid === process.getgid()) ||
41 (mode & parseInt('0100', 8) && process.getuid && uid === process.getuid()));
42};
43/**
44 * Private helper to validate that a program exists on the file system and is executable.
45 *
46 * **Throws** *{@link SfdxError}{ name: 'MissingCredentialProgramError' }* When the OS credential program isn't found.
47 *
48 * **Throws** *{@link SfdxError}{ name: 'CredentialProgramAccessError' }* When the OS credential program isn't accessible.
49 *
50 * @param programPath The absolute path of the program.
51 * @param fsIfc The file system interface.
52 * @param isExeIfc Executable validation function.
53 */
54const _validateProgram = async (programPath, fsIfc, isExeIfc) => {
55 let noPermission;
56 try {
57 const stats = fsIfc.statSync(programPath);
58 noPermission = !isExeIfc(stats.mode, stats.gid, stats.uid);
59 }
60 catch (e) {
61 throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'MissingCredentialProgramError', [programPath]);
62 }
63 if (noPermission) {
64 throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'CredentialProgramAccessError', [programPath]);
65 }
66};
67/**
68 * @private
69 */
70class KeychainAccess {
71 /**
72 * Abstract prototype for general cross platform keychain interaction.
73 * @param osImpl The platform impl for (linux, darwin, windows).
74 * @param fsIfc The file system interface.
75 */
76 constructor(osImpl, fsIfc) {
77 this.osImpl = osImpl;
78 this.fsIfc = fsIfc;
79 }
80 /**
81 * Validates the os level program is executable.
82 */
83 async validateProgram() {
84 await _validateProgram(this.osImpl.getProgram(), this.fsIfc, _isExe);
85 }
86 /**
87 * Returns a password using the native program for credential management.
88 * @param opts Options for the credential lookup.
89 * @param fn Callback function (err, password).
90 * @param retryCount Used internally to track the number of retries for getting a password out of the keychain.
91 */
92 async getPassword(opts, fn, retryCount = 0) {
93 if (opts.service == null) {
94 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainServiceRequiredError'));
95 return;
96 }
97 if (opts.account == null) {
98 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainAccountRequiredError'));
99 return;
100 }
101 await this.validateProgram();
102 const credManager = this.osImpl.getCommandFunc(opts, childProcess.spawn);
103 let stdout = '';
104 let stderr = '';
105 credManager.stdout.on('data', data => {
106 stdout += data;
107 });
108 credManager.stderr.on('data', data => {
109 stderr += data;
110 });
111 credManager.on('close', async (code) => {
112 try {
113 return await this.osImpl.onGetCommandClose(code, stdout, stderr, opts, fn);
114 }
115 catch (e) {
116 if (e.retry) {
117 if (retryCount >= GET_PASSWORD_RETRY_COUNT) {
118 throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'PasswordRetryError', [GET_PASSWORD_RETRY_COUNT]);
119 }
120 return this.getPassword(opts, fn, retryCount + 1);
121 }
122 else {
123 // if retry
124 throw e;
125 }
126 }
127 });
128 credManager.stdin.end();
129 }
130 /**
131 * Sets a password using the native program for credential management.
132 * @param opts Options for the credential lookup.
133 * @param fn Callback function (err, password).
134 */
135 async setPassword(opts, fn) {
136 if (opts.service == null) {
137 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainServiceRequiredError'));
138 return;
139 }
140 if (opts.account == null) {
141 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainAccountRequiredError'));
142 return;
143 }
144 if (opts.password == null) {
145 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'PasswordRequiredError'));
146 return;
147 }
148 await _validateProgram(this.osImpl.getProgram(), this.fsIfc, _isExe);
149 const credManager = this.osImpl.setCommandFunc(opts, childProcess.spawn);
150 let stdout = '';
151 let stderr = '';
152 credManager.stdout.on('data', (data) => {
153 stdout += data;
154 });
155 credManager.stderr.on('data', (data) => {
156 stderr += data;
157 });
158 credManager.on('close', async (code) => await this.osImpl.onSetCommandClose(code, stdout, stderr, opts, fn));
159 credManager.stdin.end();
160 }
161}
162exports.KeychainAccess = KeychainAccess;
163/**
164 * Linux implementation.
165 *
166 * Uses libsecret.
167 */
168const _linuxImpl = {
169 getProgram() {
170 return process.env.SFDX_SECRET_TOOL_PATH || path.join(path.sep, 'usr', 'bin', 'secret-tool');
171 },
172 getProgramOptions(opts) {
173 return ['lookup', 'user', opts.account, 'domain', opts.service];
174 },
175 getCommandFunc(opts, fn) {
176 return fn(_linuxImpl.getProgram(), _linuxImpl.getProgramOptions(opts));
177 },
178 async onGetCommandClose(code, stdout, stderr, opts, fn) {
179 if (code === 1) {
180 const command = `${_linuxImpl.getProgram()} ${_optionsToString(_linuxImpl.getProgramOptions(opts))}`;
181 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
182 const error = sfdxError_1.SfdxError.create(errorConfig);
183 // This is a workaround for linux.
184 // Calling secret-tool too fast can cause it to return an unexpected error. (below)
185 if (stderr != null && stderr.includes('invalid or unencryptable secret')) {
186 // @ts-ignore TODO: make an error subclass with this field
187 error.retry = true;
188 // Throwing here allows us to perform a retry in KeychainAccess
189 throw error;
190 }
191 // All other issues we will report back to the handler.
192 fn(error);
193 }
194 else {
195 fn(null, stdout.trim());
196 }
197 },
198 setProgramOptions(opts) {
199 return ['store', "--label='salesforce.com'", 'user', opts.account, 'domain', opts.service];
200 },
201 setCommandFunc(opts, fn) {
202 const secretTool = fn(_linuxImpl.getProgram(), _linuxImpl.setProgramOptions(opts));
203 secretTool.stdin.write(`${opts.password}\n`);
204 return secretTool;
205 },
206 async onSetCommandClose(code, stdout, stderr, opts, fn) {
207 if (code !== 0) {
208 const command = `${_linuxImpl.getProgram()} ${_optionsToString(_linuxImpl.setProgramOptions(opts))}`;
209 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'SetCredentialError', [`\n${stdout} - ${stderr}`], 'SetCredentialErrorAction', [os.userInfo().username, command]);
210 fn(sfdxError_1.SfdxError.create(errorConfig));
211 }
212 else {
213 fn(null);
214 }
215 }
216};
217/**
218 * OSX implementation.
219 *
220 * /usr/bin/security is a cli front end for OSX keychain.
221 */
222const _darwinImpl = {
223 getProgram() {
224 return path.join(path.sep, 'usr', 'bin', 'security');
225 },
226 getProgramOptions(opts) {
227 return ['find-generic-password', '-a', opts.account, '-s', opts.service, '-g'];
228 },
229 getCommandFunc(opts, fn) {
230 return fn(_darwinImpl.getProgram(), _darwinImpl.getProgramOptions(opts));
231 },
232 async onGetCommandClose(code, stdout, stderr, opts, fn) {
233 let err;
234 if (code !== 0) {
235 switch (code) {
236 case 128:
237 err = sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainUserCanceledError');
238 break;
239 default:
240 const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.getProgramOptions(opts))}`;
241 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
242 err = sfdxError_1.SfdxError.create(errorConfig);
243 }
244 fn(err);
245 return;
246 }
247 // For better or worse, the last line (containing the actual password) is actually written to stderr instead of
248 // stdout. Reference: http://blog.macromates.com/2006/keychain-access-from-shell/
249 if (/password/.test(stderr)) {
250 const match = stderr.match(/"(.*)"/);
251 if (!match || !match[1]) {
252 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction');
253 fn(sfdxError_1.SfdxError.create(errorConfig));
254 }
255 else {
256 fn(null, match[1]);
257 }
258 }
259 else {
260 const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.getProgramOptions(opts))}`;
261 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
262 fn(sfdxError_1.SfdxError.create(errorConfig));
263 }
264 },
265 setProgramOptions(opts) {
266 const result = ['add-generic-password', '-a', opts.account, '-s', opts.service];
267 if (opts.password) {
268 result.push('-w', opts.password);
269 }
270 return result;
271 },
272 setCommandFunc(opts, fn) {
273 return fn(_darwinImpl.getProgram(), _darwinImpl.setProgramOptions(opts));
274 },
275 async onSetCommandClose(code, stdout, stderr, opts, fn) {
276 if (code !== 0) {
277 const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.setProgramOptions(opts))}`;
278 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'SetCredentialError', [`\n${stdout} - ${stderr}`], 'SetCredentialErrorAction', [os.userInfo().username, command]);
279 fn(sfdxError_1.SfdxError.create(errorConfig));
280 }
281 else {
282 fn(null);
283 }
284 }
285};
286async function _writeFile(opts, fn) {
287 try {
288 const config = await keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions());
289 config.set(SecretField.ACCOUNT, opts.account);
290 config.set(SecretField.KEY, opts.password || '');
291 config.set(SecretField.SERVICE, opts.service);
292 await config.write();
293 fn(null, config.getContents());
294 }
295 catch (err) {
296 fn(err);
297 }
298}
299var SecretField;
300(function (SecretField) {
301 SecretField["SERVICE"] = "service";
302 SecretField["ACCOUNT"] = "account";
303 SecretField["KEY"] = "key";
304})(SecretField || (SecretField = {}));
305// istanbul ignore next - getPassword/setPassword is always mocked out
306/**
307 * @@ignore
308 */
309class GenericKeychainAccess {
310 async getPassword(opts, fn) {
311 // validate the file in .sfdx
312 await this.isValidFileAccess(async (fileAccessError) => {
313 // the file checks out.
314 if (fileAccessError == null) {
315 // read it's contents
316 return keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions())
317 .then((config) => {
318 // validate service name and account just because
319 if (opts.service === config.get(SecretField.SERVICE) && opts.account === config.get(SecretField.ACCOUNT)) {
320 const key = config.get(SecretField.KEY);
321 fn(null, ts_types_1.asString(key));
322 }
323 else {
324 // if the service and account names don't match then maybe someone or something is editing
325 // that file. #donotallow
326 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'GenericKeychainServiceError', [keychainConfig_1.KeychainConfig.getFileName()], 'GenericKeychainServiceErrorAction');
327 const err = sfdxError_1.SfdxError.create(errorConfig);
328 fn(err);
329 }
330 })
331 .catch(readJsonErr => {
332 fn(readJsonErr);
333 });
334 }
335 else {
336 if (fileAccessError.code === 'ENOENT') {
337 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'PasswordNotFoundError', []));
338 }
339 else {
340 fn(fileAccessError);
341 }
342 }
343 });
344 }
345 async setPassword(opts, fn) {
346 // validate the file in .sfdx
347 await this.isValidFileAccess(async (fileAccessError) => {
348 // if there is a validation error
349 if (fileAccessError != null) {
350 // file not found
351 if (fileAccessError.code === 'ENOENT') {
352 // create the file
353 await _writeFile.call(this, opts, fn);
354 }
355 else {
356 fn(fileAccessError);
357 }
358 }
359 else {
360 // the existing file validated. we can write the updated key
361 await _writeFile.call(this, opts, fn);
362 }
363 });
364 }
365 async isValidFileAccess(cb) {
366 try {
367 const root = await configFile_1.ConfigFile.resolveRootFolder(true);
368 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);
369 await cb(null);
370 }
371 catch (err) {
372 await cb(err);
373 }
374 }
375}
376exports.GenericKeychainAccess = GenericKeychainAccess;
377/**
378 * @ignore
379 */
380// istanbul ignore next - getPassword/setPassword is always mocked out
381class GenericUnixKeychainAccess extends GenericKeychainAccess {
382 async isValidFileAccess(cb) {
383 const secretFile = path.join(await configFile_1.ConfigFile.resolveRootFolder(true), global_1.Global.STATE_FOLDER, ts_types_1.ensure(keychainConfig_1.KeychainConfig.getDefaultOptions().filename));
384 await super.isValidFileAccess(async (err) => {
385 if (err != null) {
386 await cb(err);
387 }
388 else {
389 const keyFile = await keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions());
390 const stats = await keyFile.stat();
391 const octalModeStr = (stats.mode & 0o777).toString(8);
392 const EXPECTED_OCTAL_PERM_VALUE = '600';
393 if (octalModeStr === EXPECTED_OCTAL_PERM_VALUE) {
394 await cb(null);
395 }
396 else {
397 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'GenericKeychainInvalidPermsError', undefined, 'GenericKeychainInvalidPermsErrorAction', [secretFile, EXPECTED_OCTAL_PERM_VALUE]);
398 await cb(sfdxError_1.SfdxError.create(errorConfig));
399 }
400 }
401 });
402 }
403}
404exports.GenericUnixKeychainAccess = GenericUnixKeychainAccess;
405/**
406 * @ignore
407 */
408class GenericWindowsKeychainAccess extends GenericKeychainAccess {
409}
410exports.GenericWindowsKeychainAccess = GenericWindowsKeychainAccess;
411/**
412 * @ignore
413 */
414exports.keyChainImpl = {
415 generic_unix: new GenericUnixKeychainAccess(),
416 generic_windows: new GenericWindowsKeychainAccess(),
417 darwin: new KeychainAccess(_darwinImpl, nodeFs),
418 linux: new KeychainAccess(_linuxImpl, nodeFs),
419 validateProgram: _validateProgram
420};
421//# sourceMappingURL=keyChainImpl.js.map
\No newline at end of file