Skip to content

Commit fb13a28

Browse files
Improve error messages to users where encryption fails/isn't available (microsoft#186037)
1 parent cf5fe4d commit fb13a28

File tree

7 files changed

+201
-43
lines changed

7 files changed

+201
-43
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
77

88
export const IEncryptionService = createDecorator<IEncryptionService>('encryptionService');
9-
export interface IEncryptionService extends ICommonEncryptionService { }
9+
export interface IEncryptionService extends ICommonEncryptionService {
10+
setUsePlainTextEncryption(): Promise<void>;
11+
getKeyStorageProvider(): Promise<KnownStorageProvider>;
12+
}
1013

1114
export const IEncryptionMainService = createDecorator<IEncryptionMainService>('encryptionMainService');
1215
export interface IEncryptionMainService extends IEncryptionService { }
@@ -21,3 +24,34 @@ export interface ICommonEncryptionService {
2124

2225
isEncryptionAvailable(): Promise<boolean>;
2326
}
27+
28+
export const enum KnownStorageProvider {
29+
unknown = 'unknown',
30+
basicText = 'basic_text',
31+
32+
// Linux
33+
gnomeAny = 'gnome_any',
34+
gnomeLibsecret = 'gnome_libsecret',
35+
gnomeKeyring = 'gnome_keyring',
36+
kwallet = 'kwallet',
37+
kwallet5 = 'kwallet5',
38+
kwallet6 = 'kwallet6',
39+
40+
// Windows
41+
dplib = 'dpapi',
42+
43+
// macOS
44+
keychainAccess = 'keychain_access',
45+
}
46+
47+
export function isKwallet(backend: string): boolean {
48+
return backend === KnownStorageProvider.kwallet
49+
|| backend === KnownStorageProvider.kwallet5
50+
|| backend === KnownStorageProvider.kwallet6;
51+
}
52+
53+
export function isGnome(backend: string): boolean {
54+
return backend === KnownStorageProvider.gnomeAny
55+
|| backend === KnownStorageProvider.gnomeLibsecret
56+
|| backend === KnownStorageProvider.gnomeKeyring;
57+
}

src/vs/platform/encryption/electron-main/encryptionMainService.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { safeStorage } from 'electron';
7-
import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService';
6+
import { safeStorage as safeStorageElectron } from 'electron';
7+
import { isMacintosh, isWindows } from 'vs/base/common/platform';
8+
import { KnownStorageProvider, IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService';
89
import { ILogService } from 'vs/platform/log/common/log';
910

11+
// These APIs are currently only supported in our custom build of electron so
12+
// we need to guard against them not being available.
13+
interface ISafeStorageAdditionalAPIs {
14+
setUsePlainTextEncryption(usePlainText: boolean): void;
15+
getSelectedStorageBackend(): string;
16+
}
17+
18+
const safeStorage: typeof import('electron').safeStorage & Partial<ISafeStorageAdditionalAPIs> = safeStorageElectron;
19+
1020
export class EncryptionMainService implements IEncryptionMainService {
1121
_serviceBrand: undefined;
1222

@@ -41,6 +51,40 @@ export class EncryptionMainService implements IEncryptionMainService {
4151
return Promise.resolve(safeStorage.isEncryptionAvailable());
4252
}
4353

54+
getKeyStorageProvider(): Promise<KnownStorageProvider> {
55+
if (isWindows) {
56+
return Promise.resolve(KnownStorageProvider.dplib);
57+
}
58+
if (isMacintosh) {
59+
return Promise.resolve(KnownStorageProvider.keychainAccess);
60+
}
61+
if (safeStorage.getSelectedStorageBackend) {
62+
try {
63+
const result = safeStorage.getSelectedStorageBackend() as KnownStorageProvider;
64+
return Promise.resolve(result);
65+
} catch (e) {
66+
this.logService.error(e);
67+
}
68+
}
69+
return Promise.resolve(KnownStorageProvider.unknown);
70+
}
71+
72+
async setUsePlainTextEncryption(): Promise<void> {
73+
if (isWindows) {
74+
throw new Error('Setting plain text encryption is not supported on Windows.');
75+
}
76+
77+
if (isMacintosh) {
78+
throw new Error('Setting plain text encryption is not supported on macOS.');
79+
}
80+
81+
if (!safeStorage.setUsePlainTextEncryption) {
82+
throw new Error('Setting plain text encryption is not supported.');
83+
}
84+
85+
safeStorage.setUsePlainTextEncryption(true);
86+
}
87+
4488
// TODO: Remove this after a few releases
4589
private async oldDecrypt(value: string): Promise<string> {
4690
let encryption: { decrypt(salt: string, value: string): Promise<string> };

src/vs/platform/secrets/common/secrets.ts

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@
55

66
import { SequencerByKey } from 'vs/base/common/async';
77
import { IEncryptionService } from 'vs/platform/encryption/common/encryptionService';
8-
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
8+
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
99
import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
1010
import { Event, PauseableEmitter } from 'vs/base/common/event';
1111
import { ILogService } from 'vs/platform/log/common/log';
12-
import { isNative } from 'vs/base/common/platform';
13-
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
14-
import { localize } from 'vs/nls';
15-
import { IOpenerService } from 'vs/platform/opener/common/opener';
16-
import { once } from 'vs/base/common/functional';
1712

1813
export const ISecretStorageService = createDecorator<ISecretStorageService>('secretStorageService');
1914

@@ -29,25 +24,23 @@ export interface ISecretStorageService extends ISecretStorageProvider {
2924
onDidChangeSecret: Event<string>;
3025
}
3126

32-
export class SecretStorageService implements ISecretStorageService {
27+
export abstract class BaseSecretStorageService implements ISecretStorageService {
3328
declare readonly _serviceBrand: undefined;
3429

3530
private _storagePrefix = 'secret://';
3631

3732
private readonly _onDidChangeSecret = new PauseableEmitter<string>();
3833
onDidChangeSecret: Event<string> = this._onDidChangeSecret.event;
3934

40-
private readonly _sequencer = new SequencerByKey<string>();
41-
private initialized = this.init();
35+
protected readonly _sequencer = new SequencerByKey<string>();
36+
protected initialized = this.init();
4237

4338
private _type: 'in-memory' | 'persisted' | 'unknown' = 'unknown';
4439

4540
constructor(
4641
@IStorageService private _storageService: IStorageService,
47-
@IEncryptionService private _encryptionService: IEncryptionService,
48-
@IInstantiationService private readonly _instantiationService: IInstantiationService,
49-
@INotificationService private readonly _notificationService: INotificationService,
50-
@ILogService private readonly _logService: ILogService
42+
@IEncryptionService protected _encryptionService: IEncryptionService,
43+
@ILogService protected readonly _logService: ILogService
5144
) {
5245
this._storageService.onDidChangeValue(e => this.onDidChangeValue(e.key));
5346
}
@@ -77,13 +70,18 @@ export class SecretStorageService implements ISecretStorageService {
7770
await this.initialized;
7871

7972
const fullKey = this.getKey(key);
73+
this._logService.trace('[secrets] getting secret for key:', fullKey);
8074
const encrypted = this._storageService.get(fullKey, StorageScope.APPLICATION);
8175
if (!encrypted) {
76+
this._logService.trace('[secrets] no secret found for key:', fullKey);
8277
return undefined;
8378
}
8479

8580
try {
86-
return await this._encryptionService.decrypt(encrypted);
81+
this._logService.trace('[secrets] decrypting gotten secret for key:', fullKey);
82+
const result = await this._encryptionService.decrypt(encrypted);
83+
this._logService.trace('[secrets] decrypted secret for key:', fullKey);
84+
return result;
8785
} catch (e) {
8886
this._logService.error(e);
8987
this.delete(key);
@@ -96,18 +94,23 @@ export class SecretStorageService implements ISecretStorageService {
9694
return this._sequencer.queue(key, async () => {
9795
await this.initialized;
9896

99-
if (isNative && this.type !== 'persisted') {
100-
this.notifyNativeUserOnce();
97+
this._logService.trace('[secrets] encrypting secret for key:', key);
98+
let encrypted;
99+
try {
100+
encrypted = await this._encryptionService.encrypt(value);
101+
} catch (e) {
102+
this._logService.error(e);
103+
throw e;
101104
}
102-
103-
const encrypted = await this._encryptionService.encrypt(value);
104105
const fullKey = this.getKey(key);
105106
try {
106107
this._onDidChangeSecret.pause();
108+
this._logService.trace('[secrets] storing encrypted secret for key:', fullKey);
107109
this._storageService.store(fullKey, encrypted, StorageScope.APPLICATION, StorageTarget.MACHINE);
108110
} finally {
109111
this._onDidChangeSecret.resume();
110112
}
113+
this._logService.trace('[secrets] stored encrypted secret for key:', fullKey);
111114
});
112115
}
113116

@@ -118,10 +121,12 @@ export class SecretStorageService implements ISecretStorageService {
118121
const fullKey = this.getKey(key);
119122
try {
120123
this._onDidChangeSecret.pause();
124+
this._logService.trace('[secrets] deleting secret for key:', fullKey);
121125
this._storageService.remove(fullKey, StorageScope.APPLICATION);
122126
} finally {
123127
this._onDidChangeSecret.resume();
124128
}
129+
this._logService.trace('[secrets] deleted secret for key:', fullKey);
125130
});
126131
}
127132

@@ -140,20 +145,4 @@ export class SecretStorageService implements ISecretStorageService {
140145
private getKey(key: string): string {
141146
return `${this._storagePrefix}${key}`;
142147
}
143-
144-
private notifyNativeUserOnce = once(() => this.notifyNativeUser());
145-
private notifyNativeUser(): void {
146-
this._notificationService.prompt(
147-
Severity.Warning,
148-
localize('notEncrypted', 'Secrets are not being stored on disk because encryption is not available in this environment.'),
149-
[{
150-
label: localize('openTroubleshooting', "Open Troubleshooting"),
151-
run: () => this._instantiationService.invokeFunction(accessor => {
152-
const openerService = accessor.get(IOpenerService);
153-
// Open troubleshooting docs page
154-
return openerService.open('https://go.microsoft.com/fwlink/?linkid=2239490');
155-
})
156-
}]
157-
);
158-
}
159148
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 { once } from 'vs/base/common/functional';
7+
import { isLinux } from 'vs/base/common/platform';
8+
import Severity from 'vs/base/common/severity';
9+
import { localize } from 'vs/nls';
10+
import { IEncryptionService, KnownStorageProvider, isGnome, isKwallet } from 'vs/platform/encryption/common/encryptionService';
11+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
12+
import { ILogService } from 'vs/platform/log/common/log';
13+
import { INativeHostService } from 'vs/platform/native/common/native';
14+
import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification';
15+
import { IOpenerService } from 'vs/platform/opener/common/opener';
16+
import { BaseSecretStorageService } from 'vs/platform/secrets/common/secrets';
17+
import { IStorageService } from 'vs/platform/storage/common/storage';
18+
19+
export class NativeSecretStorageService extends BaseSecretStorageService {
20+
21+
constructor(
22+
@INotificationService private readonly _notificationService: INotificationService,
23+
@IInstantiationService private readonly _instantiationService: IInstantiationService,
24+
@IStorageService storageService: IStorageService,
25+
@IEncryptionService encryptionService: IEncryptionService,
26+
@INativeHostService private readonly _nativeHostService: INativeHostService,
27+
@ILogService logService: ILogService
28+
) {
29+
super(storageService, encryptionService, logService);
30+
}
31+
32+
override set(key: string, value: string): Promise<void> {
33+
this._sequencer.queue(key, async () => {
34+
await this.initialized;
35+
36+
if (this.type !== 'persisted') {
37+
this._logService.trace('[NativeSecretStorageService] Notifying user that secrets are not being stored on disk.');
38+
await this.notifyOfNoEncryptionOnce();
39+
}
40+
41+
});
42+
43+
return this._sequencer.queue(key, () => super.set(key, value));
44+
}
45+
46+
private notifyOfNoEncryptionOnce = once(() => this.notifyOfNoEncryption());
47+
private async notifyOfNoEncryption(): Promise<void> {
48+
const buttons: IPromptChoice[] = [];
49+
const troubleshootingButton: IPromptChoice = {
50+
label: localize('troubleshootingButton', "Open troubleshooting guide"),
51+
run: () => this._instantiationService.invokeFunction(accessor => accessor.get(IOpenerService).open('https://go.microsoft.com/fwlink/?linkid=2239490')),
52+
keepOpen: true
53+
};
54+
buttons.push(troubleshootingButton);
55+
56+
let errorMessage = localize('encryptionNotAvailableJustTroubleshootingGuide', "Secrets are not being stored on disk because encryption is not available in this environment.");
57+
58+
if (!isLinux) {
59+
this._notificationService.prompt(Severity.Error, errorMessage, buttons);
60+
return;
61+
}
62+
63+
const provider = await this._encryptionService.getKeyStorageProvider();
64+
if (isGnome(provider)) {
65+
errorMessage = localize('isGnome', "You're running in a GNOME environment but encryption is not available. Ensure you have gnome-keyring or another libsecret compatible implementation installed and running.");
66+
} else if (isKwallet(provider)) {
67+
errorMessage = localize('isKwallet', "You're running in a KDE environment but encryption is not available. Ensure you have kwallet running.");
68+
} else if (provider === KnownStorageProvider.basicText) {
69+
errorMessage += ' ' + localize('usePlainTextExtraSentence', "Open the troubleshooting guide or you can use plain text obfuscation instead.");
70+
const usePlainTextButton: IPromptChoice = {
71+
label: localize('usePlainText', "Use plain text (restart required)"),
72+
run: async () => {
73+
this._encryptionService.setUsePlainTextEncryption();
74+
await this._nativeHostService.relaunch();
75+
}
76+
};
77+
buttons.unshift(usePlainTextButton);
78+
}
79+
80+
this._notificationService.prompt(Severity.Error, errorMessage, buttons);
81+
}
82+
}

src/vs/workbench/services/encryption/browser/encryptionService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { IEncryptionService } from 'vs/platform/encryption/common/encryptionService';
6+
import { IEncryptionService, KnownStorageProvider } from 'vs/platform/encryption/common/encryptionService';
77
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
88

99
export class EncryptionService implements IEncryptionService {
@@ -21,6 +21,14 @@ export class EncryptionService implements IEncryptionService {
2121
isEncryptionAvailable(): Promise<boolean> {
2222
return Promise.resolve(false);
2323
}
24+
25+
getKeyStorageProvider(): Promise<KnownStorageProvider> {
26+
return Promise.resolve(KnownStorageProvider.basicText);
27+
}
28+
29+
setUsePlainTextEncryption(): Promise<void> {
30+
return Promise.resolve(undefined);
31+
}
2432
}
2533

2634
registerSingleton(IEncryptionService, EncryptionService, InstantiationType.Delayed);

src/vs/workbench/services/secrets/browser/secretStorageService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/
88
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
99
import { ILogService } from 'vs/platform/log/common/log';
1010
import { INotificationService } from 'vs/platform/notification/common/notification';
11-
import { ISecretStorageProvider, ISecretStorageService, SecretStorageService } from 'vs/platform/secrets/common/secrets';
11+
import { ISecretStorageProvider, ISecretStorageService, BaseSecretStorageService } from 'vs/platform/secrets/common/secrets';
1212
import { IStorageService } from 'vs/platform/storage/common/storage';
1313
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
1414

15-
export class BrowserSecretStorageService extends SecretStorageService {
15+
export class BrowserSecretStorageService extends BaseSecretStorageService {
1616

1717
private readonly _secretStorageProvider: ISecretStorageProvider | undefined;
1818

@@ -24,7 +24,7 @@ export class BrowserSecretStorageService extends SecretStorageService {
2424
@INotificationService notificationService: INotificationService,
2525
@ILogService logService: ILogService
2626
) {
27-
super(storageService, encryptionService, instantiationService, notificationService, logService);
27+
super(storageService, encryptionService, logService);
2828

2929
if (environmentService.options?.secretStorageProvider) {
3030
this._secretStorageProvider = environmentService.options.secretStorageProvider;

src/vs/workbench/workbench.desktop.main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,14 @@ import 'vs/workbench/services/extensions/electron-sandbox/nativeExtensionService
9090
import 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService';
9191

9292
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
93-
import { ISecretStorageService, SecretStorageService } from 'vs/platform/secrets/common/secrets';
93+
import { ISecretStorageService } from 'vs/platform/secrets/common/secrets';
94+
import { NativeSecretStorageService } from 'vs/platform/secrets/electron-sandbox/secretStorageService';
9495
import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';
9596
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
9697
import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService';
9798
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
9899

99-
registerSingleton(ISecretStorageService, SecretStorageService, InstantiationType.Delayed);
100+
registerSingleton(ISecretStorageService, NativeSecretStorageService, InstantiationType.Delayed);
100101
registerSingleton(IUserDataInitializationService, new SyncDescriptor(UserDataInitializationService, [[]], true));
101102
registerSingleton(IExtensionsProfileScannerService, ExtensionsProfileScannerService, InstantiationType.Delayed);
102103

0 commit comments

Comments
 (0)