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

258 LOC · 237 covered · 21 uncovered · 47 ranges · 1161 concepts · 12 introducers · 591 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- secrets.ts ×15
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 '../../../base/common/async.js';
7 > import { IEncryptionService } from '../../encryption/common/encryptionService.js';
8 > import { createDecorator } from '../../instantiation/common/instantiation.js';
9 > import { IStorageService, IStorageValueChangeEvent, InMemoryStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
10 > import { Emitter, Event } from '../../../base/common/event.js';
11 > import { ILogService } from '../../log/common/log.js';
12 > import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
13 > import { Lazy } from '../../../base/common/lazy.js';
14 > import { isWindows } from '../../../base/common/platform.js';
15 >
16 > /**
17 > * The storage key prefix used for all secrets.
18 > */
19 > export const SECRET_STORAGE_PREFIX = 'secret://';
20 >
21 > /**
22 > * Builds the full storage key for a secret.
23 > */
24 > export function secretStorageKey(key: string): string {
25 > return `${SECRET_STORAGE_PREFIX}${key}`; secrets.ts ×15
26 > }
28 > /**
29 > * Reads an encrypted secret from storage and decrypts it.
30 > * @param key The secret key (without the `secret://` prefix).
31 > * @param storageGet A function that reads the encrypted value from storage given a full storage key.
32 > * @param decrypt A function that decrypts the encrypted value.
33 > * @param logService Optional logger for trace output.
34 > */
35 > export async function readEncryptedSecret( secrets.ts ×6
36 > key: string,
37 > storageGet: (fullKey: string) => string | undefined,
38 > decrypt: (value: string) => Promise<string>,
39 > logService?: ILogService,
40 > ): Promise<string | undefined> {
41 > const fullKey = secretStorageKey(key);
42 > logService?.trace('[secrets] getting secret for key:', fullKey);
43 > const encrypted = storageGet(fullKey);
44 > if (!encrypted) {
45 > logService?.trace('[secrets] no secret found for key:', fullKey); secrets.ts ×2
46 > return undefined;
47 > }
48 > logService?.trace('[secrets] decrypting secret for key:', fullKey); secrets.ts ×6
49 > const result = await decrypt(encrypted);
50 > logService?.trace('[secrets] decrypted secret for key:', fullKey);
51 > return result;
52 > }
54 > /**
55 > * Encrypts a secret value and writes it to storage.
56 > * @param key The secret key (without the `secret://` prefix).
57 > * @param value The plaintext secret value.
58 > * @param storageSet A function that writes the encrypted value to storage given a full storage key.
59 > * @param encrypt A function that encrypts the plaintext value.
60 > * @param logService Optional logger for trace output.
61 > */
62 > export async function writeEncryptedSecret( secrets.ts ×15
63 > key: string,
64 > value: string,
65 > storageSet: (fullKey: string, encrypted: string) => void,
66 > encrypt: (value: string) => Promise<string>,
67 > logService?: ILogService,
68 > ): Promise<void> {
69 > logService?.trace('[secrets] encrypting secret for key:', key);
70 > const encrypted = await encrypt(value);
71 > const fullKey = secretStorageKey(key);
72 > logService?.trace('[secrets] storing encrypted secret for key:', fullKey);
73 > storageSet(fullKey, encrypted);
74 > logService?.trace('[secrets] stored encrypted secret for key:', fullKey);
75 > }
77 > /**
78 > * Secret keys that should be shared between the VS Code app and the agents app.
79 > * When the agents app starts and doesn't have these secrets, it requests them
80 > * from VS Code via crossAppIPC.
81 > */
82 > export const CROSS_APP_SHARED_SECRET_KEYS: readonly string[] = [
83 > '{"extensionId":"vscode.github-authentication","key":"github.auth"}',
84 > ];
85 >
86 > export const ISecretStorageService = createDecorator<ISecretStorageService>('secretStorageService');
87 >
88 > export interface ISecretStorageProvider {
89 > type: 'in-memory' | 'persisted' | 'unknown';
90 > get(key: string): Promise<string | undefined>;
91 > set(key: string, value: string): Promise<void>;
92 > delete(key: string): Promise<void>;
93 > keys?(): Promise<string[]>;
94 > }
95 >
96 > export interface ISecretStorageService extends ISecretStorageProvider {
97 > readonly _serviceBrand: undefined;
98 > readonly onDidChangeSecret: Event<string>;
99 > }
100 >
101 > export class BaseSecretStorageService extends Disposable implements ISecretStorageService {
102 > declare readonly _serviceBrand: undefined;
103 >
104 > protected readonly onDidChangeSecretEmitter = this._register(new Emitter<string>());
105 > readonly onDidChangeSecret: Event<string> = this.onDidChangeSecretEmitter.event;
106 >
107 > protected readonly _sequencer = new SequencerByKey<string>();
108 >
109 > private _type: 'in-memory' | 'persisted' | 'unknown' = 'unknown';
110 >
111 > private readonly _onDidChangeValueDisposable = this._register(new DisposableStore());
112 >
113 > constructor(
114 > private readonly _useInMemoryStorage: boolean, secrets.ts ×15
115 > @IStorageService private _storageService: IStorageService,
116 > @IEncryptionService protected _encryptionService: IEncryptionService,
117 > @ILogService protected readonly _logService: ILogService,
118 > ) {
119 > super();
120 > }
121 >
122 > protected useSharedStorage(key: string): boolean {
123 > return isWindows && CROSS_APP_SHARED_SECRET_KEYS.includes(key); secrets.ts ×1
124 > }
126 > /**
127 > * @Note initialize must be called first so that this can be resolved properly
128 > * otherwise it will return 'unknown'.
129 > */
130 > get type() {
131 > return this._type; secrets.ts ×1
132 > }
134 > private _lazyStorageService: Lazy<Promise<IStorageService>> = new Lazy(() => this.initialize());
135 > protected get resolvedStorageService() { secrets.ts ×15
136 > return this._lazyStorageService.value; secrets.ts ×15
137 > }
139 > get(key: string): Promise<string | undefined> {
140 > return this._sequencer.queue(key, async () => { secrets.ts ×6
141 > const storageService = await this.resolvedStorageService;
142 >
143 > try {
144 > return await readEncryptedSecret(
145 > key,
146 > (fullKey) => this.getValueFromStorage(key, fullKey, storageService),
147 > // If the storage service is in-memory, we don't need to decrypt
148 > this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.decrypt(v),
149 > this._logService,
150 > );
151 > } catch (e) {
152 this._logService.error(e);
153 this.delete(key);
154 return undefined;
155 }
156 > }); secrets.ts ×6
157 > }
159 > set(key: string, value: string): Promise<void> {
160 > return this._sequencer.queue(key, async () => { secrets.ts ×15
161 > const storageService = await this.resolvedStorageService;
162 >
163 > try {
164 > await writeEncryptedSecret(
165 > key,
166 > value,
167 > (fullKey, encrypted) => this.setValueInStorage(key, fullKey, encrypted, storageService),
168 > // If the storage service is in-memory, we don't need to encrypt
169 > this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.encrypt(v),
170 > this._logService,
171 > );
172 > } catch (e) {
173 this._logService.error(e);
174 throw e;
175 }
176 > }); secrets.ts ×15
177 > }
179 > delete(key: string): Promise<void> {
180 > return this._sequencer.queue(key, async () => { secrets.ts ×2
181 > const storageService = await this.resolvedStorageService;
182 >
183 > const fullKey = secretStorageKey(key);
184 > this._logService.trace('[secrets] deleting secret for key:', fullKey);
185 > const scope = this.useSharedStorage(key) ? StorageScope.APPLICATION_SHARED : StorageScope.APPLICATION;
186 > storageService.remove(fullKey, scope);
187 > this._logService.trace('[secrets] deleted secret for key:', fullKey);
188 > });
189 > }
191 > keys(): Promise<string[]> {
192 return this._sequencer.queue('__keys__', async () => {
193 const storageService = await this.resolvedStorageService;
194 this._logService.trace('[secrets] fetching keys of all secrets');
195 const allKeys = storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE);
196 this._logService.trace('[secrets] fetched keys of all secrets');
197 return allKeys.filter(key => key.startsWith(SECRET_STORAGE_PREFIX)).map(key => key.slice(SECRET_STORAGE_PREFIX.length));
198 });
199 }
201 > private getValueFromStorage(key: string, fullKey: string, storageService: IStorageService): string | undefined {
202 > if (this.useSharedStorage(key)) { secrets.ts ×6
203 > this._logService.trace(`[SecretStorageService] Fetching value for cross-app shared secret: ${fullKey}`); secrets.ts ×1
204 > return storageService.get(fullKey, StorageScope.APPLICATION_SHARED);
205 > }
206 > return storageService.get(fullKey, StorageScope.APPLICATION); secrets.ts ×1
207 > } secrets.ts ×6
209 > private setValueInStorage(key: string, fullKey: string, value: string, storageService: IStorageService): void {
210 > if (this.useSharedStorage(key)) { secrets.ts ×15
211 > this._logService.trace(`[SecretStorageService] Setting value for cross-app shared secret: ${fullKey}`); secrets.ts ×1
212 > storageService.store(fullKey, value, StorageScope.APPLICATION_SHARED, StorageTarget.MACHINE);
213 > return;
214 > }
215 > storageService.store(fullKey, value, StorageScope.APPLICATION, StorageTarget.MACHINE); secrets.ts ×1
216 > } secrets.ts ×15
218 > private async initialize(): Promise<IStorageService> {
219 > let storageService; secrets.ts ×15
220 > if (!this._useInMemoryStorage && await this._encryptionService.isEncryptionAvailable()) {
221 > this._logService.trace(`[SecretStorageService] Encryption is available, using persisted storage`); secrets.ts ×1
222 > this._type = 'persisted';
223 > storageService = this._storageService;
224 > } else { secrets.ts ×15
225 > // If we already have an in-memory storage service, we don't need to recreate it secrets.ts ×2
226 > if (this._type === 'in-memory') {
227 return this._storageService;
228 }
229 > this._logService.trace('[SecretStorageService] Encryption is not available, falling back to in-memory storage'); secrets.ts ×2
230 > this._type = 'in-memory';
231 > storageService = this._register(new InMemoryStorageService());
232 > }
234 > this._onDidChangeValueDisposable.clear();
235 > this._onDidChangeValueDisposable.add(Event.any<IStorageValueChangeEvent>(
236 > storageService.onDidChangeValue(StorageScope.APPLICATION, undefined, this._onDidChangeValueDisposable),
237 > storageService.onDidChangeValue(StorageScope.APPLICATION_SHARED, undefined, this._onDidChangeValueDisposable),
238 > )(e => {
239 > this.onDidChangeValue(e.key);
240 > }));
241 > return storageService;
242 > }
244 > protected reinitialize(): void {
245 this._lazyStorageService = new Lazy(() => this.initialize());
246 }
248 > private onDidChangeValue(key: string): void {
249 > if (!key.startsWith(SECRET_STORAGE_PREFIX)) { secrets.ts ×15
250 return;
251 }
253 > const secretKey = key.slice(SECRET_STORAGE_PREFIX.length);
254 >
255 > this._logService.trace(`[SecretStorageService] Notifying change in value for secret: ${secretKey}`);
256 > this.onDidChangeSecretEmitter.fire(secretKey);
257 > }
258 > } secrets.ts ×15