Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 68 additions & 27 deletions extensions/vscode/src/stubs/SecretStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,31 +59,50 @@ export class SecretStorage {
}

async decrypt(filePath: string): Promise<string> {
const key = await this.getOrCreateEncryptionKey();
const data = fs.readFileSync(filePath);

const salt = data.subarray(0, this.saltLength);
const iv = data.subarray(this.saltLength, this.saltLength + this.ivLength);
const tag = data.subarray(
this.saltLength + this.ivLength,
this.saltLength + this.ivLength + this.tagLength,
);
const encrypted = data.subarray(
this.saltLength + this.ivLength + this.tagLength,
);

const decipher: crypto.DecipherGCM = crypto.createDecipheriv(
this.algorithm,
key,
iv,
) as crypto.DecipherGCM;
decipher.setAuthTag(tag);

const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return decrypted.toString("utf8");
try {
const key = await this.getOrCreateEncryptionKey();
const data = fs.readFileSync(filePath);

// Validate minimum data size to detect corruption early
const minSize = this.saltLength + this.ivLength + this.tagLength;
if (data.length < minSize) {
throw new Error(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping decrypt() in a broad try/catch and rethrowing a new Error discards the original stack trace, making future failures much harder to diagnose. Please rethrow the original error (or use ErrorOptions.cause) so callers retain full context.

Prompt for AI agents
Address the following comment on extensions/vscode/src/stubs/SecretStorage.ts at line 69:

<comment>Wrapping decrypt() in a broad try/catch and rethrowing a new Error discards the original stack trace, making future failures much harder to diagnose. Please rethrow the original error (or use ErrorOptions.cause) so callers retain full context.</comment>

<file context>
@@ -59,31 +59,50 @@ export class SecretStorage {
+      // Validate minimum data size to detect corruption early
+      const minSize = this.saltLength + this.ivLength + this.tagLength;
+      if (data.length &lt; minSize) {
+        throw new Error(
+          `Corrupted cache file: insufficient data (${data.length} bytes, expected at least ${minSize})`,
+        );
</file context>

✅ Addressed in 21b987b

`Corrupted cache file: insufficient data (${data.length} bytes, expected at least ${minSize})`,
);
}

const salt = data.subarray(0, this.saltLength);
const iv = data.subarray(
this.saltLength,
this.saltLength + this.ivLength,
);
const tag = data.subarray(
this.saltLength + this.ivLength,
this.saltLength + this.ivLength + this.tagLength,
);
const encrypted = data.subarray(
this.saltLength + this.ivLength + this.tagLength,
);

const decipher: crypto.DecipherGCM = crypto.createDecipheriv(
this.algorithm,
key,
iv,
) as crypto.DecipherGCM;
decipher.setAuthTag(tag);

const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return decrypted.toString("utf8");
} catch (error: any) {
// Log the error with context for debugging
console.error(`Failed to decrypt cache file ${filePath}:`, error.message);
throw new Error(
`Cache decryption failed: ${error.message}. The cache file may be corrupted.`,
);
}
}

private keyToFilepath(key: string): string {
Expand All @@ -100,8 +119,30 @@ export class SecretStorage {
async get(key: string): Promise<string | undefined> {
const filePath = this.keyToFilepath(key);
if (fs.existsSync(filePath)) {
const value = await this.decrypt(filePath);
return value;
try {
const value = await this.decrypt(filePath);
return value;
} catch (error: any) {
// Corrupted cache file - delete it and return undefined
// This allows the auth flow to continue with a fresh start
console.error(
`Corrupted cache file detected for key "${key}". Deleting file and returning undefined.`,
error.message,
);

try {
fs.unlinkSync(filePath);
console.log(`Successfully deleted corrupted cache file: ${filePath}`);
} catch (deleteError: any) {
console.error(
`Failed to delete corrupted cache file ${filePath}:`,
deleteError.message,
);
}

// Return undefined to signal missing data (same as if file didn't exist)
return undefined;
}
}
return undefined;
}
Expand Down
18 changes: 9 additions & 9 deletions extensions/vscode/src/stubs/WorkOsAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,22 +161,22 @@ export class WorkOsAuthProvider implements AuthenticationProvider, Disposable {
scopes?: string[],
): Promise<ContinueAuthenticationSession[]> {
// await this.hasAttemptedRefresh;
const data = await this.secretStorage.get(SESSIONS_SECRET_KEY);
if (!data) {
return [];
}

try {
const data = await this.secretStorage.get(SESSIONS_SECRET_KEY);
if (!data) {
return [];
}

const value = JSON.parse(data) as ContinueAuthenticationSession[];
return value;
} catch (e: any) {
// Capture session file parsing errors to Sentry
// Capture session decrypt and parsing errors to Sentry
Logger.error(e, {
context: "workOS_sessions_json_parse",
dataLength: data.length,
context: "workOS_sessions_retrieval",
errorMessage: e.message,
});

console.warn(`Error parsing sessions.json: ${e}`);
console.warn(`Error retrieving or parsing sessions: ${e.message}`);
return [];
}
}
Expand Down
36 changes: 19 additions & 17 deletions gui/src/context/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,27 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
const currentOrg = useAppSelector(selectCurrentOrg);
const selectedProfile = useAppSelector(selectSelectedProfile);

const login: AuthContextType["login"] = (useOnboarding: boolean) => {
return new Promise(async (resolve) => {
await ideMessenger
.request("getControlPlaneSessionInfo", {
silent: false,
useOnboarding,
})
.then((result) => {
if (result.status === "error") {
resolve(false);
return;
}
const login: AuthContextType["login"] = async (useOnboarding: boolean) => {
try {
const result = await ideMessenger.request("getControlPlaneSessionInfo", {
silent: false,
useOnboarding,
});

if (result.status === "error") {
console.error("Login failed:", result.error);
return false;
}

const session = result.content;
setSession(session);
const session = result.content;
setSession(session);

resolve(true);
});
});
return true;
} catch (error: any) {
console.error("Login request failed:", error);
// Let the error propagate so the caller can handle it
throw error;
}
};

const logout = () => {
Expand Down
Loading