UNPKG

18.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 if (credManager.stdout) {
106 credManager.stdout.on('data', data => {
107 stdout += data;
108 });
109 }
110 if (credManager.stderr) {
111 credManager.stderr.on('data', data => {
112 stderr += data;
113 });
114 }
115 credManager.on('close', async (code) => {
116 try {
117 return await this.osImpl.onGetCommandClose(code, stdout, stderr, opts, fn);
118 }
119 catch (e) {
120 if (e.retry) {
121 if (retryCount >= GET_PASSWORD_RETRY_COUNT) {
122 throw sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'PasswordRetryError', [GET_PASSWORD_RETRY_COUNT]);
123 }
124 return this.getPassword(opts, fn, retryCount + 1);
125 }
126 else {
127 // if retry
128 throw e;
129 }
130 }
131 });
132 if (credManager.stdin) {
133 credManager.stdin.end();
134 }
135 }
136 /**
137 * Sets a password using the native program for credential management.
138 * @param opts Options for the credential lookup.
139 * @param fn Callback function (err, password).
140 */
141 async setPassword(opts, fn) {
142 if (opts.service == null) {
143 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainServiceRequiredError'));
144 return;
145 }
146 if (opts.account == null) {
147 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainAccountRequiredError'));
148 return;
149 }
150 if (opts.password == null) {
151 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'PasswordRequiredError'));
152 return;
153 }
154 await _validateProgram(this.osImpl.getProgram(), this.fsIfc, _isExe);
155 const credManager = this.osImpl.setCommandFunc(opts, childProcess.spawn);
156 let stdout = '';
157 let stderr = '';
158 if (credManager.stdout) {
159 credManager.stdout.on('data', (data) => {
160 stdout += data;
161 });
162 }
163 if (credManager.stderr) {
164 credManager.stderr.on('data', (data) => {
165 stderr += data;
166 });
167 }
168 credManager.on('close', async (code) => await this.osImpl.onSetCommandClose(code, stdout, stderr, opts, fn));
169 if (credManager.stdin) {
170 credManager.stdin.end();
171 }
172 }
173}
174exports.KeychainAccess = KeychainAccess;
175/**
176 * Linux implementation.
177 *
178 * Uses libsecret.
179 */
180const _linuxImpl = {
181 getProgram() {
182 return process.env.SFDX_SECRET_TOOL_PATH || path.join(path.sep, 'usr', 'bin', 'secret-tool');
183 },
184 getProgramOptions(opts) {
185 return ['lookup', 'user', opts.account, 'domain', opts.service];
186 },
187 getCommandFunc(opts, fn) {
188 return fn(_linuxImpl.getProgram(), _linuxImpl.getProgramOptions(opts));
189 },
190 async onGetCommandClose(code, stdout, stderr, opts, fn) {
191 if (code === 1) {
192 const command = `${_linuxImpl.getProgram()} ${_optionsToString(_linuxImpl.getProgramOptions(opts))}`;
193 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
194 const error = sfdxError_1.SfdxError.create(errorConfig);
195 // This is a workaround for linux.
196 // Calling secret-tool too fast can cause it to return an unexpected error. (below)
197 if (stderr != null && stderr.includes('invalid or unencryptable secret')) {
198 // @ts-ignore TODO: make an error subclass with this field
199 error.retry = true;
200 // Throwing here allows us to perform a retry in KeychainAccess
201 throw error;
202 }
203 // All other issues we will report back to the handler.
204 fn(error);
205 }
206 else {
207 fn(null, stdout.trim());
208 }
209 },
210 setProgramOptions(opts) {
211 return ['store', "--label='salesforce.com'", 'user', opts.account, 'domain', opts.service];
212 },
213 setCommandFunc(opts, fn) {
214 const secretTool = fn(_linuxImpl.getProgram(), _linuxImpl.setProgramOptions(opts));
215 if (secretTool.stdin) {
216 secretTool.stdin.write(`${opts.password}\n`);
217 }
218 return secretTool;
219 },
220 async onSetCommandClose(code, stdout, stderr, opts, fn) {
221 if (code !== 0) {
222 const command = `${_linuxImpl.getProgram()} ${_optionsToString(_linuxImpl.setProgramOptions(opts))}`;
223 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'SetCredentialError', [`\n${stdout} - ${stderr}`], 'SetCredentialErrorAction', [os.userInfo().username, command]);
224 fn(sfdxError_1.SfdxError.create(errorConfig));
225 }
226 else {
227 fn(null);
228 }
229 }
230};
231/**
232 * OSX implementation.
233 *
234 * /usr/bin/security is a cli front end for OSX keychain.
235 */
236const _darwinImpl = {
237 getProgram() {
238 return path.join(path.sep, 'usr', 'bin', 'security');
239 },
240 getProgramOptions(opts) {
241 return ['find-generic-password', '-a', opts.account, '-s', opts.service, '-g'];
242 },
243 getCommandFunc(opts, fn) {
244 return fn(_darwinImpl.getProgram(), _darwinImpl.getProgramOptions(opts));
245 },
246 async onGetCommandClose(code, stdout, stderr, opts, fn) {
247 let err;
248 if (code !== 0) {
249 switch (code) {
250 case 128:
251 err = sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'KeyChainUserCanceledError');
252 break;
253 default:
254 const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.getProgramOptions(opts))}`;
255 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
256 err = sfdxError_1.SfdxError.create(errorConfig);
257 }
258 fn(err);
259 return;
260 }
261 // For better or worse, the last line (containing the actual password) is actually written to stderr instead of
262 // stdout. Reference: http://blog.macromates.com/2006/keychain-access-from-shell/
263 if (/password/.test(stderr)) {
264 const match = stderr.match(/"(.*)"/);
265 if (!match || !match[1]) {
266 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction');
267 fn(sfdxError_1.SfdxError.create(errorConfig));
268 }
269 else {
270 fn(null, match[1]);
271 }
272 }
273 else {
274 const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.getProgramOptions(opts))}`;
275 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'PasswordNotFoundError', [`\n${stdout} - ${stderr}`], 'PasswordNotFoundErrorAction', [command]);
276 fn(sfdxError_1.SfdxError.create(errorConfig));
277 }
278 },
279 setProgramOptions(opts) {
280 const result = ['add-generic-password', '-a', opts.account, '-s', opts.service];
281 if (opts.password) {
282 result.push('-w', opts.password);
283 }
284 return result;
285 },
286 setCommandFunc(opts, fn) {
287 return fn(_darwinImpl.getProgram(), _darwinImpl.setProgramOptions(opts));
288 },
289 async onSetCommandClose(code, stdout, stderr, opts, fn) {
290 if (code !== 0) {
291 const command = `${_darwinImpl.getProgram()} ${_optionsToString(_darwinImpl.setProgramOptions(opts))}`;
292 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'SetCredentialError', [`\n${stdout} - ${stderr}`], 'SetCredentialErrorAction', [os.userInfo().username, command]);
293 fn(sfdxError_1.SfdxError.create(errorConfig));
294 }
295 else {
296 fn(null);
297 }
298 }
299};
300async function _writeFile(opts, fn) {
301 try {
302 const config = await keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions());
303 config.set(SecretField.ACCOUNT, opts.account);
304 config.set(SecretField.KEY, opts.password || '');
305 config.set(SecretField.SERVICE, opts.service);
306 await config.write();
307 fn(null, config.getContents());
308 }
309 catch (err) {
310 fn(err);
311 }
312}
313var SecretField;
314(function (SecretField) {
315 SecretField["SERVICE"] = "service";
316 SecretField["ACCOUNT"] = "account";
317 SecretField["KEY"] = "key";
318})(SecretField || (SecretField = {}));
319// istanbul ignore next - getPassword/setPassword is always mocked out
320/**
321 * @@ignore
322 */
323class GenericKeychainAccess {
324 async getPassword(opts, fn) {
325 // validate the file in .sfdx
326 await this.isValidFileAccess(async (fileAccessError) => {
327 // the file checks out.
328 if (fileAccessError == null) {
329 // read it's contents
330 return keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions())
331 .then((config) => {
332 // validate service name and account just because
333 if (opts.service === config.get(SecretField.SERVICE) && opts.account === config.get(SecretField.ACCOUNT)) {
334 const key = config.get(SecretField.KEY);
335 fn(null, ts_types_1.asString(key));
336 }
337 else {
338 // if the service and account names don't match then maybe someone or something is editing
339 // that file. #donotallow
340 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'GenericKeychainServiceError', [keychainConfig_1.KeychainConfig.getFileName()], 'GenericKeychainServiceErrorAction');
341 const err = sfdxError_1.SfdxError.create(errorConfig);
342 fn(err);
343 }
344 })
345 .catch(readJsonErr => {
346 fn(readJsonErr);
347 });
348 }
349 else {
350 if (fileAccessError.code === 'ENOENT') {
351 fn(sfdxError_1.SfdxError.create('@salesforce/core', 'encryption', 'PasswordNotFoundError', []));
352 }
353 else {
354 fn(fileAccessError);
355 }
356 }
357 });
358 }
359 async setPassword(opts, fn) {
360 // validate the file in .sfdx
361 await this.isValidFileAccess(async (fileAccessError) => {
362 // if there is a validation error
363 if (fileAccessError != null) {
364 // file not found
365 if (fileAccessError.code === 'ENOENT') {
366 // create the file
367 await _writeFile.call(this, opts, fn);
368 }
369 else {
370 fn(fileAccessError);
371 }
372 }
373 else {
374 // the existing file validated. we can write the updated key
375 await _writeFile.call(this, opts, fn);
376 }
377 });
378 }
379 async isValidFileAccess(cb) {
380 try {
381 const root = await configFile_1.ConfigFile.resolveRootFolder(true);
382 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);
383 await cb(null);
384 }
385 catch (err) {
386 await cb(err);
387 }
388 }
389}
390exports.GenericKeychainAccess = GenericKeychainAccess;
391/**
392 * @ignore
393 */
394// istanbul ignore next - getPassword/setPassword is always mocked out
395class GenericUnixKeychainAccess extends GenericKeychainAccess {
396 async isValidFileAccess(cb) {
397 const secretFile = path.join(await configFile_1.ConfigFile.resolveRootFolder(true), global_1.Global.STATE_FOLDER, ts_types_1.ensure(keychainConfig_1.KeychainConfig.getDefaultOptions().filename));
398 await super.isValidFileAccess(async (err) => {
399 if (err != null) {
400 await cb(err);
401 }
402 else {
403 const keyFile = await keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions());
404 const stats = await keyFile.stat();
405 const octalModeStr = (stats.mode & 0o777).toString(8);
406 const EXPECTED_OCTAL_PERM_VALUE = '600';
407 if (octalModeStr === EXPECTED_OCTAL_PERM_VALUE) {
408 await cb(null);
409 }
410 else {
411 const errorConfig = new sfdxError_1.SfdxErrorConfig('@salesforce/core', 'encryption', 'GenericKeychainInvalidPermsError', undefined, 'GenericKeychainInvalidPermsErrorAction', [secretFile, EXPECTED_OCTAL_PERM_VALUE]);
412 await cb(sfdxError_1.SfdxError.create(errorConfig));
413 }
414 }
415 });
416 }
417}
418exports.GenericUnixKeychainAccess = GenericUnixKeychainAccess;
419/**
420 * @ignore
421 */
422class GenericWindowsKeychainAccess extends GenericKeychainAccess {
423 async isValidFileAccess(cb) {
424 await super.isValidFileAccess(async (err) => {
425 if (err != null) {
426 await cb(err);
427 }
428 else {
429 try {
430 const secretFile = path.join(await configFile_1.ConfigFile.resolveRootFolder(true), global_1.Global.STATE_FOLDER, ts_types_1.ensure(keychainConfig_1.KeychainConfig.getDefaultOptions().filename));
431 await fs_1.fs.access(secretFile, fs_1.fs.constants.R_OK | fs_1.fs.constants.W_OK);
432 await cb(null);
433 }
434 catch (e) {
435 await cb(err);
436 }
437 }
438 });
439 }
440}
441exports.GenericWindowsKeychainAccess = GenericWindowsKeychainAccess;
442/**
443 * @ignore
444 */
445exports.keyChainImpl = {
446 generic_unix: new GenericUnixKeychainAccess(),
447 generic_windows: new GenericWindowsKeychainAccess(),
448 darwin: new KeychainAccess(_darwinImpl, nodeFs),
449 linux: new KeychainAccess(_linuxImpl, nodeFs),
450 validateProgram: _validateProgram
451};
452//# sourceMappingURL=keyChainImpl.js.map
\No newline at end of file