{"record":{"id":"d823de6ecc8e3da4","repo":"stablyai/orca","slug":"could-not-decrypt-saved-service-credential-app","errorCode":null,"errorMessage":"Could not decrypt saved ${service} credential. Approve Keychain access or reconnect ${service}.","messagePattern":"Could not decrypt saved (.+?) credential\\. Approve Keychain access or reconnect (.+?)\\.","errorType":"exception","errorClass":"CredentialDecryptionError","httpStatus":null,"severity":"error","filePath":"src/main/integration-credential-file.ts","lineNumber":133,"sourceCode":"    try {\n      return usableToken(safeStorage.decryptString(raw))\n    } catch {\n      return readPlaintextLegacyCredential(service, raw)\n    }\n  }\n\n  return readPlaintextLegacyCredential(service, raw)\n}\n\nfunction readPlaintextLegacyCredential(\n  service: IntegrationCredentialService,\n  raw: Buffer\n): string | null {\n  const plaintext = decodeUtf8(raw)\n  // Why: legacy plaintext tokens are printable UTF-8; safeStorage ciphertext\n  // such as macOS v10 blobs must not be decoded into auth-header junk.\n  if (plaintext === null || hasControlCharacter(plaintext)) {\n    throw new CredentialDecryptionError(service)\n  }\n  return usableToken(plaintext)\n}\n\nfunction usableToken(token: string): string | null {\n  return token.length > 0 ? token : null\n}\n\nfunction decodeUtf8(raw: Buffer): string | null {\n  try {\n    return new TextDecoder('utf-8', { fatal: true }).decode(raw)\n  } catch {\n    return null\n  }\n}\n\nfunction hasControlCharacter(value: string): boolean {\n  for (let index = 0; index < value.length; index += 1) {","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/integration-credential-file.ts#L115-L151","documentation":"Thrown as CredentialDecryptionError when the stored credential file holds bytes that are neither decryptable by Electron safeStorage (or safeStorage is unavailable) nor valid legacy plaintext. Specifically, readPlaintextLegacyCredential's fatal UTF-8 decode fails (decodeUtf8 returns null) OR the decoded string contains control characters — a heuristic that catches safeStorage ciphertext (e.g. macOS v10 blobs) misclassified as plaintext, preventing auth-header corruption. The message names the service and tells the user to approve Keychain access or reconnect.","triggerScenarios":"safeStorage.decryptString threw (or safeStorage unavailable) so the legacy plaintext path is tried, but the bytes are actually ciphertext: they fail fatal UTF-8 decoding, or decode to a string with control characters. Classic after an app re-sign invalidated the keychain key, or the OS keychain access was denied.","commonSituations":"macOS app re-signing/re-notarization invalidated the safeStorage key; user denied the Keychain access prompt; credential file copied between machines/users (different safeStorage keys); headless Linux without libsecret/gnome-keyring (safeStorage unavailable) holding ciphertext from a previous GUI session; OS migration.","solutions":["Reconnect the integration through the UI to overwrite the credential with a freshly-encrypted token.","On macOS, approve the Keychain access prompt (or fix the app's keychain entry after re-signing).","On Linux, ensure a secrets service (gnome-keyring/kwallet/libsecret) is running so safeStorage works.","As a last resort, delete the corrupt credential file so a new one is written on next connect."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"import { safeStorage } from 'electron'\nfunction canDecryptCredential(): boolean {\n  return safeStorage.isEncryptionAvailable()\n}","typeGuard":"function isCredentialDecryptionError(error: unknown): error is CredentialDecryptionError {\n  return error instanceof Error && error.name === 'CredentialDecryptionError'\n}","tryCatchPattern":"try {\n  token = readStoredCredentialToken(service, raw)\n} catch (error) {\n  if (error instanceof CredentialDecryptionError || (error instanceof Error && error.name === 'CredentialDecryptionError')) {\n    // prompt user to reconnect the integration to overwrite the credential\n    await promptReconnect(service)\n    return null\n  }\n  throw error\n}","preventionTips":["Ensure a secrets service (gnome-keyring/kwallet/libsecret) is running on Linux before storing credentials.","After app re-signing/re-notarization on macOS, expect to re-auth integrations.","Never copy credential files between machines/users — they are key-bound."],"tags":["credentials","encryption","keychain","safestorage","auth"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}