1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | Object.defineProperty(exports, "__esModule", { value: true });
|
9 | const ts_types_1 = require("@salesforce/ts-types");
|
10 | const childProcess = require("child_process");
|
11 | const nodeFs = require("fs");
|
12 | const os = require("os");
|
13 | const path = require("path");
|
14 | const configFile_1 = require("./config/configFile");
|
15 | const keychainConfig_1 = require("./config/keychainConfig");
|
16 | const global_1 = require("./global");
|
17 | const sfdxError_1 = require("./sfdxError");
|
18 | const fs_1 = require("./util/fs");
|
19 |
|
20 | const GET_PASSWORD_RETRY_COUNT = 3;
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | function _optionsToString(optionsArray) {
|
26 | return optionsArray.reduce((accum, element) => `${accum} ${element}`);
|
27 | }
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | const _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 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | const _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 |
|
69 |
|
70 | class KeychainAccess {
|
71 | |
72 |
|
73 |
|
74 |
|
75 |
|
76 | constructor(osImpl, fsIfc) {
|
77 | this.osImpl = osImpl;
|
78 | this.fsIfc = fsIfc;
|
79 | }
|
80 | |
81 |
|
82 |
|
83 | async validateProgram() {
|
84 | await _validateProgram(this.osImpl.getProgram(), this.fsIfc, _isExe);
|
85 | }
|
86 | |
87 |
|
88 |
|
89 |
|
90 |
|
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 |
|
128 | throw e;
|
129 | }
|
130 | }
|
131 | });
|
132 | if (credManager.stdin) {
|
133 | credManager.stdin.end();
|
134 | }
|
135 | }
|
136 | |
137 |
|
138 |
|
139 |
|
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 | }
|
174 | exports.KeychainAccess = KeychainAccess;
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 | const _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 |
|
196 |
|
197 | if (stderr != null && stderr.includes('invalid or unencryptable secret')) {
|
198 |
|
199 | error.retry = true;
|
200 |
|
201 | throw error;
|
202 | }
|
203 |
|
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 |
|
233 |
|
234 |
|
235 |
|
236 | const _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 |
|
262 |
|
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 | };
|
300 | async 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 | }
|
313 | var SecretField;
|
314 | (function (SecretField) {
|
315 | SecretField["SERVICE"] = "service";
|
316 | SecretField["ACCOUNT"] = "account";
|
317 | SecretField["KEY"] = "key";
|
318 | })(SecretField || (SecretField = {}));
|
319 |
|
320 |
|
321 |
|
322 |
|
323 | class GenericKeychainAccess {
|
324 | async getPassword(opts, fn) {
|
325 |
|
326 | await this.isValidFileAccess(async (fileAccessError) => {
|
327 |
|
328 | if (fileAccessError == null) {
|
329 |
|
330 | return keychainConfig_1.KeychainConfig.create(keychainConfig_1.KeychainConfig.getDefaultOptions())
|
331 | .then((config) => {
|
332 |
|
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 |
|
339 |
|
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 |
|
361 | await this.isValidFileAccess(async (fileAccessError) => {
|
362 |
|
363 | if (fileAccessError != null) {
|
364 |
|
365 | if (fileAccessError.code === 'ENOENT') {
|
366 |
|
367 | await _writeFile.call(this, opts, fn);
|
368 | }
|
369 | else {
|
370 | fn(fileAccessError);
|
371 | }
|
372 | }
|
373 | else {
|
374 |
|
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 | }
|
390 | exports.GenericKeychainAccess = GenericKeychainAccess;
|
391 |
|
392 |
|
393 |
|
394 |
|
395 | class 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 | }
|
418 | exports.GenericUnixKeychainAccess = GenericUnixKeychainAccess;
|
419 |
|
420 |
|
421 |
|
422 | class 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 | }
|
441 | exports.GenericWindowsKeychainAccess = GenericWindowsKeychainAccess;
|
442 |
|
443 |
|
444 |
|
445 | exports.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 |
|
\ | No newline at end of file |