Skip to content

Commit

Permalink
When keytar fails to be used, use an in-memory credential store (micr…
Browse files Browse the repository at this point in the history
…osoft#141120)

* have inmemory fallback

* move InMemoryCredentialsProvider to common for shared scenarios
  • Loading branch information
TylerLeonhardt authored Jan 21, 2022
1 parent 5993199 commit 319ee9a
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 60 deletions.
44 changes: 44 additions & 0 deletions src/vs/platform/credentials/common/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
}
}
36 changes: 29 additions & 7 deletions src/vs/platform/credentials/node/credentialsMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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' : ''}`; }
Expand Down Expand Up @@ -139,18 +143,36 @@ export class CredentialsMainService extends Disposable implements ICredentialsMa
return keytar.findCredentials(service);
}

private async withKeytar(): Promise<typeof import('keytar')> {
private async withKeytar(): Promise<KeytarModule> {
if (this._keytarCache) {
return this._keytarCache;
}

if (this.environmentMainService.disableKeytar) {
throw new Error('keytar has been disabled via --disable-keytar option');
this.logService.info('Keytar is disabled. Using in-memory credential store instead.');
this._keytarCache = new InMemoryCredentialsProvider();
return this._keytarCache;
}

return await import('keytar');
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = [];
}
}

0 comments on commit 319ee9a

Please sign in to comment.