From 319ee9a6af7c7b3fcb5e5877e896ed78a7052c8f Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 20 Jan 2022 22:30:34 -0800 Subject: [PATCH] When keytar fails to be used, use an in-memory credential store (#141120) * have inmemory fallback * move InMemoryCredentialsProvider to common for shared scenarios --- .../credentials/common/credentials.ts | 44 +++++++++++++++ .../node/credentialsMainService.ts | 38 ++++++++++--- .../credentials/browser/credentialsService.ts | 54 +------------------ 3 files changed, 75 insertions(+), 61 deletions(-) diff --git a/src/vs/platform/credentials/common/credentials.ts b/src/vs/platform/credentials/common/credentials.ts index 42d7c26a65a..dbc3ab12a5f 100644 --- a/src/vs/platform/credentials/common/credentials.ts +++ b/src/vs/platform/credentials/common/credentials.ts @@ -38,3 +38,47 @@ export interface ICredentialsService extends ICredentialsProvider { export const ICredentialsMainService = createDecorator('credentialsMainService'); export interface ICredentialsMainService extends ICredentialsService { } + +interface ISecretVault { + [service: string]: { [account: string]: string } | undefined; +} + +export class InMemoryCredentialsProvider implements ICredentialsProvider { + private secretVault: ISecretVault = {}; + + async getPassword(service: string, account: string): Promise { + return this.secretVault[service]?.[account] ?? null; + } + + async setPassword(service: string, account: string, password: string): Promise { + this.secretVault[service] = this.secretVault[service] ?? {}; + this.secretVault[service]![account] = password; + } + + async deletePassword(service: string, account: string): Promise { + if (!this.secretVault[service]?.[account]) { + return false; + } + delete this.secretVault[service]![account]; + if (Object.keys(this.secretVault[service]!).length === 0) { + delete this.secretVault[service]; + } + return true; + } + + async findPassword(service: string): Promise { + return JSON.stringify(this.secretVault[service]) ?? null; + } + + async findCredentials(service: string): Promise> { + const credentials: { account: string, password: string }[] = []; + for (const account of Object.keys(this.secretVault[service] || {})) { + credentials.push({ account, password: this.secretVault[service]![account] }); + } + return credentials; + } + + async clear(): Promise { + this.secretVault = {}; + } +} diff --git a/src/vs/platform/credentials/node/credentialsMainService.ts b/src/vs/platform/credentials/node/credentialsMainService.ts index 59ad69496b1..180b31ef9e3 100644 --- a/src/vs/platform/credentials/node/credentialsMainService.ts +++ b/src/vs/platform/credentials/node/credentialsMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsChangeEvent, ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; +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'; @@ -16,6 +16,8 @@ interface ChunkedPassword { hasNextChunk: boolean; } +type KeytarModule = typeof import('keytar'); + export class CredentialsMainService extends Disposable implements ICredentialsMainService { private static readonly MAX_PASSWORD_LENGTH = 2500; @@ -25,6 +27,8 @@ export class CredentialsMainService extends Disposable implements ICredentialsMa 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' : ''}`; } @@ -139,18 +143,36 @@ export class CredentialsMainService extends Disposable implements ICredentialsMa return keytar.findCredentials(service); } - private async withKeytar(): Promise { - if (this.environmentMainService.disableKeytar) { - throw new Error('keytar has been disabled via --disable-keytar option'); + private async withKeytar(): Promise { + if (this._keytarCache) { + return this._keytarCache; } - return await import('keytar'); + 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.logService.warn(`Switching to using in-memory credential store instead because Keytar failed to load: ${e.message}`); + this._keytarCache = new InMemoryCredentialsProvider(); + } + return this._keytarCache; } - // This class doesn't implement the clear() function 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? 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/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts index b3a373950e4..c67af447218 100644 --- a/src/vs/workbench/services/credentials/browser/credentialsService.ts +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsService, ICredentialsProvider, ICredentialsChangeEvent } from 'vs/platform/credentials/common/credentials'; +import { ICredentialsService, ICredentialsProvider, ICredentialsChangeEvent, InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -77,55 +77,3 @@ export class BrowserCredentialsService extends Disposable implements ICredential } } } - -interface ICredential { - service: string; - account: string; - password: string; -} - -class InMemoryCredentialsProvider implements ICredentialsProvider { - - private credentials: ICredential[] = []; - - async getPassword(service: string, account: string): Promise { - const credential = this.doFindPassword(service, account); - - return credential ? credential.password : null; - } - - async setPassword(service: string, account: string, password: string): Promise { - this.deletePassword(service, account); - this.credentials.push({ service, account, password }); - } - - async deletePassword(service: string, account: string): Promise { - const credential = this.doFindPassword(service, account); - if (credential) { - this.credentials.splice(this.credentials.indexOf(credential), 1); - } - - return !!credential; - } - - async findPassword(service: string): Promise { - const credential = this.doFindPassword(service); - - return credential ? credential.password : null; - } - - private doFindPassword(service: string, account?: string): ICredential | undefined { - return this.credentials.find(credential => - credential.service === service && (typeof account !== 'string' || credential.account === account)); - } - - async findCredentials(service: string): Promise> { - return this.credentials - .filter(credential => credential.service === service) - .map(({ account, password }) => ({ account, password })); - } - - async clear(): Promise { - this.credentials = []; - } -}