When keytar fails to be used, use an in-memory credential store (#141120)

* have inmemory fallback

* move InMemoryCredentialsProvider to common for shared scenarios
This commit is contained in:
Tyler James Leonhardt
2022-01-20 22:30:34 -08:00
committed by GitHub
parent 5993199079
commit 319ee9a6af
3 changed files with 75 additions and 61 deletions
@@ -38,3 +38,47 @@ export interface ICredentialsService extends ICredentialsProvider {
export const ICredentialsMainService = createDecorator<ICredentialsMainService>('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<string | null> {
return this.secretVault[service]?.[account] ?? null;
}
async setPassword(service: string, account: string, password: string): Promise<void> {
this.secretVault[service] = this.secretVault[service] ?? {};
this.secretVault[service]![account] = password;
}
async deletePassword(service: string, account: string): Promise<boolean> {
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<string | null> {
return JSON.stringify(this.secretVault[service]) ?? null;
}
async findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
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<void> {
this.secretVault = {};
}
}
@@ -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<ICredentialsChangeEvent> = 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<typeof import('keytar')> {
if (this.environmentMainService.disableKeytar) {
throw new Error('keytar has been disabled via --disable-keytar option');
private async withKeytar(): Promise<KeytarModule> {
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<void> {
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();
}
}
@@ -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<string | null> {
const credential = this.doFindPassword(service, account);
return credential ? credential.password : null;
}
async setPassword(service: string, account: string, password: string): Promise<void> {
this.deletePassword(service, account);
this.credentials.push({ service, account, password });
}
async deletePassword(service: string, account: string): Promise<boolean> {
const credential = this.doFindPassword(service, account);
if (credential) {
this.credentials.splice(this.credentials.indexOf(credential), 1);
}
return !!credential;
}
async findPassword(service: string): Promise<string | null> {
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<Array<{ account: string, password: string; }>> {
return this.credentials
.filter(credential => credential.service === service)
.map(({ account, password }) => ({ account, password }));
}
async clear(): Promise<void> {
this.credentials = [];
}
}