{"record":{"id":"e089049e5332aa35","repo":"OtterMind/Chat2DB","slug":"decryption-error","errorCode":null,"errorMessage":"Decryption error","messagePattern":"Decryption error","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"chat2db-community-client/src/utils/cryptography.ts","lineNumber":121,"sourceCode":"\n  // AES-GCM decryption\n  public async decryptAes(encryptedValue: string | null): Promise<string | null> {\n    if (encryptedValue === null) return null;\n    const key = await this.generateAESKeyFromToken(this.accessKey);\n    const decoded = atob(encryptedValue);\n    const decodedBytes = new Uint8Array(decoded.split('').map((char) => char.charCodeAt(0)));\n    const nonce = decodedBytes.slice(0, GCM_NONCE_LENGTH);\n    const encryptedBytes = decodedBytes.slice(GCM_NONCE_LENGTH);\n    try {\n      const decryptedData = await crypto.subtle.decrypt(\n        { name: 'AES-GCM', iv: nonce, tagLength: 128 },\n        key,\n        encryptedBytes,\n      );\n      return new TextDecoder().decode(decryptedData);\n    } catch (exception) {\n      console.error('decrypt aes error', exception);\n      throw new Error('Decryption error');\n    }\n  }\n\n  // Derive signing key\n  private async deriveSigningKey(date: string): Promise<ArrayBuffer> {\n    const kSecret = new TextEncoder().encode('CHAT2DB' + this.secretKey);\n    const kDate = await this.hmacSHA256(date, kSecret);\n    return this.hmacSHA256(this.country, kDate);\n  }\n\n  // Compute signature\n  public async calculateSignature(canonicalRequest: string): Promise<string> {\n    const stringToSign = await this.createStringToSign(canonicalRequest);\n    const currentTime = this.getCurrentTimeZoneFormatted();\n    const signingKey = await this.deriveSigningKey(currentTime.substring(0, 8));\n    const signatureArrayBuffer = await this.hmacSHA256(stringToSign, signingKey);\n    return this.bufferToHex(signatureArrayBuffer);\n  }","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/chat2db-community-client/src/utils/cryptography.ts#L103-L139","documentation":"CryptographyUtil.decryptAes throws 'Decryption error' when crypto.subtle.decrypt fails for an AES-GCM ciphertext. The method derives the key from the accessKey (SHA-256, first 16 bytes), splits the base64-decoded payload into a 12-byte nonce and the ciphertext+tag, then attempts GCM decryption. Failure means the key is wrong, the data is corrupt/truncated, the nonce/tag is mismatched, or the base64 encoding is invalid. The original exception is logged to console before rethrowing as a generic Error.","triggerScenarios":"Calling decryptAes(encryptedValue) where: (1) the accessKey used for decryption differs from the one used for encryption, (2) the encryptedValue was not produced by encryptAes (different format/nonce), (3) the base64 string is corrupted or truncated, (4) the GCM authentication tag does not match (data tampering or truncation).","commonSituations":"The accessKey/secretKey changed server-side but stale encrypted data remains. Decoding a value encrypted by a different instance/region with a different key. Data corruption in storage or transit. Encoding mismatch (URL-safe base64 vs standard base64).","solutions":["Verify the accessKey matches the one used at encryption time (decryptAes uses this.accessKey, encryptAes also uses this.accessKey — they must be the same instance config).","Check console.error('decrypt aes error', exception) output for the specific WebCrypto error (OperationError usually indicates key/data mismatch).","Ensure the encrypted value is a complete, uncorrupted base64 string with at least 12 bytes (nonce) + 16 bytes (tag).","If the key rotated, re-encrypt the data with the new key or fall back to a default/empty value."],"exampleFix":"// before\nconst decrypted = await crypto.decryptAes(encryptedValue);\n\n// after\nlet decrypted = encryptedValue;\ntry {\n  decrypted = await crypto.decryptAes(encryptedValue);\n} catch (e) {\n  console.warn('Decryption failed, using raw value as fallback');\n}","handlingStrategy":"try-catch","validationCode":"function isLikelyEncryptedAes(value: string | null): boolean {\n  if (!value) return false;\n  try {\n    const decoded = atob(value);\n    return decoded.length >= 28; // 12 (nonce) + 16 (tag) minimum\n  } catch {\n    return false;\n  }\n}\n\nif (!isLikelyEncryptedAes(encryptedValue)) {\n  return encryptedValue; // not encrypted, return as-is\n}","typeGuard":"function isBase64String(v: unknown): v is string {\n  if (typeof v !== 'string') return false;\n  try { atob(v); return true; } catch { return false; }\n}","tryCatchPattern":"let decrypted = encryptedValue;\ntry {\n  decrypted = await crypto.decryptAes(encryptedValue);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Decryption error') {\n    // key mismatch or corrupt data — return raw value as fallback\n    decrypted = encryptedValue;\n  } else {\n    throw e;\n  }\n}","preventionTips":["Ensure the CryptographyUtil instance used for decryption has the same accessKey as the one used for encryption.","Validate the base64 integrity before attempting decryption.","Log the original WebCrypto exception (console.error) for diagnosis.","Avoid re-using encrypted data across key rotations without re-encryption."],"tags":["cryptography","aes-gcm","encryption","security","frontend"],"backgroundTag":null,"analyzedSha":"5ee1e990e73fbcae1969dc554be254fedb3ab888","analyzedAt":"2026-08-14T07:05:03.077Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}