stablyai/orca · error · CredentialDecryptionError

Could not decrypt saved ${service} credential. Approve Keych

Error message

Could not decrypt saved ${service} credential. Approve Keychain access or reconnect ${service}.

What it means

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.

Source

Thrown at src/main/integration-credential-file.ts:133

    try {
      return usableToken(safeStorage.decryptString(raw))
    } catch {
      return readPlaintextLegacyCredential(service, raw)
    }
  }

  return readPlaintextLegacyCredential(service, raw)
}

function readPlaintextLegacyCredential(
  service: IntegrationCredentialService,
  raw: Buffer
): string | null {
  const plaintext = decodeUtf8(raw)
  // Why: legacy plaintext tokens are printable UTF-8; safeStorage ciphertext
  // such as macOS v10 blobs must not be decoded into auth-header junk.
  if (plaintext === null || hasControlCharacter(plaintext)) {
    throw new CredentialDecryptionError(service)
  }
  return usableToken(plaintext)
}

function usableToken(token: string): string | null {
  return token.length > 0 ? token : null
}

function decodeUtf8(raw: Buffer): string | null {
  try {
    return new TextDecoder('utf-8', { fatal: true }).decode(raw)
  } catch {
    return null
  }
}

function hasControlCharacter(value: string): boolean {
  for (let index = 0; index < value.length; index += 1) {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Reconnect the integration through the UI to overwrite the credential with a freshly-encrypted token.
  2. On macOS, approve the Keychain access prompt (or fix the app's keychain entry after re-signing).
  3. On Linux, ensure a secrets service (gnome-keyring/kwallet/libsecret) is running so safeStorage works.
  4. As a last resort, delete the corrupt credential file so a new one is written on next connect.
Defensive patterns

Strategy: try-catch

Validate before calling

import { safeStorage } from 'electron'
function canDecryptCredential(): boolean {
  return safeStorage.isEncryptionAvailable()
}

Type guard

function isCredentialDecryptionError(error: unknown): error is CredentialDecryptionError {
  return error instanceof Error && error.name === 'CredentialDecryptionError'
}

Try / catch

try {
  token = readStoredCredentialToken(service, raw)
} catch (error) {
  if (error instanceof CredentialDecryptionError || (error instanceof Error && error.name === 'CredentialDecryptionError')) {
    // prompt user to reconnect the integration to overwrite the credential
    await promptReconnect(service)
    return null
  }
  throw error
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/d823de6ecc8e3da4. Report an issue: GitHub.