Skip to content

Commit b07469d

Browse files
First move off of keytar (microsoft#185077)
* First move off of keytar Since keytar is now deprecated, we need a solution going forward. That solution is the electron safeStorage API. This PR: * Uses the Electron safeStorage API for encryption * Since we have encrypted strings we then store them in the StorageService (at the application & machine level) This PR also refactors things quite a bit... a diagram of the change is going to be in the PR. It gives embedders the ability to override the behavior of the secret storage similar to the existing Credential Provider embedder API... only with a better API surface since we no longer need to conform to keytar's shape. More will come after this PR such as: * Converting all CredentialService usages to SecretStorageService usages After a while: * Removing MainThreadKeytar * Removing all the old code marked in this PR * Use InMemoryStorageService * use pausable emitter
1 parent 9e5edb7 commit b07469d

File tree

14 files changed

+468
-49
lines changed

14 files changed

+468
-49
lines changed

src/vs/code/electron-main/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'
3737
import { DiagnosticsMainService, IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService';
3838
import { DialogMainService, IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
3939
import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService';
40-
import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService';
40+
import { EncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService';
4141
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
4242
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
4343
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';

src/vs/platform/encryption/common/encryptionService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
77

8-
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
8+
export const IEncryptionService = createDecorator<IEncryptionService>('encryptionService');
9+
export interface IEncryptionService extends ICommonEncryptionService { }
910

10-
export interface IEncryptionMainService extends ICommonEncryptionService { }
11+
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
12+
export interface IEncryptionMainService extends IEncryptionService { }
1113

1214
export interface ICommonEncryptionService {
1315

@@ -16,4 +18,6 @@ export interface ICommonEncryptionService {
1618
encrypt(value: string): Promise<string>;
1719

1820
decrypt(value: string): Promise<string>;
21+
22+
isEncryptionAvailable(): Promise<boolean>;
1923
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { safeStorage } from 'electron';
7+
import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService';
8+
import { ILogService } from 'vs/platform/log/common/log';
9+
10+
export class EncryptionMainService implements IEncryptionMainService {
11+
_serviceBrand: undefined;
12+
13+
constructor(
14+
private readonly machineId: string,
15+
@ILogService private readonly logService: ILogService
16+
) { }
17+
18+
async encrypt(value: string): Promise<string> {
19+
return JSON.stringify(safeStorage.encryptString(value));
20+
}
21+
22+
async decrypt(value: string): Promise<string> {
23+
let parsedValue: { data: string };
24+
try {
25+
parsedValue = JSON.parse(value);
26+
if (!parsedValue.data) {
27+
this.logService.trace('[EncryptionMainService] Unable to parse encrypted value. Attempting old decryption.');
28+
return this.oldDecrypt(value);
29+
}
30+
} catch (e) {
31+
this.logService.trace('[EncryptionMainService] Unable to parse encrypted value. Attempting old decryption.', e);
32+
return this.oldDecrypt(value);
33+
}
34+
const bufferToDecrypt = Buffer.from(parsedValue.data);
35+
36+
this.logService.trace('[EncryptionMainService] Decrypting value.');
37+
return safeStorage.decryptString(bufferToDecrypt);
38+
}
39+
40+
isEncryptionAvailable(): Promise<boolean> {
41+
return Promise.resolve(safeStorage.isEncryptionAvailable());
42+
}
43+
44+
// TODO: Remove this after a few releases
45+
private async oldDecrypt(value: string): Promise<string> {
46+
let encryption: { decrypt(salt: string, value: string): Promise<string> };
47+
try {
48+
encryption = await new Promise((resolve, reject) => require(['vscode-encrypt'], resolve, reject));
49+
} catch (e) {
50+
return value;
51+
}
52+
53+
try {
54+
return encryption.decrypt(this.machineId, value);
55+
} catch (e) {
56+
this.logService.error(e);
57+
return value;
58+
}
59+
}
60+
}

src/vs/platform/encryption/node/encryptionMainService.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,13 @@ export class EncryptionMainService implements ICommonEncryptionService {
5252
return value;
5353
}
5454
}
55+
56+
async isEncryptionAvailable(): Promise<boolean> {
57+
try {
58+
await this.encryption();
59+
return true;
60+
} catch (e) {
61+
return false;
62+
}
63+
}
5564
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { SequencerByKey } from 'vs/base/common/async';
7+
import { IEncryptionService } from 'vs/platform/encryption/common/encryptionService';
8+
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
9+
import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
10+
import { Event, PauseableEmitter } from 'vs/base/common/event';
11+
import { ILogService } from 'vs/platform/log/common/log';
12+
13+
export const ISecretStorageService = createDecorator<ISecretStorageService>('secretStorageService');
14+
15+
export interface ISecretStorageProvider {
16+
get(key: string): Promise<string | undefined>;
17+
set(key: string, value: string): Promise<void>;
18+
delete(key: string): Promise<void>;
19+
}
20+
21+
export interface ISecretStorageService extends ISecretStorageProvider {
22+
readonly _serviceBrand: undefined;
23+
onDidChangeSecret: Event<string>;
24+
}
25+
26+
export class SecretStorageService implements ISecretStorageService {
27+
declare readonly _serviceBrand: undefined;
28+
29+
private _storagePrefix = 'secret://';
30+
31+
private readonly _onDidChangeSecret = new PauseableEmitter<string>();
32+
onDidChangeSecret: Event<string> = this._onDidChangeSecret.event;
33+
34+
private readonly _sequencer = new SequencerByKey<string>();
35+
private initialized = this.init();
36+
37+
constructor(
38+
@IStorageService private _storageService: IStorageService,
39+
@IEncryptionService private _encryptionService: IEncryptionService,
40+
@ILogService private readonly _logService: ILogService,
41+
) {
42+
this._storageService.onDidChangeValue(e => this.onDidChangeValue(e.key));
43+
}
44+
45+
private onDidChangeValue(key: string): void {
46+
if (!key.startsWith(this._storagePrefix)) {
47+
return;
48+
}
49+
50+
if (this._onDidChangeSecret.isPaused) {
51+
this._logService.trace(`[SecretStorageService] Skipping change event for secret: ${key} because it is paused`);
52+
return;
53+
}
54+
55+
const secretKey = key.slice(this._storagePrefix.length);
56+
57+
this._logService.trace(`[SecretStorageService] Notifying change in value for secret: ${secretKey}`);
58+
this._onDidChangeSecret.fire(secretKey);
59+
}
60+
61+
get(key: string): Promise<string | undefined> {
62+
return this._sequencer.queue(key, async () => {
63+
await this.initialized;
64+
65+
const fullKey = this.getKey(key);
66+
const encrypted = this._storageService.get(fullKey, StorageScope.APPLICATION);
67+
if (!encrypted) {
68+
return undefined;
69+
}
70+
71+
try {
72+
return await this._encryptionService.decrypt(encrypted);
73+
} catch (e) {
74+
this._logService.error(e);
75+
this.delete(key);
76+
return undefined;
77+
}
78+
});
79+
}
80+
81+
set(key: string, value: string): Promise<void> {
82+
return this._sequencer.queue(key, async () => {
83+
await this.initialized;
84+
85+
const encrypted = await this._encryptionService.encrypt(value);
86+
const fullKey = this.getKey(key);
87+
try {
88+
this._onDidChangeSecret.pause();
89+
this._storageService.store(fullKey, encrypted, StorageScope.APPLICATION, StorageTarget.MACHINE);
90+
} finally {
91+
this._onDidChangeSecret.resume();
92+
}
93+
});
94+
}
95+
96+
delete(key: string): Promise<void> {
97+
return this._sequencer.queue(key, async () => {
98+
await this.initialized;
99+
100+
const fullKey = this.getKey(key);
101+
try {
102+
this._onDidChangeSecret.pause();
103+
this._storageService.remove(fullKey, StorageScope.APPLICATION);
104+
} finally {
105+
this._onDidChangeSecret.resume();
106+
}
107+
});
108+
}
109+
110+
private async init(): Promise<void> {
111+
if (await this._encryptionService.isEncryptionAvailable()) {
112+
return;
113+
}
114+
115+
this._logService.trace('[SecretStorageService] Encryption is not available, falling back to in-memory storage');
116+
117+
this._storageService = new InMemoryStorageService();
118+
}
119+
120+
private getKey(key: string): string {
121+
return `${this._storagePrefix}${key}`;
122+
}
123+
}

src/vs/server/node/serverServices.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import { CredentialsWebMainService } from 'vs/platform/credentials/node/credenti
2020
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
2121
import { IDownloadService } from 'vs/platform/download/common/download';
2222
import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc';
23-
import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService';
24-
import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService';
2523
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
2624
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
2725
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -202,8 +200,6 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
202200
const ptyService = instantiationService.createInstance(PtyHostService, ptyHostStarter);
203201
services.set(IPtyService, ptyService);
204202

205-
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId]));
206-
207203
services.set(ICredentialsMainService, new SyncDescriptor(CredentialsWebMainService));
208204

209205
instantiationService.invokeFunction(accessor => {
@@ -230,9 +226,6 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
230226
const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority));
231227
socketServer.registerChannel('extensions', channel);
232228

233-
const encryptionChannel = ProxyChannel.fromService<RemoteAgentConnectionContext>(accessor.get(IEncryptionMainService));
234-
socketServer.registerChannel('encryption', encryptionChannel);
235-
236229
const credentialsChannel = ProxyChannel.fromService<RemoteAgentConnectionContext>(accessor.get(ICredentialsMainService));
237230
socketServer.registerChannel('credentials', credentialsChannel);
238231

0 commit comments

Comments
 (0)