mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-31 00:10:04 +08:00
[cherry-pick] Revert "Share secrets between Code and Agents app via macOS Keychain" (#312735)
Co-authored-by: vs-code-engineering[bot] <vs-code-engineering[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
88406fc3c6
commit
dd05fac328
@@ -159,14 +159,6 @@ vsda/**
|
||||
@vscode/policy-watcher/index.d.ts
|
||||
!@vscode/policy-watcher/build/Release/vscode-policy-watcher.node
|
||||
|
||||
@vscode/macos-keychain/build/**
|
||||
@vscode/macos-keychain/src/**
|
||||
@vscode/macos-keychain/test/**
|
||||
@vscode/macos-keychain/binding.gyp
|
||||
@vscode/macos-keychain/README.md
|
||||
@vscode/macos-keychain/index.d.ts
|
||||
!@vscode/macos-keychain/build/Release/keychainNative.node
|
||||
|
||||
@vscode/windows-ca-certs/**/*
|
||||
!@vscode/windows-ca-certs/package.json
|
||||
!@vscode/windows-ca-certs/**/*.node
|
||||
|
||||
@@ -10,9 +10,5 @@
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)com.microsoft.vscode.shared-secrets</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -332,16 +332,6 @@ steps:
|
||||
BUILD_SOURCESDIRECTORY: $(Build.SourcesDirectory)
|
||||
displayName: ✍️ Codesign & Notarize
|
||||
|
||||
# Re-sign the app without the provisioning profile for tests.
|
||||
# This strips the keychain-access-groups entitlement which requires a
|
||||
# provisioning profile and is not needed for running tests. The codesign
|
||||
# step reads from the archives packaged above which have the full entitlements.
|
||||
- script: |
|
||||
set -e
|
||||
export CODESIGN_IDENTITY=$(security find-identity -v -p codesigning $(agent.tempdirectory)/buildagent.keychain | grep -oEi "([0-9A-F]{40})" | head -n 1)
|
||||
DEBUG=electron-osx-sign* node build/darwin/sign.ts --skip-provisioning-profile $(agent.builddirectory)
|
||||
displayName: Set Hardened Entitlements (for tests)
|
||||
|
||||
- ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}:
|
||||
- template: product-build-darwin-test.yml@self
|
||||
parameters:
|
||||
|
||||
@@ -18,14 +18,7 @@ function getElectronVersion(): string {
|
||||
return target;
|
||||
}
|
||||
|
||||
const mainProvisioningProfilePath = path.join(baseDir, 'darwin', 'main.provisionprofile');
|
||||
const agentsProvisioningProfilePath = path.join(baseDir, 'darwin', 'agents.provisionprofile');
|
||||
|
||||
function hasProvisioningProfile(): boolean {
|
||||
return fs.existsSync(mainProvisioningProfilePath);
|
||||
}
|
||||
|
||||
function getEntitlementsForFile(filePath: string, tempDir: string, useProvisioningProfile: boolean, teamId?: string): string {
|
||||
function getEntitlementsForFile(filePath: string): string {
|
||||
if (filePath.includes(' Helper (GPU).app')) {
|
||||
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist');
|
||||
} else if (filePath.includes(' Helper (Renderer).app')) {
|
||||
@@ -33,51 +26,7 @@ function getEntitlementsForFile(filePath: string, tempDir: string, useProvisioni
|
||||
} else if (filePath.includes(' Helper (Plugin).app')) {
|
||||
return path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist');
|
||||
}
|
||||
const entitlementsPath = path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist');
|
||||
if (!useProvisioningProfile) {
|
||||
// Without a provisioning profile, keychain-access-groups entitlement
|
||||
// will cause signing failures. Strip it from the entitlements plist.
|
||||
return getStrippedEntitlements(entitlementsPath, tempDir);
|
||||
}
|
||||
if (teamId) {
|
||||
return getExpandedEntitlements(entitlementsPath, tempDir, teamId);
|
||||
}
|
||||
return entitlementsPath;
|
||||
}
|
||||
|
||||
let _strippedEntitlementsPath: string | undefined;
|
||||
|
||||
/**
|
||||
* Returns a path to a copy of the entitlements plist with the
|
||||
* keychain-access-groups key removed.
|
||||
*/
|
||||
function getStrippedEntitlements(entitlementsPath: string, tempDir: string): string {
|
||||
if (!_strippedEntitlementsPath) {
|
||||
const content = fs.readFileSync(entitlementsPath, 'utf8');
|
||||
const stripped = content.replace(
|
||||
/\s*<key>keychain-access-groups<\/key>\s*<array>[\s\S]*?<\/array>/,
|
||||
''
|
||||
);
|
||||
_strippedEntitlementsPath = path.join(tempDir, 'app-entitlements-stripped.plist');
|
||||
fs.writeFileSync(_strippedEntitlementsPath, stripped);
|
||||
}
|
||||
return _strippedEntitlementsPath;
|
||||
}
|
||||
|
||||
let expandedEntitlementsPath: string | undefined;
|
||||
|
||||
/**
|
||||
* Returns a path to a copy of the entitlements plist with
|
||||
* $(TeamIdentifierPrefix) expanded to the actual team identifier.
|
||||
*/
|
||||
function getExpandedEntitlements(entitlementsPath: string, tempDir: string, teamId: string): string {
|
||||
if (!expandedEntitlementsPath) {
|
||||
const content = fs.readFileSync(entitlementsPath, 'utf8');
|
||||
const expanded = content.replace(/\$\(TeamIdentifierPrefix\)/g, teamId + '.');
|
||||
expandedEntitlementsPath = path.join(tempDir, 'app-entitlements.plist');
|
||||
fs.writeFileSync(expandedEntitlementsPath, expanded);
|
||||
}
|
||||
return expandedEntitlementsPath;
|
||||
return path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist');
|
||||
}
|
||||
|
||||
async function retrySignOnKeychainError<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T> {
|
||||
@@ -109,7 +58,7 @@ async function retrySignOnKeychainError<T>(fn: () => Promise<T>, maxRetries: num
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function main(buildDir?: string, skipProvisioningProfile?: boolean): Promise<void> {
|
||||
async function main(buildDir?: string): Promise<void> {
|
||||
const tempDir = process.env['AGENT_TEMPDIRECTORY'];
|
||||
const arch = process.env['VSCODE_ARCH'];
|
||||
const identity = process.env['CODESIGN_IDENTITY'];
|
||||
@@ -129,42 +78,15 @@ async function main(buildDir?: string, skipProvisioningProfile?: boolean): Promi
|
||||
? path.resolve(appRoot, appName, 'Contents', 'Applications', `${product.embedded.nameLong}.app`, 'Contents', 'Info.plist')
|
||||
: undefined;
|
||||
|
||||
const useProvisioningProfile = !skipProvisioningProfile && hasProvisioningProfile();
|
||||
const resolvedProvisioningProfile = useProvisioningProfile ? mainProvisioningProfilePath : undefined;
|
||||
|
||||
let teamId: string | undefined;
|
||||
if (resolvedProvisioningProfile) {
|
||||
const profilePlist = await spawn('security', ['cms', '-D', '-i', resolvedProvisioningProfile]);
|
||||
const teamIdMatch = /<key>TeamIdentifier<\/key>\s*<array>\s*<string>(.*?)<\/string>/s.exec(profilePlist);
|
||||
if (teamIdMatch) {
|
||||
teamId = teamIdMatch[1];
|
||||
console.log(`Extracted TeamIdentifier from provisioning profile: ${teamId}`);
|
||||
} else {
|
||||
console.warn('Could not extract TeamIdentifier from provisioning profile; $(TeamIdentifierPrefix) will not be expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// Embed the agents provisioning profile into the embedded app bundle
|
||||
// before signing, since @electron/osx-sign only supports one top-level profile.
|
||||
if (useProvisioningProfile && product.embedded && fs.existsSync(agentsProvisioningProfilePath)) {
|
||||
const embeddedAppPath = path.join(appRoot, appName, 'Contents', 'Applications', `${product.embedded.nameLong}.app`);
|
||||
if (fs.existsSync(embeddedAppPath)) {
|
||||
const embeddedProfileDest = path.join(embeddedAppPath, 'Contents', 'embedded.provisionprofile');
|
||||
fs.copyFileSync(agentsProvisioningProfilePath, embeddedProfileDest);
|
||||
console.log(`Embedded agents provisioning profile into ${embeddedProfileDest}`);
|
||||
}
|
||||
}
|
||||
|
||||
const appOpts: SignOptions = {
|
||||
app: path.join(appRoot, appName),
|
||||
platform: 'darwin',
|
||||
optionsForFile: (filePath) => ({
|
||||
entitlements: getEntitlementsForFile(filePath, tempDir, useProvisioningProfile, teamId),
|
||||
entitlements: getEntitlementsForFile(filePath),
|
||||
hardenedRuntime: true,
|
||||
}),
|
||||
preAutoEntitlements: false,
|
||||
preEmbedProvisioningProfile: !!resolvedProvisioningProfile,
|
||||
provisioningProfile: resolvedProvisioningProfile,
|
||||
preEmbedProvisioningProfile: false,
|
||||
keychain: path.join(tempDir, 'buildagent.keychain'),
|
||||
version: getElectronVersion(),
|
||||
identity,
|
||||
@@ -172,8 +94,7 @@ async function main(buildDir?: string, skipProvisioningProfile?: boolean): Promi
|
||||
|
||||
// Only overwrite plist entries for x64 and arm64 builds,
|
||||
// universal will get its copy from the x64 build.
|
||||
// Skip when re-signing (skipProvisioningProfile) since entries already exist.
|
||||
if (arch !== 'universal' && !skipProvisioningProfile) {
|
||||
if (arch !== 'universal') {
|
||||
await spawn('plutil', [
|
||||
'-insert',
|
||||
'NSAppleEventsUsageDescription',
|
||||
@@ -250,19 +171,10 @@ async function main(buildDir?: string, skipProvisioningProfile?: boolean): Promi
|
||||
}
|
||||
|
||||
await retrySignOnKeychainError(() => sign(appOpts));
|
||||
|
||||
// Dump entitlements from the signed binary for diagnostic purposes
|
||||
const mainBinary = path.join(appRoot, appName, 'Contents', 'MacOS', product.nameShort);
|
||||
console.log(`Dumping entitlements from signed binary: ${mainBinary}`);
|
||||
const entitlementsDump = await spawn('codesign', ['--display', '--entitlements', '-', '--xml', mainBinary]);
|
||||
console.log(`Signed entitlements:\n${entitlementsDump}`);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
const skipProvisioningProfile = args.includes('--skip-provisioning-profile');
|
||||
const buildDir = args.filter(a => !a.startsWith('--'))[0];
|
||||
main(buildDir, skipProvisioningProfile).catch(async err => {
|
||||
main(process.argv[2]).catch(async err => {
|
||||
console.error(err);
|
||||
const tempDir = process.env['AGENT_TEMPDIRECTORY'];
|
||||
if (tempDir) {
|
||||
|
||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -171,7 +171,6 @@
|
||||
"yaserver": "^0.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vscode/macos-keychain": "^0.0.1",
|
||||
"windows-foreground-love": "0.6.1"
|
||||
}
|
||||
},
|
||||
@@ -3812,31 +3811,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/macos-keychain": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/macos-keychain/-/macos-keychain-0.0.1.tgz",
|
||||
"integrity": "sha512-8R5eKUZRoRUJvmoKgPrXFlEpBg6n8XKq0jyA85DLDuO1ZMbGuKsu2KsUCl7jWm06+h0ajZXUF0Z7dkk6j4IguA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"node-addon-api": "^8.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/macos-keychain/node_modules/node-addon-api": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz",
|
||||
"integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/native-watchdog": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/native-watchdog/-/native-watchdog-1.4.6.tgz",
|
||||
|
||||
@@ -267,7 +267,6 @@
|
||||
"url": "https://github.com/microsoft/vscode/issues"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vscode/macos-keychain": "^0.0.1",
|
||||
"windows-foreground-love": "0.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"win32TunnelServiceMutex": "vscodeoss-tunnelservice",
|
||||
"win32TunnelMutex": "vscodeoss-tunnel",
|
||||
"darwinBundleIdentifier": "com.visualstudio.code.oss",
|
||||
"darwinSharedKeychainServiceName": "com.visualstudio.code.oss.shared-secrets",
|
||||
"darwinProfileUUID": "47827DD9-4734-49A0-AF80-7E19B11495CC",
|
||||
"darwinProfilePayloadUUID": "CF808BE7-53F3-46C6-A7E2-7EDB98A5E959",
|
||||
"linuxIconName": "code-oss",
|
||||
|
||||
15
src/typings/macos-keychain.d.ts
vendored
15
src/typings/macos-keychain.d.ts
vendored
@@ -1,15 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Type declarations for @vscode/macos-keychain.
|
||||
// The package is an optional dependency (macOS-only native addon), so types
|
||||
// are duplicated here to ensure TypeScript compilation succeeds on all platforms.
|
||||
|
||||
declare module '@vscode/macos-keychain' {
|
||||
export function keychainSet(service: string, account: string, value: string): void;
|
||||
export function keychainGet(service: string, account: string): string | undefined;
|
||||
export function keychainDelete(service: string, account: string): boolean;
|
||||
export function keychainList(service: string): string[];
|
||||
}
|
||||
@@ -224,7 +224,6 @@ export interface IProductConfiguration {
|
||||
readonly darwinUniversalAssetId?: string;
|
||||
readonly darwinBundleIdentifier?: string;
|
||||
readonly darwinSiblingBundleIdentifier?: string;
|
||||
readonly darwinSharedKeychainServiceName?: string;
|
||||
readonly profileTemplatesUrl?: string;
|
||||
|
||||
readonly commonlyUsedSettings?: string[];
|
||||
|
||||
@@ -37,8 +37,6 @@ import { DiagnosticsMainService, IDiagnosticsMainService } from '../../platform/
|
||||
import { DialogMainService, IDialogMainService } from '../../platform/dialogs/electron-main/dialogMainService.js';
|
||||
import { IEncryptionMainService } from '../../platform/encryption/common/encryptionService.js';
|
||||
import { EncryptionMainService } from '../../platform/encryption/electron-main/encryptionMainService.js';
|
||||
import { ISharedKeychainMainService } from '../../platform/secrets/common/sharedKeychainService.js';
|
||||
import { SharedKeychainMainService } from '../../platform/secrets/electron-main/sharedKeychainMainService.js';
|
||||
import { ipcBrowserViewChannelName } from '../../platform/browserView/common/browserView.js';
|
||||
import { ipcBrowserViewGroupChannelName } from '../../platform/browserView/common/browserViewGroup.js';
|
||||
import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js';
|
||||
@@ -1094,9 +1092,6 @@ export class CodeApplication extends Disposable {
|
||||
// Encryption
|
||||
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService));
|
||||
|
||||
// Shared Keychain
|
||||
services.set(ISharedKeychainMainService, new SyncDescriptor(SharedKeychainMainService));
|
||||
|
||||
// Cross-app IPC
|
||||
services.set(ICrossAppIPCService, new SyncDescriptor(CrossAppIPCService));
|
||||
|
||||
@@ -1275,12 +1270,12 @@ export class CodeApplication extends Disposable {
|
||||
this._register(new MacOSCrossAppSecretSharing(
|
||||
accessor.get(IStorageMainService),
|
||||
accessor.get(IEncryptionMainService),
|
||||
accessor.get(ISharedKeychainMainService),
|
||||
accessor.get(IStateService),
|
||||
this.logService,
|
||||
this.environmentMainService,
|
||||
accessor.get(ILaunchMainService),
|
||||
this.lifecycleMainService,
|
||||
crossAppIPCService,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1297,10 +1292,6 @@ export class CodeApplication extends Disposable {
|
||||
const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel('encryption', encryptionChannel);
|
||||
|
||||
// Shared Keychain
|
||||
const sharedKeychainChannel = ProxyChannel.fromService(accessor.get(ISharedKeychainMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel('sharedKeychain', sharedKeychainChannel);
|
||||
|
||||
// Browser View
|
||||
const browserViewChannel = ProxyChannel.fromService(accessor.get(IBrowserViewMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel);
|
||||
|
||||
@@ -76,6 +76,8 @@ export async function writeEncryptedSecret(
|
||||
|
||||
/**
|
||||
* Secret keys that should be shared between the VS Code app and the agents app.
|
||||
* When the agents app starts and doesn't have these secrets, it requests them
|
||||
* from VS Code via crossAppIPC.
|
||||
*/
|
||||
export const CROSS_APP_SHARED_SECRET_KEYS: readonly string[] = [
|
||||
'{"extensionId":"vscode.github-authentication","key":"github.auth"}',
|
||||
@@ -135,14 +137,7 @@ export class BaseSecretStorageService extends Disposable implements ISecretStora
|
||||
}
|
||||
|
||||
get(key: string): Promise<string | undefined> {
|
||||
return this._sequencer.queue(key, () => this._doGet(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the safeStorage+SQLite pipeline without going through the sequencer.
|
||||
* Must only be called from within a sequencer-queued task for the same key.
|
||||
*/
|
||||
protected async _doGet(key: string): Promise<string | undefined> {
|
||||
return this._sequencer.queue(key, async () => {
|
||||
const storageService = await this.resolvedStorageService;
|
||||
|
||||
try {
|
||||
@@ -158,17 +153,11 @@ export class BaseSecretStorageService extends Disposable implements ISecretStora
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set(key: string, value: string): Promise<void> {
|
||||
return this._sequencer.queue(key, () => this._doSet(key, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the safeStorage+SQLite pipeline without going through the sequencer.
|
||||
* Must only be called from within a sequencer-queued task for the same key.
|
||||
*/
|
||||
protected async _doSet(key: string, value: string): Promise<void> {
|
||||
return this._sequencer.queue(key, async () => {
|
||||
const storageService = await this.resolvedStorageService;
|
||||
|
||||
try {
|
||||
@@ -184,17 +173,11 @@ export class BaseSecretStorageService extends Disposable implements ISecretStora
|
||||
this._logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete(key: string): Promise<void> {
|
||||
return this._sequencer.queue(key, () => this._doDelete(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from the safeStorage+SQLite pipeline without going through the sequencer.
|
||||
* Must only be called from within a sequencer-queued task for the same key.
|
||||
*/
|
||||
protected async _doDelete(key: string): Promise<void> {
|
||||
return this._sequencer.queue(key, async () => {
|
||||
const storageService = await this.resolvedStorageService;
|
||||
|
||||
const fullKey = secretStorageKey(key);
|
||||
@@ -202,22 +185,17 @@ export class BaseSecretStorageService extends Disposable implements ISecretStora
|
||||
const scope = this.useSharedStorage(key) ? StorageScope.APPLICATION_SHARED : StorageScope.APPLICATION;
|
||||
storageService.remove(fullKey, scope);
|
||||
this._logService.trace('[secrets] deleted secret for key:', fullKey);
|
||||
});
|
||||
}
|
||||
|
||||
keys(): Promise<string[]> {
|
||||
return this._sequencer.queue('__keys__', () => this._doGetKeys());
|
||||
}
|
||||
|
||||
/**
|
||||
* List all secret keys from the safeStorage+SQLite pipeline without going through the sequencer.
|
||||
* Must only be called from within a sequencer-queued task.
|
||||
*/
|
||||
protected async _doGetKeys(): Promise<string[]> {
|
||||
return this._sequencer.queue('__keys__', async () => {
|
||||
const storageService = await this.resolvedStorageService;
|
||||
this._logService.trace('[secrets] fetching keys of all secrets');
|
||||
const allKeys = storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
this._logService.trace('[secrets] fetched keys of all secrets');
|
||||
return allKeys.filter(key => key.startsWith(SECRET_STORAGE_PREFIX)).map(key => key.slice(SECRET_STORAGE_PREFIX.length));
|
||||
});
|
||||
}
|
||||
|
||||
private getValueFromStorage(key: string, fullKey: string, storageService: IStorageService): string | undefined {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 '../../instantiation/common/instantiation.js';
|
||||
|
||||
/**
|
||||
* Provides shared keychain access between Code and the embedded Agents app
|
||||
* via a macOS keychain access group. On non-macOS platforms the implementation
|
||||
* is a no-op (returns undefined/empty for all operations).
|
||||
*/
|
||||
export const ISharedKeychainService = createDecorator<ISharedKeychainService>('sharedKeychainService');
|
||||
|
||||
export interface ISharedKeychainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
get(key: string): Promise<string | undefined>;
|
||||
set(key: string, value: string): Promise<void>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
keys(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export const ISharedKeychainMainService = createDecorator<ISharedKeychainMainService>('sharedKeychainMainService');
|
||||
|
||||
export interface ISharedKeychainMainService extends ISharedKeychainService { }
|
||||
@@ -5,49 +5,75 @@
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { dirname } from '../../../base/common/path.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IEncryptionMainService } from '../../encryption/common/encryptionService.js';
|
||||
import { IStorageMainService } from '../../storage/electron-main/storageMainService.js';
|
||||
import { CROSS_APP_SHARED_SECRET_KEYS, readEncryptedSecret } from '../common/secrets.js';
|
||||
import { CROSS_APP_SHARED_SECRET_KEYS, secretStorageKey, readEncryptedSecret, writeEncryptedSecret } from '../common/secrets.js';
|
||||
import { IStateService } from '../../state/node/state.js';
|
||||
import { INodeProcess, isMacintosh } from '../../../base/common/platform.js';
|
||||
import { IStorageMain } from '../../storage/electron-main/storageMain.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
import { ILaunchMainService } from '../../launch/electron-main/launchMainService.js';
|
||||
import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ISharedKeychainMainService } from '../common/sharedKeychainService.js';
|
||||
import { ICrossAppIPCService } from '../../crossAppIpc/electron-main/crossAppIpcService.js';
|
||||
|
||||
const MIGRATION_STATE_KEY = 'sharedKeychain.migrationDone';
|
||||
const HOST_SPAWN_STATE_KEY = 'sharedKeychain.hostSpawnDone';
|
||||
const MIGRATION_STATE_KEY = 'crossAppSecretSharing.migrationDone';
|
||||
|
||||
/**
|
||||
* Message types exchanged between apps over crossAppIPC for secret sharing.
|
||||
*/
|
||||
const enum CrossAppSecretMessageType {
|
||||
/** Agents → Host: Request secrets */
|
||||
SecretRequest = 'secrets/request',
|
||||
/** Host → Agents: Response with secrets */
|
||||
SecretResponse = 'secrets/response',
|
||||
/** Agents → Host: Confirms secrets were stored, both sides mark migration done */
|
||||
SecretAck = 'secrets/ack',
|
||||
}
|
||||
|
||||
interface CrossAppSecretMessage {
|
||||
type: CrossAppSecretMessageType;
|
||||
data?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates one-time secret migration between the VS Code app and the
|
||||
* agents app via the macOS shared keychain (macOS only).
|
||||
* agents app using Electron's crossAppIPC (macOS only).
|
||||
*
|
||||
* Each app migrates its own secrets from safeStorage+SQLite into the
|
||||
* shared keychain on startup. The agents app also spawns Code.app
|
||||
* (once) with `--share-secrets-with-agents-app` to trigger Code's
|
||||
* migration if the shared keychain doesn't yet contain all expected
|
||||
* keys.
|
||||
* **Demand-driven**: Only the agents app initiates migration. If it
|
||||
* detects that migration hasn't been done yet, it:
|
||||
* 1. Waits for the crossAppIPC connection (managed by ICrossAppIPCService).
|
||||
* 2. Spawns Code.app with `--share-secrets-with-agents-app`, which
|
||||
* either starts Code.app fresh or (if already running) forwards
|
||||
* the arg to the existing instance via the node IPC socket.
|
||||
* 3. Code.app creates its own crossAppIPC connection when it sees
|
||||
* the arg, and the two connect.
|
||||
* 4. Agents app sends `SecretRequest` → Code.app responds with
|
||||
* `SecretResponse` → Agents app sends `SecretAck`.
|
||||
* 5. Both sides mark migration as done. Code.app quits if it was
|
||||
* launched solely for this purpose.
|
||||
*
|
||||
* After migration, both apps read from and write to the shared keychain
|
||||
* for cross-app secret keys (via {@link NativeSecretStorageService}).
|
||||
* Security: crossAppIPC uses code-signature verification (Mach ports
|
||||
* on macOS) — the kernel authenticates both endpoints. No secrets are
|
||||
* ever in process args, files, or network.
|
||||
*/
|
||||
export class MacOSCrossAppSecretSharing extends Disposable {
|
||||
|
||||
private readonly isEmbeddedApp: boolean;
|
||||
private readonly applicationStorage: IStorageMain;
|
||||
private _onHostMigrationComplete: (() => void) | undefined;
|
||||
private readonly hostHandshakeListeners = this._register(new DisposableStore());
|
||||
|
||||
constructor(
|
||||
storageMainService: IStorageMainService,
|
||||
private readonly encryptionMainService: IEncryptionMainService,
|
||||
private readonly sharedKeychainMainService: ISharedKeychainMainService,
|
||||
private readonly stateService: IStateService,
|
||||
private readonly logService: ILogService,
|
||||
environmentMainService: IEnvironmentMainService,
|
||||
launchMainService: ILaunchMainService,
|
||||
lifecycleMainService: ILifecycleMainService,
|
||||
private readonly crossAppIPCService: ICrossAppIPCService,
|
||||
) {
|
||||
super();
|
||||
this.isEmbeddedApp = !!(process as INodeProcess).isEmbeddedApp;
|
||||
@@ -61,96 +87,143 @@ export class MacOSCrossAppSecretSharing extends Disposable {
|
||||
lifecycleMainService: ILifecycleMainService,
|
||||
): void {
|
||||
if (this.isEmbeddedApp) {
|
||||
// Agents app: migrate own secrets + spawn Code.app if needed
|
||||
// Agents app: initiate migration if needed
|
||||
this.initializeAsAgentsApp();
|
||||
} else if (environmentMainService.args['share-secrets-with-agents-app']) {
|
||||
// Code.app launched with --share-secrets-with-agents-app:
|
||||
// migrate secrets to shared keychain, then quit if no other reason to stay
|
||||
// Code.app launched fresh with --share-secrets-with-agents-app:
|
||||
// respond to the agents app's request, then quit if no other reason to stay
|
||||
const hasOtherArgs = environmentMainService.args._.length > 0 || environmentMainService.args['folder-uri'] || environmentMainService.args['file-uri'];
|
||||
this.migrateSecrets().then(() => {
|
||||
if (!hasOtherArgs) {
|
||||
this.initializeAsHostApp(hasOtherArgs ? undefined : () => {
|
||||
this.logService.info('[CrossAppSecretSharing] Host app was launched for migration only, quitting');
|
||||
lifecycleMainService.quit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Code.app normal startup: migrate own secrets
|
||||
this.migrateSecrets();
|
||||
// Also respond to spawn requests from the agents app
|
||||
// Code.app already running: listen for --share-secrets-with-agents-app
|
||||
// forwarded from a second instance via the launch service
|
||||
this._register(launchMainService.onDidRequestShareSecrets(() => {
|
||||
this.migrateSecrets();
|
||||
this.initializeAsHostApp();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeAsAgentsApp(): Promise<void> {
|
||||
if (!isMacintosh) {
|
||||
if (!isMacintosh || !this.isEmbeddedApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate own secrets (if any) to shared keychain
|
||||
await this.migrateSecrets();
|
||||
|
||||
// If we've already spawned Code.app before, don't do it again
|
||||
if (this.stateService.getItem<boolean>(HOST_SPAWN_STATE_KEY, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the shared keychain has all expected keys
|
||||
let needsHostMigration = false;
|
||||
for (const key of CROSS_APP_SHARED_SECRET_KEYS) {
|
||||
if (await this.sharedKeychainMainService.get(key) === undefined) {
|
||||
needsHostMigration = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsHostMigration) {
|
||||
this.logService.info('[CrossAppSecretSharing] Shared keychain incomplete, spawning host app');
|
||||
this.spawnHostApp();
|
||||
}
|
||||
|
||||
// Mark that we've attempted the host spawn (don't retry on next startup)
|
||||
this.stateService.setItem(HOST_SPAWN_STATE_KEY, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates this app's secrets from safeStorage+SQLite to the shared keychain.
|
||||
* Idempotent — skips if already done.
|
||||
*/
|
||||
private async migrateSecrets(): Promise<void> {
|
||||
if (!isMacintosh) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stateService.getItem<boolean>(MIGRATION_STATE_KEY, false)) {
|
||||
if (this.isMigrationDone()) {
|
||||
this.logService.trace('[CrossAppSecretSharing] Migration already done, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for storage to be ready before we start — handleSecretResponse
|
||||
// will write secrets into applicationStorage.
|
||||
await this.applicationStorage.whenInit;
|
||||
|
||||
this.logService.info('[CrossAppSecretSharing] Starting shared keychain migration');
|
||||
if (!this.crossAppIPCService.initialized) {
|
||||
this.logService.info('[CrossAppSecretSharing] crossAppIPC not initialized, skipping migration');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info('[CrossAppSecretSharing] Migration needed, starting...');
|
||||
|
||||
// Listen for connection — when connected, request secrets
|
||||
this._register(this.crossAppIPCService.onDidConnect(isServer => {
|
||||
this.logService.info(`[CrossAppSecretSharing] Connected (isServer=${isServer}), requesting secrets from host app`);
|
||||
this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretRequest });
|
||||
}));
|
||||
|
||||
// Listen for messages
|
||||
this._register(this.crossAppIPCService.onDidReceiveMessage(msg => {
|
||||
const secretMsg = msg as CrossAppSecretMessage;
|
||||
if (secretMsg?.type === CrossAppSecretMessageType.SecretResponse) {
|
||||
this.handleSecretResponse(secretMsg.data ?? {});
|
||||
}
|
||||
}));
|
||||
|
||||
// If already connected (e.g. service was initialized before storage was ready),
|
||||
// send the request immediately.
|
||||
if (this.crossAppIPCService.connected) {
|
||||
this.logService.info(`[CrossAppSecretSharing] Already connected (isServer=${this.crossAppIPCService.isServer}), requesting secrets from host app`);
|
||||
this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretRequest });
|
||||
}
|
||||
|
||||
// Spawn Code.app with --share-secrets-with-agents-app
|
||||
this.spawnHostApp();
|
||||
|
||||
// Timeout: if migration doesn't complete within 30s, give up
|
||||
setTimeout(() => {
|
||||
if (!this.isMigrationDone()) {
|
||||
this.logService.warn('[CrossAppSecretSharing] Migration timed out');
|
||||
}
|
||||
}, 30_000);
|
||||
}
|
||||
|
||||
private async initializeAsHostApp(onComplete?: () => void): Promise<void> {
|
||||
if (!isMacintosh || this.isEmbeddedApp) {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMigrationDone()) {
|
||||
this.logService.trace('[CrossAppSecretSharing] Migration already done, skipping');
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for application storage to be fully initialized before
|
||||
// checking for secrets — storage may still be in-memory at this
|
||||
// point during early startup.
|
||||
await this.applicationStorage.whenInit;
|
||||
|
||||
if (!this.hasAnySharedSecrets()) {
|
||||
this.logService.trace('[CrossAppSecretSharing] No shared secrets to share, skipping');
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.crossAppIPCService.initialized) {
|
||||
this.logService.info('[CrossAppSecretSharing] crossAppIPC not initialized');
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
this._onHostMigrationComplete = onComplete;
|
||||
|
||||
this.logService.info('[CrossAppSecretSharing] Host app responding to secret sharing request');
|
||||
|
||||
// Dispose previous listeners if initializeAsHostApp is called again
|
||||
// (e.g. via repeated onDidRequestShareSecrets events).
|
||||
this.hostHandshakeListeners.clear();
|
||||
|
||||
// Listen for messages from the agents app
|
||||
this.hostHandshakeListeners.add(this.crossAppIPCService.onDidReceiveMessage(msg => {
|
||||
const secretMsg = msg as CrossAppSecretMessage;
|
||||
if (secretMsg?.type === CrossAppSecretMessageType.SecretRequest) {
|
||||
this.handleSecretRequest();
|
||||
} else if (secretMsg?.type === CrossAppSecretMessageType.SecretAck) {
|
||||
this.handleSecretAck();
|
||||
}
|
||||
}));
|
||||
|
||||
// If disconnected before ack, still allow the host to quit
|
||||
this.hostHandshakeListeners.add(this.crossAppIPCService.onDidDisconnect(() => {
|
||||
this._onHostMigrationComplete?.();
|
||||
this._onHostMigrationComplete = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
private isMigrationDone(): boolean {
|
||||
return this.stateService.getItem<boolean>(MIGRATION_STATE_KEY, false);
|
||||
}
|
||||
|
||||
private hasAnySharedSecrets(): boolean {
|
||||
for (const key of CROSS_APP_SHARED_SECRET_KEYS) {
|
||||
try {
|
||||
const decrypted = await readEncryptedSecret(
|
||||
key,
|
||||
(fullKey) => this.applicationStorage.get(fullKey),
|
||||
(value) => this.encryptionMainService.decrypt(value),
|
||||
this.logService,
|
||||
);
|
||||
if (decrypted !== undefined) {
|
||||
await this.sharedKeychainMainService.set(key, decrypted);
|
||||
this.logService.trace('[CrossAppSecretSharing] Migrated key to shared keychain:', key);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logService.error('[CrossAppSecretSharing] Failed to migrate key:', key, err);
|
||||
if (this.applicationStorage.get(secretStorageKey(key)) !== undefined) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.stateService.setItem(MIGRATION_STATE_KEY, true);
|
||||
this.logService.info('[CrossAppSecretSharing] Migration complete');
|
||||
return false;
|
||||
}
|
||||
|
||||
private spawnHostApp(): void {
|
||||
@@ -174,4 +247,69 @@ export class MacOSCrossAppSecretSharing extends Disposable {
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
|
||||
private async handleSecretRequest(): Promise<void> {
|
||||
this.logService.info('[CrossAppSecretSharing] Host app handling secret request');
|
||||
|
||||
const secrets: Record<string, string> = {};
|
||||
|
||||
for (const key of CROSS_APP_SHARED_SECRET_KEYS) {
|
||||
try {
|
||||
const decrypted = await readEncryptedSecret(
|
||||
key,
|
||||
(fullKey) => this.applicationStorage.get(fullKey),
|
||||
(value) => this.encryptionMainService.decrypt(value),
|
||||
this.logService,
|
||||
);
|
||||
if (decrypted !== undefined) {
|
||||
secrets[key] = decrypted;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logService.error('[CrossAppSecretSharing] Failed to read secret for key:', key, err);
|
||||
}
|
||||
}
|
||||
|
||||
this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretResponse, data: secrets });
|
||||
this.logService.info('[CrossAppSecretSharing] Sent secrets response with', Object.keys(secrets).length, 'keys');
|
||||
}
|
||||
|
||||
private async handleSecretResponse(secrets: Record<string, string>): Promise<void> {
|
||||
this.logService.info('[CrossAppSecretSharing] Agents app received', Object.keys(secrets).length, 'secrets');
|
||||
|
||||
for (const [key, value] of Object.entries(secrets)) {
|
||||
if (!CROSS_APP_SHARED_SECRET_KEYS.includes(key)) {
|
||||
this.logService.warn('[CrossAppSecretSharing] Ignoring unexpected key:', key);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await writeEncryptedSecret(
|
||||
key,
|
||||
value,
|
||||
(fullKey, encrypted) => this.applicationStorage.set(fullKey, encrypted),
|
||||
(v) => this.encryptionMainService.encrypt(v),
|
||||
this.logService,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logService.error('[CrossAppSecretSharing] Failed to store secret for key:', key, err);
|
||||
}
|
||||
}
|
||||
|
||||
this.stateService.setItem(MIGRATION_STATE_KEY, true);
|
||||
this.logService.info('[CrossAppSecretSharing] Migration complete');
|
||||
|
||||
// Tell the host app migration is done so it can also record it.
|
||||
// Don't close here — let the host close first after receiving the ack.
|
||||
this.crossAppIPCService.sendMessage({ type: CrossAppSecretMessageType.SecretAck });
|
||||
}
|
||||
|
||||
private handleSecretAck(): void {
|
||||
this.stateService.setItem(MIGRATION_STATE_KEY, true);
|
||||
this.logService.info('[CrossAppSecretSharing] Host app received ack, migration complete on both sides');
|
||||
|
||||
const onComplete = this._onHostMigrationComplete;
|
||||
this._onHostMigrationComplete = undefined;
|
||||
|
||||
onComplete?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isMacintosh } from '../../../base/common/platform.js';
|
||||
import { ISharedKeychainMainService } from '../common/sharedKeychainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
|
||||
type KeychainModule = typeof import('@vscode/macos-keychain');
|
||||
|
||||
export class SharedKeychainMainService implements ISharedKeychainMainService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _modulePromise: Promise<KeychainModule> | undefined;
|
||||
private readonly serviceName: string;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor(
|
||||
@IProductService productService: IProductService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
this.enabled = isMacintosh && !!productService.darwinSharedKeychainServiceName;
|
||||
this.serviceName = productService.darwinSharedKeychainServiceName ?? '';
|
||||
}
|
||||
|
||||
private getModule(): Promise<KeychainModule> {
|
||||
if (!this._modulePromise) {
|
||||
this._modulePromise = import('@vscode/macos-keychain');
|
||||
}
|
||||
return this._modulePromise;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | undefined> {
|
||||
if (!this.enabled) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const mod = await this.getModule();
|
||||
const value = mod.keychainGet(this.serviceName, key);
|
||||
this.logService.trace('[SharedKeychainMainService] get:', key, value !== undefined ? '(found)' : '(not found)');
|
||||
return value;
|
||||
} catch (err) {
|
||||
this.logService.error('[SharedKeychainMainService] get failed:', key, err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, value: string): Promise<void> {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const mod = await this.getModule();
|
||||
mod.keychainSet(this.serviceName, key, value);
|
||||
this.logService.trace('[SharedKeychainMainService] set:', key);
|
||||
} catch (err) {
|
||||
this.logService.error('[SharedKeychainMainService] set failed:', key, err);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const mod = await this.getModule();
|
||||
const deleted = mod.keychainDelete(this.serviceName, key);
|
||||
this.logService.trace('[SharedKeychainMainService] delete:', key, deleted ? '(deleted)' : '(not found)');
|
||||
return deleted;
|
||||
} catch (err) {
|
||||
this.logService.error('[SharedKeychainMainService] delete failed:', key, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
if (!this.enabled) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const mod = await this.getModule();
|
||||
const result = mod.keychainList(this.serviceName);
|
||||
this.logService.trace('[SharedKeychainMainService] keys: found', result.length, 'entries');
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.logService.error('[SharedKeychainMainService] keys failed:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,6 @@ import '../workbench/services/mcp/electron-browser/mcpWorkbenchManagementService
|
||||
import '../workbench/services/encryption/electron-browser/encryptionService.js';
|
||||
import '../workbench/services/imageResize/electron-browser/imageResizeService.js';
|
||||
import '../workbench/services/secrets/electron-browser/secretStorageService.js';
|
||||
import '../workbench/services/secrets/electron-browser/sharedKeychainService.js';
|
||||
import '../workbench/services/localization/electron-browser/languagePackService.js';
|
||||
import '../workbench/services/telemetry/electron-browser/telemetryService.js';
|
||||
import '../workbench/services/extensions/electron-browser/extensionHostStarter.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createSingleCallFunction } from '../../../../base/common/functional.js';
|
||||
import { isLinux, isMacintosh } from '../../../../base/common/platform.js';
|
||||
import { isLinux } from '../../../../base/common/platform.js';
|
||||
import Severity from '../../../../base/common/severity.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
@@ -14,8 +14,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { INotificationService, IPromptChoice } from '../../../../platform/notification/common/notification.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import { BaseSecretStorageService, CROSS_APP_SHARED_SECRET_KEYS, ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
|
||||
import { ISharedKeychainService } from '../../../../platform/secrets/common/sharedKeychainService.js';
|
||||
import { BaseSecretStorageService, ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
import { IJSONEditingService } from '../../configuration/common/jsonEditing.js';
|
||||
|
||||
@@ -27,7 +26,6 @@ export class NativeSecretStorageService extends BaseSecretStorageService {
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IJSONEditingService private readonly _jsonEditingService: IJSONEditingService,
|
||||
@INativeEnvironmentService private readonly _environmentService: INativeEnvironmentService,
|
||||
@ISharedKeychainService private readonly _sharedKeychainService: ISharedKeychainService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IEncryptionService encryptionService: IEncryptionService,
|
||||
@ILogService logService: ILogService
|
||||
@@ -40,20 +38,6 @@ export class NativeSecretStorageService extends BaseSecretStorageService {
|
||||
);
|
||||
}
|
||||
|
||||
override get(key: string): Promise<string | undefined> {
|
||||
return this._sequencer.queue(key, async () => {
|
||||
if (isMacintosh && this.type !== 'in-memory' && CROSS_APP_SHARED_SECRET_KEYS.includes(key)) {
|
||||
// Try shared keychain first
|
||||
const value = await this._sharedKeychainService.get(key);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
// Fall back to old safeStorage+SQLite pipeline
|
||||
return this._doGet(key);
|
||||
});
|
||||
}
|
||||
|
||||
override set(key: string, value: string): Promise<void> {
|
||||
this._sequencer.queue(key, async () => {
|
||||
await this.resolvedStorageService;
|
||||
@@ -62,42 +46,10 @@ export class NativeSecretStorageService extends BaseSecretStorageService {
|
||||
this._logService.trace('[NativeSecretStorageService] Notifying user that secrets are not being stored on disk.');
|
||||
await this.notifyOfNoEncryptionOnce();
|
||||
}
|
||||
});
|
||||
return this._sequencer.queue(key, async () => {
|
||||
if (isMacintosh && this.type !== 'in-memory' && CROSS_APP_SHARED_SECRET_KEYS.includes(key)) {
|
||||
// Write to shared keychain
|
||||
await this._sharedKeychainService.set(key, value);
|
||||
}
|
||||
// Also write to legacy pipeline
|
||||
await this._doSet(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
override delete(key: string): Promise<void> {
|
||||
return this._sequencer.queue(key, async () => {
|
||||
if (isMacintosh && this.type !== 'in-memory' && CROSS_APP_SHARED_SECRET_KEYS.includes(key)) {
|
||||
// Delete from shared keychain
|
||||
await this._sharedKeychainService.delete(key);
|
||||
}
|
||||
// Delete from legacy pipeline
|
||||
await this._doDelete(key);
|
||||
});
|
||||
}
|
||||
|
||||
override async keys(): Promise<string[]> {
|
||||
return this._sequencer.queue('__keys__', async () => {
|
||||
const legacyKeys = await this._doGetKeys();
|
||||
if (isMacintosh && this.type !== 'in-memory') {
|
||||
// Include any cross-app shared keys present in the shared keychain
|
||||
for (const sharedKey of CROSS_APP_SHARED_SECRET_KEYS) {
|
||||
const sharedValue = await this._sharedKeychainService.get(sharedKey);
|
||||
if (sharedValue !== undefined && !legacyKeys.includes(sharedKey)) {
|
||||
legacyKeys.push(sharedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return legacyKeys;
|
||||
});
|
||||
return super.set(key, value);
|
||||
}
|
||||
|
||||
private notifyOfNoEncryptionOnce = createSingleCallFunction(() => this.notifyOfNoEncryption());
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerMainProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js';
|
||||
import { ISharedKeychainService } from '../../../../platform/secrets/common/sharedKeychainService.js';
|
||||
|
||||
registerMainProcessRemoteService(ISharedKeychainService, 'sharedKeychain');
|
||||
@@ -59,7 +59,6 @@ import './services/mcp/electron-browser/mcpWorkbenchManagementService.js';
|
||||
import './services/encryption/electron-browser/encryptionService.js';
|
||||
import './services/imageResize/electron-browser/imageResizeService.js';
|
||||
import './services/secrets/electron-browser/secretStorageService.js';
|
||||
import './services/secrets/electron-browser/sharedKeychainService.js';
|
||||
import './services/localization/electron-browser/languagePackService.js';
|
||||
import './services/telemetry/electron-browser/telemetryService.js';
|
||||
import './services/extensions/electron-browser/extensionHostStarter.js';
|
||||
|
||||
Reference in New Issue
Block a user