diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 198e668a449..e0cb1596ec6 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -32,7 +32,6 @@ import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; -import { CredentialsMainService } from 'vs/platform/credentials/node/credentialsMainService'; import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; import { DiagnosticsMainService, IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; @@ -99,6 +98,7 @@ import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesHistoryMainService, WorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IWorkspacesManagementMainService, WorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; +import { CredentialsDesktopMainService } from 'vs/platform/credentials/electron-main/credentialsMainService'; /** * The main VS Code application. There will only ever be one instance, @@ -632,7 +632,7 @@ export class CodeApplication extends Disposable { services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess])); // Credentials - services.set(ICredentialsMainService, new SyncDescriptor(CredentialsMainService, [false])); + services.set(ICredentialsMainService, new SyncDescriptor(CredentialsDesktopMainService)); // Webview Manager services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); diff --git a/src/vs/platform/credentials/common/credentialsMainService.ts b/src/vs/platform/credentials/common/credentialsMainService.ts new file mode 100644 index 00000000000..6bed808b144 --- /dev/null +++ b/src/vs/platform/credentials/common/credentialsMainService.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICredentialsChangeEvent, ICredentialsMainService, InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { isWindows } from 'vs/base/common/platform'; + +interface ChunkedPassword { + content: string; + hasNextChunk: boolean; +} + +export type KeytarModule = typeof import('keytar'); + +export abstract class BaseCredentialsMainService extends Disposable implements ICredentialsMainService { + + private static readonly MAX_PASSWORD_LENGTH = 2500; + private static readonly PASSWORD_CHUNK_SIZE = BaseCredentialsMainService.MAX_PASSWORD_LENGTH - 100; + declare readonly _serviceBrand: undefined; + + private _onDidChangePassword: Emitter = this._register(new Emitter()); + readonly onDidChangePassword = this._onDidChangePassword.event; + + protected _keytarCache: KeytarModule | undefined; + + constructor( + @ILogService protected readonly logService: ILogService, + ) { + super(); + } + + //#region abstract + + public abstract getSecretStoragePrefix(): Promise; + protected abstract withKeytar(): Promise; + + //#endregion + + async getPassword(service: string, account: string): Promise { + const keytar = await this.withKeytar(); + + const password = await keytar.getPassword(service, account); + if (password) { + try { + let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); + if (!content || !hasNextChunk) { + return password; + } + + let index = 1; + while (hasNextChunk) { + const nextChunk = await keytar.getPassword(service, `${account}-${index}`); + const result: ChunkedPassword = JSON.parse(nextChunk!); + content += result.content; + hasNextChunk = result.hasNextChunk; + index++; + } + + return content; + } catch { + return password; + } + } + + return password; + } + + async setPassword(service: string, account: string, password: string): Promise { + const keytar = await this.withKeytar(); + const MAX_SET_ATTEMPTS = 3; + + // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. + const setPasswordWithRetry = async (service: string, account: string, password: string) => { + let attempts = 0; + let error: any; + while (attempts < MAX_SET_ATTEMPTS) { + try { + await keytar.setPassword(service, account, password); + return; + } catch (e) { + error = e; + this.logService.warn('Error attempting to set a password: ', e); + attempts++; + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + + // throw last error + throw error; + }; + + if (isWindows && password.length > BaseCredentialsMainService.MAX_PASSWORD_LENGTH) { + let index = 0; + let chunk = 0; + let hasNextChunk = true; + while (hasNextChunk) { + const passwordChunk = password.substring(index, index + BaseCredentialsMainService.PASSWORD_CHUNK_SIZE); + index += BaseCredentialsMainService.PASSWORD_CHUNK_SIZE; + hasNextChunk = password.length - index > 0; + + const content: ChunkedPassword = { + content: passwordChunk, + hasNextChunk: hasNextChunk + }; + + await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); + chunk++; + } + + } else { + await setPasswordWithRetry(service, account, password); + } + + this._onDidChangePassword.fire({ service, account }); + } + + async deletePassword(service: string, account: string): Promise { + const keytar = await this.withKeytar(); + + const didDelete = await keytar.deletePassword(service, account); + if (didDelete) { + this._onDidChangePassword.fire({ service, account }); + } + + return didDelete; + } + + async findPassword(service: string): Promise { + const keytar = await this.withKeytar(); + + return keytar.findPassword(service); + } + + async findCredentials(service: string): Promise> { + const keytar = await this.withKeytar(); + + return keytar.findCredentials(service); + } + + public clear(): Promise { + if (this._keytarCache instanceof InMemoryCredentialsProvider) { + return this._keytarCache.clear(); + } + + // We don't know how to properly clear Keytar because we don't know + // what services have stored credentials. For reference, a "service" is an extension. + // TODO: should we clear credentials for the built-in auth extensions? + return Promise.resolve(); + } +} diff --git a/src/vs/platform/credentials/electron-main/credentialsMainService.ts b/src/vs/platform/credentials/electron-main/credentialsMainService.ts new file mode 100644 index 00000000000..ddccf607a47 --- /dev/null +++ b/src/vs/platform/credentials/electron-main/credentialsMainService.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; + +export class CredentialsDesktopMainService extends BaseCredentialsMainService { + + constructor( + @ILogService logService: ILogService, + @INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService, + @IProductService private readonly productService: IProductService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + ) { + super(logService); + } + + // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the + // client would store the credentials. + public override async getSecretStoragePrefix() { return Promise.resolve(this.productService.urlProtocol); } + + protected async withKeytar(): Promise { + if (this._keytarCache) { + return this._keytarCache; + } + + if (this.environmentMainService.disableKeytar) { + this.logService.info('Keytar is disabled. Using in-memory credential store instead.'); + this._keytarCache = new InMemoryCredentialsProvider(); + return this._keytarCache; + } + + try { + this._keytarCache = await import('keytar'); + // Try using keytar to see if it throws or not. + await this._keytarCache.findCredentials('test-keytar-loads'); + } catch (e) { + this.windowsMainService.sendToFocused('vscode:showCredentialsError', e.message ?? e); + throw e; + } + return this._keytarCache; + } +} diff --git a/src/vs/platform/credentials/node/credentialsMainService.ts b/src/vs/platform/credentials/node/credentialsMainService.ts index f3101496358..f00f3532d00 100644 --- a/src/vs/platform/credentials/node/credentialsMainService.ts +++ b/src/vs/platform/credentials/node/credentialsMainService.ts @@ -3,147 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsChangeEvent, ICredentialsMainService, InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; import { ILogService } from 'vs/platform/log/common/log'; -import { isWindows } from 'vs/base/common/platform'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; +import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; -interface ChunkedPassword { - content: string; - hasNextChunk: boolean; -} - -type KeytarModule = typeof import('keytar'); - -export class CredentialsMainService extends Disposable implements ICredentialsMainService { - - private static readonly MAX_PASSWORD_LENGTH = 2500; - private static readonly PASSWORD_CHUNK_SIZE = CredentialsMainService.MAX_PASSWORD_LENGTH - 100; - declare readonly _serviceBrand: undefined; - - private _onDidChangePassword: Emitter = this._register(new Emitter()); - readonly onDidChangePassword = this._onDidChangePassword.event; - - private _keytarCache: KeytarModule | undefined; - - // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the - // client would store the credentials. - public async getSecretStoragePrefix() { return `${this.productService.urlProtocol}${this.isRunningOnServer ? '-server' : ''}`; } +export class CredentialsWebMainService extends BaseCredentialsMainService { constructor( - private isRunningOnServer: boolean, - @ILogService private readonly logService: ILogService, + @ILogService logService: ILogService, @INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService, @IProductService private readonly productService: IProductService, ) { - super(); + super(logService); } - async getPassword(service: string, account: string): Promise { - const keytar = await this.withKeytar(); + // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the + // client would store the credentials. + public override async getSecretStoragePrefix() { return Promise.resolve(`${this.productService.urlProtocol}-server`); } - const password = await keytar.getPassword(service, account); - if (password) { - try { - let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); - if (!content || !hasNextChunk) { - return password; - } - - let index = 1; - while (hasNextChunk) { - const nextChunk = await keytar.getPassword(service, `${account}-${index}`); - const result: ChunkedPassword = JSON.parse(nextChunk!); - content += result.content; - hasNextChunk = result.hasNextChunk; - index++; - } - - return content; - } catch { - return password; - } - } - - return password; - } - - async setPassword(service: string, account: string, password: string): Promise { - const keytar = await this.withKeytar(); - const MAX_SET_ATTEMPTS = 3; - - // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. - const setPasswordWithRetry = async (service: string, account: string, password: string) => { - let attempts = 0; - let error: any; - while (attempts < MAX_SET_ATTEMPTS) { - try { - await keytar.setPassword(service, account, password); - return; - } catch (e) { - error = e; - this.logService.warn('Error attempting to set a password: ', e); - attempts++; - await new Promise(resolve => setTimeout(resolve, 200)); - } - } - - // throw last error - throw error; - }; - - if (isWindows && password.length > CredentialsMainService.MAX_PASSWORD_LENGTH) { - let index = 0; - let chunk = 0; - let hasNextChunk = true; - while (hasNextChunk) { - const passwordChunk = password.substring(index, index + CredentialsMainService.PASSWORD_CHUNK_SIZE); - index += CredentialsMainService.PASSWORD_CHUNK_SIZE; - hasNextChunk = password.length - index > 0; - - const content: ChunkedPassword = { - content: passwordChunk, - hasNextChunk: hasNextChunk - }; - - await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); - chunk++; - } - - } else { - await setPasswordWithRetry(service, account, password); - } - - this._onDidChangePassword.fire({ service, account }); - } - - async deletePassword(service: string, account: string): Promise { - const keytar = await this.withKeytar(); - - const didDelete = await keytar.deletePassword(service, account); - if (didDelete) { - this._onDidChangePassword.fire({ service, account }); - } - - return didDelete; - } - - async findPassword(service: string): Promise { - const keytar = await this.withKeytar(); - - return keytar.findPassword(service); - } - - async findCredentials(service: string): Promise> { - const keytar = await this.withKeytar(); - - return keytar.findCredentials(service); - } - - private async withKeytar(): Promise { + protected async withKeytar(): Promise { if (this._keytarCache) { return this._keytarCache; } @@ -159,27 +39,10 @@ export class CredentialsMainService extends Disposable implements ICredentialsMa // Try using keytar to see if it throws or not. await this._keytarCache.findCredentials('test-keytar-loads'); } catch (e) { - // We should still throw errors on desktop so that the user is prompted with the - // troubleshooting steps. - if (!this.isRunningOnServer) { - throw e; - } - this.logService.warn( - `Using the in-memory credential store as the operating system's credential store could not be accessed. Please see https://aka.ms/vscode-server-keyring on how to set this up. Details: ${e.message}`); + `Using the in-memory credential store as the operating system's credential store could not be accessed. Please see https://aka.ms/vscode-server-keyring on how to set this up. Details: ${e.message ?? e}`); this._keytarCache = new InMemoryCredentialsProvider(); } return this._keytarCache; } - - public clear(): Promise { - if (this._keytarCache instanceof InMemoryCredentialsProvider) { - return this._keytarCache.clear(); - } - - // We don't know how to properly clear Keytar because we don't know - // what services have stored credentials. For reference, a "service" is an extension. - // TODO: should we clear credentials for the built-in auth extensions? - return Promise.resolve(); - } } diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 0cef732294e..10ff028117a 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -16,7 +16,7 @@ import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; -import { CredentialsMainService } from 'vs/platform/credentials/node/credentialsMainService'; +import { CredentialsWebMainService } from 'vs/platform/credentials/node/credentialsMainService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc'; @@ -168,7 +168,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId])); - services.set(ICredentialsMainService, new SyncDescriptor(CredentialsMainService, [true])); + services.set(ICredentialsMainService, new SyncDescriptor(CredentialsWebMainService)); instantiationService.invokeFunction(accessor => { const extensionManagementService = accessor.get(IExtensionManagementService); diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index f2b96e2d3f0..23329c15f9e 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -197,6 +197,15 @@ export class NativeWindow extends Disposable { }] )); + ipcRenderer.on('vscode:showCredentialsError', (event: unknown, message: string) => this.notificationService.prompt( + Severity.Error, + localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", message), + [{ + label: localize('troubleshooting', "Troubleshooting Guide"), + run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2190713') + }] + )); + // Fullscreen Events ipcRenderer.on('vscode:enterFullScreen', async () => setFullscreen(true)); ipcRenderer.on('vscode:leaveFullScreen', async () => setFullscreen(false));