Skip to content

Commit 68a90ab

Browse files
authored
chore(connections): remove keytar, keytar migration, and connection model migration VSCODE-499 (#625)
1 parent 996882c commit 68a90ab

File tree

8 files changed

+54
-977
lines changed

8 files changed

+54
-977
lines changed

.depcheckrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ ignores:
55
- "@types/jest"
66
- "buffer"
77
- "eslint-config-mongodb-js"
8-
- "keytar"
98
- "electron"
109
- "mocha-junit-reporter"
1110
- "mocha-multi"

src/connectionController.ts

Lines changed: 24 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import ConnectionString from 'mongodb-connection-string-url';
88
import { EventEmitter } from 'events';
99
import type { MongoClientOptions } from 'mongodb';
1010
import { v4 as uuidv4 } from 'uuid';
11-
import { createKeytar } from './utils/keytar';
1211
import { CONNECTION_STATUS } from './views/webview-app/extension-app-message-constants';
1312
import { createLogger } from './logging';
14-
import { ext } from './extensionConstants';
1513
import formatError from './utils/formatError';
1614
import type LegacyConnectionModel from './views/webview-app/legacy/connection-model/legacy-connection-model';
1715
import type { SecretStorageLocationType } from './storage/storageController';
@@ -84,41 +82,21 @@ interface ConnectionQuickPicks {
8482
data: { type: NewConnectionType; connectionId?: string };
8583
}
8684

87-
type StoreConnectionInfoWithConnectionModel = StoreConnectionInfo &
88-
Required<Pick<StoreConnectionInfo, 'connectionModel'>>;
89-
9085
type StoreConnectionInfoWithConnectionOptions = StoreConnectionInfo &
9186
Required<Pick<StoreConnectionInfo, 'connectionOptions'>>;
9287

9388
type StoreConnectionInfoWithSecretStorageLocation = StoreConnectionInfo &
9489
Required<Pick<StoreConnectionInfo, 'secretStorageLocation'>>;
9590

96-
type MigratedStoreConnectionInfoWithConnectionOptions =
97-
StoreConnectionInfoWithConnectionOptions &
98-
StoreConnectionInfoWithSecretStorageLocation;
99-
100-
type FailedMigrationConnectionDescriptor = {
101-
connectionId: string;
102-
connectionName: string;
103-
};
104-
105-
export const keytarMigrationFailedMessage = (failedConnections: number) => {
106-
// Writing the message this way to keep it readable.
107-
return [
108-
`Unable to migrate ${failedConnections} of your cluster connections from the deprecated Keytar to the VS Code SecretStorage.`,
109-
'Keytar is officially archived and not being maintained.',
110-
'In an effort to promote good security practices by not depending on an archived piece of software, VS Code removes Keytar shim in favour of built-in storage utility for secrets.',
111-
'Please review your connections and delete or recreate those with missing secrets.',
112-
'You can still access your secrets via OS keychain.',
113-
].join(' ');
114-
};
91+
type LoadedConnection = StoreConnectionInfoWithConnectionOptions &
92+
StoreConnectionInfoWithSecretStorageLocation;
11593

11694
export default class ConnectionController {
11795
// This is a map of connection ids to their configurations.
11896
// These connections can be saved on the session (runtime),
11997
// on the workspace, or globally in vscode.
12098
_connections: {
121-
[connectionId: string]: MigratedStoreConnectionInfoWithConnectionOptions;
99+
[connectionId: string]: LoadedConnection;
122100
} = Object.create(null);
123101
_activeDataService: DataService | null = null;
124102
_storageController: StorageController;
@@ -168,67 +146,19 @@ export default class ConnectionController {
168146
),
169147
});
170148

171-
// A list of connection ids that we could not migrate in previous load of
172-
// connections because of Keytar not being available.
173-
const connectionIdsThatDidNotMigrateEarlier =
174-
globalAndWorkspaceConnections.reduce<string[]>(
175-
(ids, [connectionId, connectionInfo]) => {
176-
if (
177-
connectionInfo.secretStorageLocation ===
178-
SecretStorageLocation.KeytarSecondAttempt
179-
) {
180-
return [...ids, connectionId];
181-
}
182-
return ids;
183-
},
184-
[]
185-
);
186-
187-
const hasConnectionsThatDidNotMigrateEarlier =
188-
!!globalAndWorkspaceConnections.some(
189-
([, connectionInfo]) =>
190-
!connectionInfo.secretStorageLocation ||
191-
connectionInfo.secretStorageLocation === 'vscode.Keytar'
192-
);
193-
194-
if (hasConnectionsThatDidNotMigrateEarlier) {
195-
try {
196-
ext.keytarModule =
197-
ext.keytarModule === undefined ? createKeytar() : ext.keytarModule;
198-
} catch (err) {
199-
// Couldn't load keytar, proceed without storing & loading connections.
200-
}
201-
}
202-
203-
// A list of connection descriptors that we could not migration in the
204-
// current load of connections because of Keytar not being available.
205-
const connectionsThatDidNotMigrate = (
206-
await Promise.all(
207-
globalAndWorkspaceConnections.map<
208-
Promise<FailedMigrationConnectionDescriptor | undefined>
209-
>(async ([connectionId, connectionInfo]) => {
149+
await Promise.all(
150+
globalAndWorkspaceConnections.map(
151+
async ([connectionId, connectionInfo]) => {
210152
const connectionInfoWithSecrets =
211153
await this._getConnectionInfoWithSecrets(connectionInfo);
212154
if (!connectionInfoWithSecrets) {
213155
return;
214156
}
215157

216158
this._connections[connectionId] = connectionInfoWithSecrets;
217-
const connectionSecretsInKeytar =
218-
connectionInfoWithSecrets.secretStorageLocation ===
219-
SecretStorageLocation.KeytarSecondAttempt;
220-
if (
221-
connectionSecretsInKeytar &&
222-
!connectionIdsThatDidNotMigrateEarlier.includes(connectionId)
223-
) {
224-
return {
225-
connectionId,
226-
connectionName: connectionInfo.name,
227-
};
228-
}
229-
})
159+
}
230160
)
231-
).filter((conn): conn is FailedMigrationConnectionDescriptor => !!conn);
161+
);
232162

233163
const loadedConnections = Object.values(this._connections);
234164
if (loadedConnections.length) {
@@ -240,69 +170,39 @@ export default class ConnectionController {
240170
/* this._telemetryService.trackSavedConnectionsLoaded({
241171
saved_connections: globalAndWorkspaceConnections.length,
242172
loaded_connections: loadedConnections.length,
243-
connections_with_secrets_in_keytar: loadedConnections.filter(
244-
(connection) =>
245-
connection.secretStorageLocation === SecretStorageLocation.Keytar ||
246-
connection.secretStorageLocation ===
247-
SecretStorageLocation.KeytarSecondAttempt
248173
).length,
249174
connections_with_secrets_in_secret_storage: loadedConnections.filter(
250175
(connection) =>
251176
connection.secretStorageLocation ===
252177
SecretStorageLocation.SecretStorage
253178
).length,
254179
}); */
255-
256-
if (connectionsThatDidNotMigrate.length) {
257-
log.error(
258-
`Could not migrate secrets for ${connectionsThatDidNotMigrate.length} connections`,
259-
connectionsThatDidNotMigrate
260-
);
261-
this._telemetryService.trackKeytarSecretsMigrationFailed({
262-
saved_connections: globalAndWorkspaceConnections.length,
263-
loaded_connections: loadedConnections.length,
264-
connections_with_failed_keytar_migration:
265-
connectionsThatDidNotMigrate.length,
266-
});
267-
void vscode.window.showInformationMessage(
268-
keytarMigrationFailedMessage(connectionsThatDidNotMigrate.length)
269-
);
270-
}
271180
}
272181

182+
// TODO: Move this into the connectionStorage.
273183
async _getConnectionInfoWithSecrets(
274184
connectionInfo: StoreConnectionInfo
275-
): Promise<MigratedStoreConnectionInfoWithConnectionOptions | undefined> {
185+
): Promise<LoadedConnection | undefined> {
276186
try {
277-
if (connectionInfo.connectionModel) {
278-
return await this._migrateConnectionWithConnectionModel(
279-
connectionInfo as StoreConnectionInfoWithConnectionModel
280-
);
281-
}
282-
283-
if (
284-
!connectionInfo.secretStorageLocation ||
285-
connectionInfo.secretStorageLocation === 'vscode.Keytar'
286-
) {
287-
return await this._migrateConnectionWithKeytarSecrets(
288-
connectionInfo as StoreConnectionInfoWithConnectionOptions
289-
);
290-
}
291-
292187
// We tried migrating this connection earlier but failed because Keytar was not
293188
// available. So we return simply the connection without secrets.
294189
if (
190+
connectionInfo.connectionModel ||
191+
!connectionInfo.secretStorageLocation ||
192+
connectionInfo.secretStorageLocation === 'vscode.Keytar' ||
295193
connectionInfo.secretStorageLocation ===
296-
SecretStorageLocation.KeytarSecondAttempt
194+
SecretStorageLocation.KeytarSecondAttempt
297195
) {
298-
return connectionInfo as MigratedStoreConnectionInfoWithConnectionOptions;
196+
// We had migrations in VSCode for ~5 months. We drop the connections
197+
// that did not migrate.
198+
return undefined;
299199
}
300200

301201
const unparsedSecrets =
302202
(await this._storageController.getSecret(connectionInfo.id)) ?? '';
303203

304204
return this._mergedConnectionInfoWithSecrets(
305-
connectionInfo as MigratedStoreConnectionInfoWithConnectionOptions,
205+
connectionInfo as LoadedConnection,
306206
unparsedSecrets
307207
);
308208
} catch (error) {
@@ -311,66 +211,10 @@ export default class ConnectionController {
311211
}
312212
}
313213

314-
async _migrateConnectionWithConnectionModel(
315-
savedConnectionInfo: StoreConnectionInfoWithConnectionModel
316-
): Promise<MigratedStoreConnectionInfoWithConnectionOptions | undefined> {
317-
// Transform a raw connection model from storage to an ampersand model.
318-
const newConnectionInfoWithSecrets = convertConnectionModelToInfo(
319-
savedConnectionInfo.connectionModel
320-
);
321-
322-
const connectionInfoWithSecrets = {
323-
id: savedConnectionInfo.id,
324-
name: savedConnectionInfo.name,
325-
storageLocation: savedConnectionInfo.storageLocation,
326-
secretStorageLocation: SecretStorageLocation.SecretStorage,
327-
connectionOptions: newConnectionInfoWithSecrets.connectionOptions,
328-
};
329-
330-
await this._saveConnectionWithSecrets(connectionInfoWithSecrets);
331-
return connectionInfoWithSecrets;
332-
}
333-
334-
async _migrateConnectionWithKeytarSecrets(
335-
savedConnectionInfo: StoreConnectionInfoWithConnectionOptions
336-
): Promise<MigratedStoreConnectionInfoWithConnectionOptions | undefined> {
337-
// If the Keytar module is not available, we simply mark the connections
338-
// storage as Keytar and return
339-
if (!ext.keytarModule) {
340-
log.error('Could not migrate Keytar secrets, module not found');
341-
return await this._storageController.saveConnection<MigratedStoreConnectionInfoWithConnectionOptions>(
342-
{
343-
...savedConnectionInfo,
344-
secretStorageLocation: SecretStorageLocation.KeytarSecondAttempt,
345-
}
346-
);
347-
}
348-
349-
// If there is nothing in keytar, we will save an empty object as secrets in
350-
// new storage and mark this connection as migrated
351-
const keytarSecrets =
352-
(await ext.keytarModule.getPassword(
353-
this._serviceName,
354-
savedConnectionInfo.id
355-
)) || '{}';
356-
357-
const migratedConnectionInfoWithSecrets =
358-
this._mergedConnectionInfoWithSecrets(
359-
{
360-
...savedConnectionInfo,
361-
secretStorageLocation: SecretStorageLocation.SecretStorage,
362-
},
363-
keytarSecrets
364-
);
365-
366-
await this._saveConnectionWithSecrets(migratedConnectionInfoWithSecrets);
367-
368-
return migratedConnectionInfoWithSecrets;
369-
}
370-
371-
_mergedConnectionInfoWithSecrets<
372-
T extends StoreConnectionInfoWithConnectionOptions
373-
>(connectionInfo: T, unparsedSecrets: string) {
214+
_mergedConnectionInfoWithSecrets(
215+
connectionInfo: LoadedConnection,
216+
unparsedSecrets: string
217+
): LoadedConnection {
374218
if (!unparsedSecrets) {
375219
return connectionInfo;
376220
}
@@ -490,8 +334,8 @@ export default class ConnectionController {
490334
}
491335

492336
private async _saveConnectionWithSecrets(
493-
newStoreConnectionInfoWithSecrets: MigratedStoreConnectionInfoWithConnectionOptions
494-
): Promise<MigratedStoreConnectionInfoWithConnectionOptions> {
337+
newStoreConnectionInfoWithSecrets: LoadedConnection
338+
): Promise<LoadedConnection> {
495339
// We don't want to store secrets to disc.
496340
const { connectionInfo: safeConnectionInfo, secrets } = extractSecrets(
497341
newStoreConnectionInfoWithSecrets as ConnectionInfoFromLegacyDS
@@ -729,15 +573,6 @@ export default class ConnectionController {
729573

730574
private async _removeSecretsFromKeychain(connectionId: string) {
731575
await this._storageController.deleteSecret(connectionId);
732-
// Even though we migrated to SecretStorage from keytar, we are still removing
733-
// secrets from keytar to make sure that we don't leave any left-overs when a
734-
// connection is removed. This block can safely be removed after our migration
735-
// has been out for some time.
736-
try {
737-
await ext.keytarModule?.deletePassword(this._serviceName, connectionId);
738-
} catch (error) {
739-
log.error('Failed to remove secret from keytar', error);
740-
}
741576
}
742577

743578
async removeSavedConnection(connectionId: string): Promise<void> {

src/extensionConstants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { ExtensionContext } from 'vscode';
2-
import type { KeytarInterface } from './utils/keytar';
32

43
// eslint-disable-next-line @typescript-eslint/no-namespace
54
export namespace ext {
65
export let context: ExtensionContext;
7-
export let keytarModule: KeytarInterface | undefined;
86
}
97

108
export function getImagesPath(): string {

src/storage/storageController.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ export type ConnectionsFromStorage = {
3232
[connectionId: string]: StoreConnectionInfo;
3333
};
3434

35+
// Keytar is deprecated and no longer used. All new
36+
// connections use 'SecretStorage'.
3537
export const SecretStorageLocation = {
3638
Keytar: 'vscode.Keytar',
3739
KeytarSecondAttempt: 'vscode.KeytarSecondAttempt',
40+
3841
SecretStorage: 'vscode.SecretStorage',
3942
} as const;
4043

0 commit comments

Comments
 (0)