Add getPassword, setPassword, and deletePassword APIs, #95475

Co-authored-by: SteVen Batten <sbatten@microsoft.com>
This commit is contained in:
Rachel Macfarlane 2020-10-06 14:57:16 -07:00 committed by GitHub
parent 1a9e0af641
commit dafce599a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 279 additions and 40 deletions

View file

@ -28,24 +28,12 @@ export type Keytar = {
deletePassword: typeof keytarType['deletePassword'];
};
const SERVICE_ID = `${vscode.env.uriScheme}-github.login`;
const ACCOUNT_ID = 'account';
const SERVICE_ID = `github.auth`;
export class Keychain {
private keytar: Keytar;
constructor() {
const keytar = getKeytar();
if (!keytar) {
throw new Error('System keychain unavailable');
}
this.keytar = keytar;
}
async setToken(token: string): Promise<void> {
try {
return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token);
return await vscode.authentication.setPassword(SERVICE_ID, token);
} catch (e) {
// Ignore
Logger.error(`Setting token failed: ${e}`);
@ -59,7 +47,7 @@ export class Keychain {
async getToken(): Promise<string | null | undefined> {
try {
return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID);
return await vscode.authentication.getPassword(SERVICE_ID);
} catch (e) {
// Ignore
Logger.error(`Getting token failed: ${e}`);
@ -67,15 +55,35 @@ export class Keychain {
}
}
async deleteToken(): Promise<boolean | undefined> {
async deleteToken(): Promise<void> {
try {
return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID);
return await vscode.authentication.deletePassword(SERVICE_ID);
} catch (e) {
// Ignore
Logger.error(`Deleting token failed: ${e}`);
return Promise.resolve(undefined);
}
}
async tryMigrate(): Promise<string | null | undefined> {
try {
const keytar = getKeytar();
if (!keytar) {
throw new Error('keytar unavailable');
}
const oldValue = await keytar.getPassword(`${vscode.env.uriScheme}-github.login`, 'account');
if (oldValue) {
await this.setToken(oldValue);
await keytar.deletePassword(`${vscode.env.uriScheme}-github.login`, 'account');
}
return oldValue;
} catch (_) {
// Ignore
return Promise.resolve(undefined);
}
}
}
export const keychain = new Keychain();

View file

@ -82,7 +82,7 @@ export class GitHubAuthenticationProvider {
}
private async readSessions(): Promise<vscode.AuthenticationSession[]> {
const storedSessions = await keychain.getToken();
const storedSessions = await keychain.getToken() || await keychain.tryMigrate();
if (storedSessions) {
try {
const sessionData: SessionData[] = JSON.parse(storedSessions);

View file

@ -74,10 +74,10 @@ base64-js@^1.0.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
bl@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a"
integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==
bl@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
@ -287,9 +287,9 @@ napi-build-utils@^1.0.1:
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
node-abi@^2.7.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.17.0.tgz#f167c92780497ff01eeaf473fcf8138e0fcc87fa"
integrity sha512-dFRAA0ACk/aBo0TIXQMEWMLUTyWYYT8OBYIzLmEUrQTElGRjxDCvyBZIsDL0QA7QCaj9PrawhOmTEdsuLY4uOQ==
version "2.19.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.1.tgz#6aa32561d0a5e2fdb6810d8c25641b657a8cea85"
integrity sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A==
dependencies:
semver "^5.4.1"
@ -427,9 +427,9 @@ signal-exit@^3.0.0:
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
simple-concat@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^3.0.3:
version "3.1.0"
@ -501,11 +501,11 @@ tar-fs@^2.0.0:
tar-stream "^2.0.0"
tar-stream@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325"
integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==
version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa"
integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==
dependencies:
bl "^4.0.1"
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"

View file

@ -100,7 +100,7 @@ export class AzureActiveDirectoryService {
}
public async initialize(): Promise<void> {
const storedData = await keychain.getToken();
const storedData = await keychain.getToken() || await keychain.tryMigrate();
if (storedData) {
try {
const sessions = this.parseStoredData(storedData);

View file

@ -28,7 +28,8 @@ export type Keytar = {
deletePassword: typeof keytarType['deletePassword'];
};
const SERVICE_ID = `${vscode.env.uriScheme}-microsoft.login`;
const OLD_SERVICE_ID = `${vscode.env.uriScheme}-microsoft.login`;
const SERVICE_ID = `microsoft.login`;
const ACCOUNT_ID = 'account';
export class Keychain {
@ -46,7 +47,7 @@ export class Keychain {
async setToken(token: string): Promise<void> {
try {
return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token);
return await vscode.authentication.setPassword(SERVICE_ID, token);
} catch (e) {
Logger.error(`Setting token failed: ${e}`);
@ -68,7 +69,7 @@ export class Keychain {
async getToken(): Promise<string | null | undefined> {
try {
return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID);
return await vscode.authentication.getPassword(SERVICE_ID);
} catch (e) {
// Ignore
Logger.error(`Getting token failed: ${e}`);
@ -76,15 +77,30 @@ export class Keychain {
}
}
async deleteToken(): Promise<boolean | undefined> {
async deleteToken(): Promise<void> {
try {
return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID);
return await vscode.authentication.deletePassword(SERVICE_ID);
} catch (e) {
// Ignore
Logger.error(`Deleting token failed: ${e}`);
return Promise.resolve(undefined);
}
}
async tryMigrate(): Promise<string | null> {
try {
const oldValue = await this.keytar.getPassword(OLD_SERVICE_ID, ACCOUNT_ID);
if (oldValue) {
await this.setToken(oldValue);
await this.keytar.deletePassword(OLD_SERVICE_ID, ACCOUNT_ID);
}
return oldValue;
} catch (_) {
// Ignore
return Promise.resolve(null);
}
}
}
export const keychain = new Keychain();

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.51.0",
"distro": "e42a5273a865424266dd22838961d286c5c45ddc",
"distro": "ee20dc175593a951f2c01e62d044796a60004cb7",
"author": {
"name": "Microsoft Corporation"
},

View file

@ -82,6 +82,7 @@ import { IFileService } from 'vs/platform/files/common/files';
import { stripComments } from 'vs/base/common/json';
import { generateUuid } from 'vs/base/common/uuid';
import { VSBuffer } from 'vs/base/common/buffer';
import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
export class CodeApplication extends Disposable {
private windowsMainService: IWindowsMainService | undefined;
@ -443,6 +444,7 @@ export class CodeApplication extends Disposable {
services.set(IDiagnosticsService, createChannelSender(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics')))));
services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [machineId, this.userEnv]));
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId]));
services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService));
services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService));
services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService));
@ -531,6 +533,10 @@ export class CodeApplication extends Disposable {
const issueChannel = createChannelReceiver(issueMainService);
electronIpcServer.registerChannel('issue', issueChannel);
const encryptionMainService = accessor.get(IEncryptionMainService);
const encryptionChannel = createChannelReceiver(encryptionMainService);
electronIpcServer.registerChannel('encryption', encryptionChannel);
const nativeHostMainService = accessor.get(INativeHostMainService);
const nativeHostChannel = createChannelReceiver(nativeHostMainService);
electronIpcServer.registerChannel('nativeHost', nativeHostChannel);

View file

@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ICommonEncryptionService {
readonly _serviceBrand: undefined;
encrypt(value: string): Promise<string>;
decrypt(value: string): Promise<string>;
}

View file

@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICommonEncryptionService } from 'vs/platform/encryption/electron-main/common/encryptionService';
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
export interface IEncryptionMainService extends ICommonEncryptionService { }
export interface Encryption {
encrypt(salt: string, value: string): Promise<string>;
decrypt(salt: string, value: string): Promise<string>;
}
export class EncryptionMainService implements ICommonEncryptionService {
declare readonly _serviceBrand: undefined;
constructor(
private machineId: string) {
}
private encryption(): Promise<Encryption> {
return new Promise((resolve, reject) => require(['vscode-encrypt'], resolve, reject));
}
async encrypt(value: string): Promise<string> {
try {
const encryption = await this.encryption();
return encryption.encrypt(this.machineId, value);
} catch (e) {
return value;
}
}
async decrypt(value: string): Promise<string> {
try {
const encryption = await this.encryption();
return encryption.decrypt(this.machineId, value);
} catch (e) {
return value;
}
}
}

View file

@ -143,6 +143,26 @@ declare module 'vscode' {
* provider
*/
export function logout(providerId: string, sessionId: string): Thenable<void>;
/**
* Retrieve a password that was stored with key. Returns undefined if there
* is no password matching that key.
* @param key The key the password was stored under.
*/
export function getPassword(key: string): Thenable<string | undefined>;
/**
* Store a password under a given key.
* @param key The key to store the password under
* @param value The password
*/
export function setPassword(key: string, value: string): Thenable<void>;
/**
* Remove a password from storage.
* @param key The key the password was stored under.
*/
export function deletePassword(key: string): Thenable<void>;
}
//#endregion

View file

@ -19,6 +19,9 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA
import { fromNow } from 'vs/base/common/date';
import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { isWeb } from 'vs/base/common/platform';
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
import { IProductService } from 'vs/platform/product/common/productService';
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser'];
@ -220,7 +223,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IExtensionService private readonly extensionService: IExtensionService
@IExtensionService private readonly extensionService: IExtensionService,
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IEncryptionService private readonly encryptionService: IEncryptionService,
@IProductService private readonly productService: IProductService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
@ -453,4 +459,46 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL);
}
private getFullKey(extensionId: string): string {
return `${this.productService.urlProtocol}${extensionId}`;
}
async $getPassword(extensionId: string, key: string): Promise<string | undefined> {
const fullKey = this.getFullKey(extensionId);
const password = await this.credentialsService.getPassword(fullKey, key);
const decrypted = password && await this.encryptionService.decrypt(password);
if (decrypted) {
try {
const value = JSON.parse(decrypted);
if (value.extensionId === extensionId) {
return value.content;
}
} catch (_) {
throw new Error('Cannot get password');
}
}
return undefined;
}
async $setPassword(extensionId: string, key: string, value: string): Promise<void> {
const fullKey = this.getFullKey(extensionId);
const toEncrypt = JSON.stringify({
extensionId,
content: value
});
const encrypted = await this.encryptionService.encrypt(toEncrypt);
return this.credentialsService.setPassword(fullKey, key, encrypted);
}
async $deletePassword(extensionId: string, key: string): Promise<void> {
try {
const fullKey = this.getFullKey(extensionId);
await this.credentialsService.deletePassword(fullKey, key);
} catch (_) {
throw new Error('Cannot delete password');
}
}
}

View file

@ -225,6 +225,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
get onDidChangeSessions(): Event<vscode.AuthenticationSessionsChangeEvent> {
return extHostAuthentication.onDidChangeSessions;
},
getPassword(key: string): Thenable<string | undefined> {
return extHostAuthentication.getPassword(extension, key);
},
setPassword(key: string, value: string): Thenable<void> {
return extHostAuthentication.setPassword(extension, key, value);
},
deletePassword(key: string): Thenable<void> {
return extHostAuthentication.deletePassword(extension, key);
}
};
// namespace: commands

View file

@ -173,6 +173,10 @@ export interface MainThreadAuthenticationShape extends IDisposable {
$getSessions(providerId: string): Promise<ReadonlyArray<modes.AuthenticationSession>>;
$login(providerId: string, scopes: string[]): Promise<modes.AuthenticationSession>;
$logout(providerId: string, sessionId: string): Promise<void>;
$getPassword(extensionId: string, key: string): Promise<string | undefined>;
$setPassword(extensionId: string, key: string, value: string): Promise<void>;
$deletePassword(extensionId: string, key: string): Promise<void>;
}
export interface MainThreadConfigurationShape extends IDisposable {

View file

@ -203,4 +203,19 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
this._onDidChangeAuthenticationProviders.fire({ added, removed });
return Promise.resolve();
}
getPassword(requestingExtension: IExtensionDescription, key: string): Promise<string | undefined> {
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
return this._proxy.$getPassword(extensionId, key);
}
setPassword(requestingExtension: IExtensionDescription, key: string, value: string): Promise<void> {
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
return this._proxy.$setPassword(extensionId, key, value);
}
deletePassword(requestingExtension: IExtensionDescription, key: string): Promise<void> {
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
return this._proxy.$deletePassword(extensionId, key);
}
}

View file

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
export class EncryptionService {
declare readonly _serviceBrand: undefined;
encrypt(value: string): Promise<string> {
return Promise.resolve(value);
}
decrypt(value: string): Promise<string> {
return Promise.resolve(value);
}
}
registerSingleton(IEncryptionService, EncryptionService, true);

View file

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICommonEncryptionService } from 'vs/platform/encryption/electron-main/common/encryptionService';
export const IEncryptionService = createDecorator<IEncryptionService>('encryptionService');
export interface IEncryptionService extends ICommonEncryptionService { }

View file

@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
export class EncryptionService {
declare readonly _serviceBrand: undefined;
constructor(@IMainProcessService mainProcessService: IMainProcessService) {
return createChannelSender<IEncryptionService>(mainProcessService.getChannel('encryption'));
}
}
registerSingleton(IEncryptionService, EncryptionService, true);

View file

@ -82,6 +82,7 @@ import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreMan
import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService';
import 'vs/workbench/services/sharedProcess/electron-browser/sharedProcessService';
import 'vs/workbench/services/localizations/electron-browser/localizationsService';
import 'vs/workbench/services/encryption/electron-sandbox/encryptionService';
import 'vs/workbench/services/diagnostics/electron-browser/diagnosticsService';
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View file

@ -51,6 +51,7 @@ import 'vs/workbench/services/clipboard/browser/clipboardService';
import 'vs/workbench/services/extensionResourceLoader/browser/extensionResourceLoaderService';
import 'vs/workbench/services/path/browser/pathService';
import 'vs/workbench/services/themes/browser/browserHostColorSchemeService';
import 'vs/workbench/services/encryption/browser/encryptionService';
import 'vs/workbench/services/userDataSync/browser/userDataSyncResourceEnablementService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';