Skip to content

Commit

Permalink
Apply Secrets plugin API
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Vinokur <[email protected]>
  • Loading branch information
vinokurig committed Jun 8, 2021
1 parent 2880525 commit a9df031
Show file tree
Hide file tree
Showing 15 changed files with 518 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"ajv": "^6.5.3",
"body-parser": "^1.17.2",
"cookie": "^0.4.0",
"keytar": "7.7.0",
"drivelist": "^9.0.2",
"es6-promise": "^4.2.4",
"express": "^4.16.3",
Expand Down
106 changes: 106 additions & 0 deletions packages/core/src/browser/credentials-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/********************************************************************************
* Copyright (C) 2021 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// code copied and modified from https://github.com/microsoft/vscode/blob/1.55.2/src/vs/workbench/services/credentials/common/credentials.ts#L12

import { inject, injectable } from 'inversify';
import { Emitter, Event } from '../common/event';
import { KeytarService } from '../common/keytar-protocol';

export interface CredentialsProvider {
getPassword(service: string, account: string): Promise<string | undefined>;
setPassword(service: string, account: string, password: string): Promise<void>;
deletePassword(service: string, account: string): Promise<boolean>;
findPassword(service: string): Promise<string | undefined>;
findCredentials(service: string): Promise<Array<{ account: string, password: string }>>;
}

export const CredentialsService = Symbol('CredentialsService');

export interface CredentialsService extends CredentialsProvider {
readonly onDidChangePassword: Event<CredentialsChangeEvent>;
}

export interface CredentialsChangeEvent {
service: string
account: string;
}

@injectable()
export class CredentialsServiceImpl implements CredentialsService {
private onDidChangePasswordEmitter = new Emitter<CredentialsChangeEvent>();
readonly onDidChangePassword = this.onDidChangePasswordEmitter.event;

private credentialsProvider: CredentialsProvider;

constructor(@inject(KeytarService) private readonly keytarService: KeytarService) {
this.credentialsProvider = new KeytarCredentialsProvider(this.keytarService);
}

getPassword(service: string, account: string): Promise<string | undefined> {
return this.credentialsProvider.getPassword(service, account);
}

async setPassword(service: string, account: string, password: string): Promise<void> {
await this.credentialsProvider.setPassword(service, account, password);

this.onDidChangePasswordEmitter.fire({ service, account });
}

deletePassword(service: string, account: string): Promise<boolean> {
const didDelete = this.credentialsProvider.deletePassword(service, account);
this.onDidChangePasswordEmitter.fire({ service, account });

return didDelete;
}

findPassword(service: string): Promise<string | undefined> {
return this.credentialsProvider.findPassword(service);
}

findCredentials(service: string): Promise<Array<{ account: string, password: string; }>> {
return this.credentialsProvider.findCredentials(service);
}
}

class KeytarCredentialsProvider implements CredentialsProvider {

constructor(private readonly keytarService: KeytarService) {}

deletePassword(service: string, account: string): Promise<boolean> {
return this.keytarService.deletePassword(service, account);
}

findCredentials(service: string): Promise<Array<{ account: string; password: string }>> {
return this.keytarService.findCredentials(service);
}

findPassword(service: string): Promise<string | undefined> {
return this.keytarService.findPassword(service);
}

getPassword(service: string, account: string): Promise<string | undefined> {
return this.keytarService.getPassword(service, account);
}

setPassword(service: string, account: string, password: string): Promise<void> {
return this.keytarService.setPassword(service, account, password);
}
}
9 changes: 9 additions & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ import { EncodingRegistry } from './encoding-registry';
import { EncodingService } from '../common/encoding-service';
import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service';
import { DecorationsService, DecorationsServiceImpl } from './decorations-service';
import { keytarServicePath, KeytarService } from '../common/keytar-protocol';
import { CredentialsService, CredentialsServiceImpl } from './credentials-service';

export { bindResourceProvider, bindMessageService, bindPreferenceService };

Expand Down Expand Up @@ -344,4 +346,11 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo

bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope();
bind(DecorationsService).to(DecorationsServiceImpl).inSingletonScope();

bind(KeytarService).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
return connection.createProxy<KeytarService>(keytarServicePath);
}).inSingletonScope();

bind(CredentialsService).to(CredentialsServiceImpl);
});
26 changes: 26 additions & 0 deletions packages/core/src/common/keytar-protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* Copyright (C) 2021 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

export const keytarServicePath = '/services/keytar';

export const KeytarService = Symbol('KeytarService');
export interface KeytarService {
setPassword(service: string, account: string, password: string): Promise<void>;
getPassword(service: string, account: string): Promise<string | undefined>;
deletePassword(service: string, account: string): Promise<boolean>;
findPassword(service: string): Promise<string | undefined>;
findCredentials(service: string): Promise<Array<{ account: string, password: string }>>;
}
6 changes: 6 additions & 0 deletions packages/core/src/node/backend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { EnvVariablesServerImpl } from './env-variables';
import { ConnectionContainerModule } from './messaging/connection-container-module';
import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service';
import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators';
import { KeytarService, keytarServicePath } from '../common/keytar-protocol';
import { KeytarServiceImpl } from './keytar-server';

decorate(injectable(), ApplicationPackage);

Expand Down Expand Up @@ -95,4 +97,8 @@ export const backendApplicationModule = new ContainerModule(bind => {

bind(WsRequestValidator).toSelf().inSingletonScope();
bindContributionProvider(bind, WsRequestValidatorContribution);
bind(KeytarService).to(KeytarServiceImpl).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(ctx =>
new JsonRpcConnectionHandler(keytarServicePath, () => ctx.container.get<KeytarService>(KeytarService))
).inSingletonScope();
});
98 changes: 98 additions & 0 deletions packages/core/src/node/keytar-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/********************************************************************************
* Copyright (C) 2021 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// code copied and modified from https://github.com/microsoft/vscode/blob/1.55.2/src/vs/platform/native/electron-main/nativeHostMainService.ts#L679-L771

import { KeytarService } from '../common/keytar-protocol';
import { injectable } from 'inversify';
import { isWindows } from '../common';
import * as keytar from 'keytar';

@injectable()
export class KeytarServiceImpl implements KeytarService {
private static readonly MAX_PASSWORD_LENGTH = 2500;
private static readonly PASSWORD_CHUNK_SIZE = KeytarServiceImpl.MAX_PASSWORD_LENGTH - 100;

async setPassword(service: string, account: string, password: string): Promise<void> {
if (isWindows && password.length > KeytarServiceImpl.MAX_PASSWORD_LENGTH) {
let index = 0;
let chunk = 0;
let hasNextChunk = true;
while (hasNextChunk) {
const passwordChunk = password.substring(index, index + KeytarServiceImpl.PASSWORD_CHUNK_SIZE);
index += KeytarServiceImpl.PASSWORD_CHUNK_SIZE;
hasNextChunk = password.length - index > 0;

const content: ChunkedPassword = {
content: passwordChunk,
hasNextChunk: hasNextChunk
};

await keytar.setPassword(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content));
chunk++;
}

} else {
await keytar.setPassword(service, account, password);
}
}

deletePassword(service: string, account: string): Promise<boolean> {
return keytar.deletePassword(service, account);
}

async getPassword(service: string, account: string): Promise<string | undefined> {
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;
}

return content;
} catch {
return password;
}
}
}
async findPassword(service: string): Promise<string | undefined> {
const password = await keytar.findPassword(service);
if (password) {
return password;
}
}
async findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
return keytar.findCredentials(service);
}
}

interface ChunkedPassword {
content: string;
hasNextChunk: boolean;
}
12 changes: 12 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,7 @@ export const PLUGIN_RPC_CONTEXT = {
DEBUG_MAIN: createProxyIdentifier<DebugMain>('DebugMain'),
FILE_SYSTEM_MAIN: createProxyIdentifier<FileSystemMain>('FileSystemMain'),
SCM_MAIN: createProxyIdentifier<ScmMain>('ScmMain'),
SECRETS_MAIN: createProxyIdentifier<SecretsMain>('SecretsMain'),
DECORATIONS_MAIN: createProxyIdentifier<DecorationsMain>('DecorationsMain'),
WINDOW_MAIN: createProxyIdentifier<WindowMain>('WindowMain'),
CLIPBOARD_MAIN: <ProxyIdentifier<ClipboardMain>>createProxyIdentifier<ClipboardMain>('ClipboardMain'),
Expand Down Expand Up @@ -1750,6 +1751,7 @@ export const MAIN_RPC_CONTEXT = {
FILE_SYSTEM_EXT: createProxyIdentifier<FileSystemExt>('FileSystemExt'),
ExtHostFileSystemEventService: createProxyIdentifier<ExtHostFileSystemEventServiceShape>('ExtHostFileSystemEventService'),
SCM_EXT: createProxyIdentifier<ScmExt>('ScmExt'),
SECRETS_EXT: createProxyIdentifier<SecretsExt>('SecretsExt'),
DECORATIONS_EXT: createProxyIdentifier<DecorationsExt>('DecorationsExt'),
LABEL_SERVICE_EXT: createProxyIdentifier<LabelServiceExt>('LabelServiceExt'),
TIMELINE_EXT: createProxyIdentifier<TimelineExt>('TimeLineExt'),
Expand Down Expand Up @@ -1807,3 +1809,13 @@ export interface LabelServiceMain {
$registerResourceLabelFormatter(handle: number, formatter: ResourceLabelFormatter): void;
$unregisterResourceLabelFormatter(handle: number): void;
}

export interface SecretsExt {
$onDidChangePassword(e: { extensionId: string, key: string }): Promise<void>;
}

export interface SecretsMain {
$getPassword(extensionId: string, key: string): Promise<string | undefined>;
$setPassword(extensionId: string, key: string, value: string): Promise<void>;
$deletePassword(extensionId: string, key: string): Promise<void>;
}
4 changes: 3 additions & 1 deletion packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { WebviewsExtImpl } from '../../../plugin/webviews';
import { loadManifest } from './plugin-manifest-loader';
import { TerminalServiceExtImpl } from '../../../plugin/terminal-ext';
import { reviver } from '../../../plugin/types-impl';
import { SecretsExtImpl } from '../../../plugin/secrets-ext';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ctx = self as any;
Expand Down Expand Up @@ -68,6 +69,7 @@ const preferenceRegistryExt = new PreferenceRegistryExtImpl(rpc, workspaceExt);
const debugExt = createDebugExtStub(rpc);
const clipboardExt = new ClipboardExt(rpc);
const webviewExt = new WebviewsExtImpl(rpc, workspaceExt);
const secretsExt = new SecretsExtImpl(rpc);
const terminalService: TerminalServiceExt = new TerminalServiceExtImpl(rpc);

const pluginManager = new PluginManagerExtImpl({
Expand Down Expand Up @@ -155,7 +157,7 @@ const pluginManager = new PluginManagerExtImpl({
}
}
}
}, envExt, terminalService, storageProxy, preferenceRegistryExt, webviewExt, rpc);
}, envExt, terminalService, storageProxy, secretsExt, preferenceRegistryExt, webviewExt, rpc);

const apiFactory = createAPIFactory(
rpc,
Expand Down
10 changes: 7 additions & 3 deletions packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { loadManifest } from './plugin-manifest-loader';
import { KeyValueStorageProxy } from '../../plugin/plugin-storage';
import { WebviewsExtImpl } from '../../plugin/webviews';
import { TerminalServiceExtImpl } from '../../plugin/terminal-ext';
import { SecretsExtImpl } from '../../plugin/secrets-ext';

/**
* Handle the RPC calls.
Expand All @@ -56,13 +57,15 @@ export class PluginHostRPC {
const clipboardExt = new ClipboardExt(this.rpc);
const webviewExt = new WebviewsExtImpl(this.rpc, workspaceExt);
const terminalService = new TerminalServiceExtImpl(this.rpc);
this.pluginManager = this.createPluginManager(envExt, terminalService, storageProxy, preferenceRegistryExt, webviewExt, this.rpc);
const secretsExt = new SecretsExtImpl(this.rpc);
this.pluginManager = this.createPluginManager(envExt, terminalService, storageProxy, preferenceRegistryExt, webviewExt, secretsExt, this.rpc);
this.rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, this.pluginManager);
this.rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, editorsAndDocumentsExt);
this.rpc.set(MAIN_RPC_CONTEXT.WORKSPACE_EXT, workspaceExt);
this.rpc.set(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, preferenceRegistryExt);
this.rpc.set(MAIN_RPC_CONTEXT.STORAGE_EXT, storageProxy);
this.rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, webviewExt);
this.rpc.set(MAIN_RPC_CONTEXT.SECRETS_EXT, secretsExt);

this.apiFactory = createAPIFactory(
this.rpc,
Expand Down Expand Up @@ -95,7 +98,8 @@ export class PluginHostRPC {
}

createPluginManager(
envExt: EnvExtImpl, terminalService: TerminalServiceExtImpl, storageProxy: KeyValueStorageProxy, preferencesManager: PreferenceRegistryExtImpl, webview: WebviewsExtImpl,
envExt: EnvExtImpl, terminalService: TerminalServiceExtImpl, storageProxy: KeyValueStorageProxy,
preferencesManager: PreferenceRegistryExtImpl, webview: WebviewsExtImpl, secretsExt: SecretsExtImpl,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rpc: any): PluginManagerExtImpl {
const { extensionTestsPath } = process.env;
Expand Down Expand Up @@ -229,7 +233,7 @@ export class PluginHostRPC {
`Path ${extensionTestsPath} does not point to a valid extension test runner.`
);
} : undefined
}, envExt, terminalService, storageProxy, preferencesManager, webview, rpc);
}, envExt, terminalService, storageProxy, secretsExt, preferencesManager, webview, rpc);
return pluginManager;
}
}
4 changes: 4 additions & 0 deletions packages/plugin-ext/src/main/browser/main-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { AuthenticationMainImpl } from './authentication-main';
import { ThemingMainImpl } from './theming-main';
import { CommentsMainImp } from './comments/comments-main';
import { CustomEditorsMainImpl } from './custom-editors/custom-editors-main';
import { SecretsMainImpl } from './secrets-main';

export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
const authenticationMain = new AuthenticationMainImpl(rpc, container);
Expand Down Expand Up @@ -151,6 +152,9 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
const scmMain = new ScmMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.SCM_MAIN, scmMain);

const secretsMain = new SecretsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.SECRETS_MAIN, secretsMain);

const decorationsMain = new DecorationsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.DECORATIONS_MAIN, decorationsMain);

Expand Down
Loading

0 comments on commit a9df031

Please sign in to comment.