Skip to content

Commit a68b804

Browse files
committed
fix: handle corrupted VSCode auth cache with automatic cleanup and recovery
1 parent 2fb5a74 commit a68b804

File tree

3 files changed

+96
-53
lines changed

3 files changed

+96
-53
lines changed

extensions/vscode/src/stubs/SecretStorage.ts

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,50 @@ export class SecretStorage {
5959
}
6060

6161
async decrypt(filePath: string): Promise<string> {
62-
const key = await this.getOrCreateEncryptionKey();
63-
const data = fs.readFileSync(filePath);
64-
65-
const salt = data.subarray(0, this.saltLength);
66-
const iv = data.subarray(this.saltLength, this.saltLength + this.ivLength);
67-
const tag = data.subarray(
68-
this.saltLength + this.ivLength,
69-
this.saltLength + this.ivLength + this.tagLength,
70-
);
71-
const encrypted = data.subarray(
72-
this.saltLength + this.ivLength + this.tagLength,
73-
);
74-
75-
const decipher: crypto.DecipherGCM = crypto.createDecipheriv(
76-
this.algorithm,
77-
key,
78-
iv,
79-
) as crypto.DecipherGCM;
80-
decipher.setAuthTag(tag);
81-
82-
const decrypted = Buffer.concat([
83-
decipher.update(encrypted),
84-
decipher.final(),
85-
]);
86-
return decrypted.toString("utf8");
62+
try {
63+
const key = await this.getOrCreateEncryptionKey();
64+
const data = fs.readFileSync(filePath);
65+
66+
// Validate minimum data size to detect corruption early
67+
const minSize = this.saltLength + this.ivLength + this.tagLength;
68+
if (data.length < minSize) {
69+
throw new Error(
70+
`Corrupted cache file: insufficient data (${data.length} bytes, expected at least ${minSize})`,
71+
);
72+
}
73+
74+
const salt = data.subarray(0, this.saltLength);
75+
const iv = data.subarray(
76+
this.saltLength,
77+
this.saltLength + this.ivLength,
78+
);
79+
const tag = data.subarray(
80+
this.saltLength + this.ivLength,
81+
this.saltLength + this.ivLength + this.tagLength,
82+
);
83+
const encrypted = data.subarray(
84+
this.saltLength + this.ivLength + this.tagLength,
85+
);
86+
87+
const decipher: crypto.DecipherGCM = crypto.createDecipheriv(
88+
this.algorithm,
89+
key,
90+
iv,
91+
) as crypto.DecipherGCM;
92+
decipher.setAuthTag(tag);
93+
94+
const decrypted = Buffer.concat([
95+
decipher.update(encrypted),
96+
decipher.final(),
97+
]);
98+
return decrypted.toString("utf8");
99+
} catch (error: any) {
100+
// Log the error with context for debugging
101+
console.error(`Failed to decrypt cache file ${filePath}:`, error.message);
102+
throw new Error(
103+
`Cache decryption failed: ${error.message}. The cache file may be corrupted.`,
104+
);
105+
}
87106
}
88107

89108
private keyToFilepath(key: string): string {
@@ -100,8 +119,30 @@ export class SecretStorage {
100119
async get(key: string): Promise<string | undefined> {
101120
const filePath = this.keyToFilepath(key);
102121
if (fs.existsSync(filePath)) {
103-
const value = await this.decrypt(filePath);
104-
return value;
122+
try {
123+
const value = await this.decrypt(filePath);
124+
return value;
125+
} catch (error: any) {
126+
// Corrupted cache file - delete it and return undefined
127+
// This allows the auth flow to continue with a fresh start
128+
console.error(
129+
`Corrupted cache file detected for key "${key}". Deleting file and returning undefined.`,
130+
error.message,
131+
);
132+
133+
try {
134+
fs.unlinkSync(filePath);
135+
console.log(`Successfully deleted corrupted cache file: ${filePath}`);
136+
} catch (deleteError: any) {
137+
console.error(
138+
`Failed to delete corrupted cache file ${filePath}:`,
139+
deleteError.message,
140+
);
141+
}
142+
143+
// Return undefined to signal missing data (same as if file didn't exist)
144+
return undefined;
145+
}
105146
}
106147
return undefined;
107148
}

extensions/vscode/src/stubs/WorkOsAuthProvider.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,22 +161,22 @@ export class WorkOsAuthProvider implements AuthenticationProvider, Disposable {
161161
scopes?: string[],
162162
): Promise<ContinueAuthenticationSession[]> {
163163
// await this.hasAttemptedRefresh;
164-
const data = await this.secretStorage.get(SESSIONS_SECRET_KEY);
165-
if (!data) {
166-
return [];
167-
}
168-
169164
try {
165+
const data = await this.secretStorage.get(SESSIONS_SECRET_KEY);
166+
if (!data) {
167+
return [];
168+
}
169+
170170
const value = JSON.parse(data) as ContinueAuthenticationSession[];
171171
return value;
172172
} catch (e: any) {
173-
// Capture session file parsing errors to Sentry
173+
// Capture session decrypt and parsing errors to Sentry
174174
Logger.error(e, {
175-
context: "workOS_sessions_json_parse",
176-
dataLength: data.length,
175+
context: "workOS_sessions_retrieval",
176+
errorMessage: e.message,
177177
});
178178

179-
console.warn(`Error parsing sessions.json: ${e}`);
179+
console.warn(`Error retrieving or parsing sessions: ${e.message}`);
180180
return [];
181181
}
182182
}

gui/src/context/Auth.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,27 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
5050
const currentOrg = useAppSelector(selectCurrentOrg);
5151
const selectedProfile = useAppSelector(selectSelectedProfile);
5252

53-
const login: AuthContextType["login"] = (useOnboarding: boolean) => {
54-
return new Promise(async (resolve) => {
55-
await ideMessenger
56-
.request("getControlPlaneSessionInfo", {
57-
silent: false,
58-
useOnboarding,
59-
})
60-
.then((result) => {
61-
if (result.status === "error") {
62-
resolve(false);
63-
return;
64-
}
53+
const login: AuthContextType["login"] = async (useOnboarding: boolean) => {
54+
try {
55+
const result = await ideMessenger.request("getControlPlaneSessionInfo", {
56+
silent: false,
57+
useOnboarding,
58+
});
59+
60+
if (result.status === "error") {
61+
console.error("Login failed:", result.error);
62+
return false;
63+
}
6564

66-
const session = result.content;
67-
setSession(session);
65+
const session = result.content;
66+
setSession(session);
6867

69-
resolve(true);
70-
});
71-
});
68+
return true;
69+
} catch (error: any) {
70+
console.error("Login request failed:", error);
71+
// Let the error propagate so the caller can handle it
72+
throw error;
73+
}
7274
};
7375

7476
const logout = () => {

0 commit comments

Comments
 (0)