Skip to content

Commit 3baf6f7

Browse files
authored
feat(cli-repl): Ask for tlsCertificateKeyFilePassword when it is needed MONGOSH-1730
When using TLS enabled with a Certificate that requires a password, the mongosh does not prompt for a passphrase automatically, failing with an error. This adds a password prompt for that case.
1 parent 0360e4b commit 3baf6f7

File tree

3 files changed

+83
-2
lines changed

3 files changed

+83
-2
lines changed

packages/cli-repl/src/cli-repl.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ export class CliRepl implements MongoshIOProvider {
288288
if (this.isPasswordMissingURI(cs)) {
289289
cs.password = encodeURIComponent(await this.requirePassword());
290290
}
291+
292+
if (await this.isTlsKeyFilePasswordMissingURI(searchParams)) {
293+
const keyFilePassword = encodeURIComponent(
294+
await this.requirePassword('Enter TLS key file password')
295+
);
296+
searchParams.set('tlsCertificateKeyFilePassword', keyFilePassword);
297+
}
298+
291299
this.ensurePasswordFieldIsPresentInAuth(driverOptions);
292300
driverUri = cs.toString();
293301
}
@@ -1008,6 +1016,28 @@ export class CliRepl implements MongoshIOProvider {
10081016
);
10091017
}
10101018

1019+
async isTlsKeyFilePasswordMissingURI(
1020+
searchParams: ReturnType<
1021+
typeof ConnectionString.prototype.typedSearchParams<DevtoolsConnectOptions>
1022+
>
1023+
): Promise<boolean> {
1024+
const tlsCertificateKeyFile = searchParams.get('tlsCertificateKeyFile');
1025+
const tlsCertificateKeyFilePassword = searchParams.get(
1026+
'tlsCertificateKeyFilePassword'
1027+
);
1028+
1029+
if (tlsCertificateKeyFile && !tlsCertificateKeyFilePassword) {
1030+
const { contents } = await this.readFileUTF8(tlsCertificateKeyFile);
1031+
1032+
// Matches standard encrypted key formats for PKCS#12/PKCS#8 and PKCS#1
1033+
return (
1034+
contents.search(/(ENCRYPTED PRIVATE KEY|Proc-Type: 4,ENCRYPTED)/) !== -1
1035+
);
1036+
}
1037+
1038+
return false;
1039+
}
1040+
10111041
/**
10121042
* Sets the auth.password field to undefined in the driverOptions if the auth
10131043
* object is present with a truthy username. This is required by the driver, e.g.
@@ -1028,13 +1058,13 @@ export class CliRepl implements MongoshIOProvider {
10281058
/**
10291059
* Require the user to enter a password.
10301060
*/
1031-
async requirePassword(): Promise<string> {
1061+
async requirePassword(passwordPrompt = 'Enter password'): Promise<string> {
10321062
const passwordPromise = askpassword({
10331063
input: this.input,
10341064
output: this.promptOutput,
10351065
replacementCharacter: '*',
10361066
});
1037-
this.promptOutput.write('Enter password: ');
1067+
this.promptOutput.write(`${passwordPrompt}: `);
10381068
try {
10391069
try {
10401070
return (await passwordPromise).toString();

packages/e2e-tests/test/e2e-tls.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,41 @@ describe('e2e TLS', function () {
494494
expect(logFileContents).not.to.include(CLIENT_CERT_PASSWORD);
495495
});
496496

497+
it('asks for tlsCertificateKeyFilePassword when it is needed (connection string, encrypted)', async function () {
498+
const shell = this.startTestShell({
499+
args: [
500+
await connectionStringWithLocalhost(server, {
501+
serverSelectionTimeoutMS: '1500',
502+
authMechanism: 'MONGODB-X509',
503+
tls: 'true',
504+
tlsCAFile: CA_CERT,
505+
tlsCertificateKeyFile: CLIENT_CERT_ENCRYPTED,
506+
}),
507+
],
508+
env,
509+
});
510+
511+
await shell.waitForLine(/Enter TLS key file password:/);
512+
await shell.executeLine(CLIENT_CERT_PASSWORD);
513+
514+
expect(
515+
await shell.executeLine('db.runCommand({ connectionStatus: 1 })')
516+
).to.include(`user: '${certUser}'`);
517+
518+
expect(
519+
await shell.executeLine(
520+
'db.getSiblingDB("$external").auth({mechanism: "MONGODB-X509"})'
521+
)
522+
).to.include('ok: 1');
523+
expect(
524+
await shell.executeLine('db.runCommand({ connectionStatus: 1 })')
525+
).to.include(`user: '${certUser}'`);
526+
527+
const logPath = path.join(logBasePath, `${shell.logId}_log`);
528+
const logFileContents = await fs.readFile(logPath, 'utf8');
529+
expect(logFileContents).not.to.include(CLIENT_CERT_PASSWORD);
530+
});
531+
497532
it('fails with invalid cert (args)', async function () {
498533
const shell = this.startTestShell({
499534
args: [

packages/e2e-tests/test/test-shell.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,22 @@ export class TestShell {
183183
return this._process;
184184
}
185185

186+
async waitForLine(pattern: RegExp, start = 0): Promise<void> {
187+
await eventually(() => {
188+
const output = this._output.slice(start);
189+
const lines = output.split('\n');
190+
const found = !!lines.filter((l) => pattern.exec(l));
191+
if (!found) {
192+
throw new assert.AssertionError({
193+
message: 'expected line',
194+
expected: pattern.toString(),
195+
actual:
196+
this._output.slice(0, start) + '[line search starts here]' + output,
197+
});
198+
}
199+
});
200+
}
201+
186202
async waitForPrompt(start = 0): Promise<void> {
187203
await eventually(() => {
188204
const output = this._output.slice(start);

0 commit comments

Comments
 (0)