diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index d1784eb7c..db97e18de 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -288,6 +288,14 @@ export class CliRepl implements MongoshIOProvider { if (this.isPasswordMissingURI(cs)) { cs.password = encodeURIComponent(await this.requirePassword()); } + + if (await this.isTlsKeyFilePasswordMissingURI(searchParams)) { + const keyFilePassword = encodeURIComponent( + await this.requirePassword('Enter TLS key file password') + ); + searchParams.set('tlsCertificateKeyFilePassword', keyFilePassword); + } + this.ensurePasswordFieldIsPresentInAuth(driverOptions); driverUri = cs.toString(); } @@ -1008,6 +1016,28 @@ export class CliRepl implements MongoshIOProvider { ); } + async isTlsKeyFilePasswordMissingURI( + searchParams: ReturnType< + typeof ConnectionString.prototype.typedSearchParams + > + ): Promise { + const tlsCertificateKeyFile = searchParams.get('tlsCertificateKeyFile'); + const tlsCertificateKeyFilePassword = searchParams.get( + 'tlsCertificateKeyFilePassword' + ); + + if (tlsCertificateKeyFile && !tlsCertificateKeyFilePassword) { + const { contents } = await this.readFileUTF8(tlsCertificateKeyFile); + + // Matches standard encrypted key formats for PKCS#12/PKCS#8 and PKCS#1 + return ( + contents.search(/(ENCRYPTED PRIVATE KEY|Proc-Type: 4,ENCRYPTED)/) !== -1 + ); + } + + return false; + } + /** * Sets the auth.password field to undefined in the driverOptions if the auth * object is present with a truthy username. This is required by the driver, e.g. @@ -1028,13 +1058,13 @@ export class CliRepl implements MongoshIOProvider { /** * Require the user to enter a password. */ - async requirePassword(): Promise { + async requirePassword(passwordPrompt = 'Enter password'): Promise { const passwordPromise = askpassword({ input: this.input, output: this.promptOutput, replacementCharacter: '*', }); - this.promptOutput.write('Enter password: '); + this.promptOutput.write(`${passwordPrompt}: `); try { try { return (await passwordPromise).toString(); diff --git a/packages/e2e-tests/test/e2e-tls.spec.ts b/packages/e2e-tests/test/e2e-tls.spec.ts index add6d86b0..1e407eb13 100644 --- a/packages/e2e-tests/test/e2e-tls.spec.ts +++ b/packages/e2e-tests/test/e2e-tls.spec.ts @@ -494,6 +494,41 @@ describe('e2e TLS', function () { expect(logFileContents).not.to.include(CLIENT_CERT_PASSWORD); }); + it('asks for tlsCertificateKeyFilePassword when it is needed (connection string, encrypted)', async function () { + const shell = this.startTestShell({ + args: [ + await connectionStringWithLocalhost(server, { + serverSelectionTimeoutMS: '1500', + authMechanism: 'MONGODB-X509', + tls: 'true', + tlsCAFile: CA_CERT, + tlsCertificateKeyFile: CLIENT_CERT_ENCRYPTED, + }), + ], + env, + }); + + await shell.waitForLine(/Enter TLS key file password:/); + await shell.executeLine(CLIENT_CERT_PASSWORD); + + expect( + await shell.executeLine('db.runCommand({ connectionStatus: 1 })') + ).to.include(`user: '${certUser}'`); + + expect( + await shell.executeLine( + 'db.getSiblingDB("$external").auth({mechanism: "MONGODB-X509"})' + ) + ).to.include('ok: 1'); + expect( + await shell.executeLine('db.runCommand({ connectionStatus: 1 })') + ).to.include(`user: '${certUser}'`); + + const logPath = path.join(logBasePath, `${shell.logId}_log`); + const logFileContents = await fs.readFile(logPath, 'utf8'); + expect(logFileContents).not.to.include(CLIENT_CERT_PASSWORD); + }); + it('fails with invalid cert (args)', async function () { const shell = this.startTestShell({ args: [ diff --git a/packages/e2e-tests/test/test-shell.ts b/packages/e2e-tests/test/test-shell.ts index 66565a1a1..1420b380d 100644 --- a/packages/e2e-tests/test/test-shell.ts +++ b/packages/e2e-tests/test/test-shell.ts @@ -183,6 +183,22 @@ export class TestShell { return this._process; } + async waitForLine(pattern: RegExp, start = 0): Promise { + await eventually(() => { + const output = this._output.slice(start); + const lines = output.split('\n'); + const found = !!lines.filter((l) => pattern.exec(l)); + if (!found) { + throw new assert.AssertionError({ + message: 'expected line', + expected: pattern.toString(), + actual: + this._output.slice(0, start) + '[line search starts here]' + output, + }); + } + }); + } + async waitForPrompt(start = 0): Promise { await eventually(() => { const output = this._output.slice(start);