OtterMind/Chat2DB · error · Error

Decryption error

Error message

Decryption error

What it means

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.

Source

Thrown at chat2db-community-client/src/utils/cryptography.ts:121

  // AES-GCM decryption
  public async decryptAes(encryptedValue: string | null): Promise<string | null> {
    if (encryptedValue === null) return null;
    const key = await this.generateAESKeyFromToken(this.accessKey);
    const decoded = atob(encryptedValue);
    const decodedBytes = new Uint8Array(decoded.split('').map((char) => char.charCodeAt(0)));
    const nonce = decodedBytes.slice(0, GCM_NONCE_LENGTH);
    const encryptedBytes = decodedBytes.slice(GCM_NONCE_LENGTH);
    try {
      const decryptedData = await crypto.subtle.decrypt(
        { name: 'AES-GCM', iv: nonce, tagLength: 128 },
        key,
        encryptedBytes,
      );
      return new TextDecoder().decode(decryptedData);
    } catch (exception) {
      console.error('decrypt aes error', exception);
      throw new Error('Decryption error');
    }
  }

  // Derive signing key
  private async deriveSigningKey(date: string): Promise<ArrayBuffer> {
    const kSecret = new TextEncoder().encode('CHAT2DB' + this.secretKey);
    const kDate = await this.hmacSHA256(date, kSecret);
    return this.hmacSHA256(this.country, kDate);
  }

  // Compute signature
  public async calculateSignature(canonicalRequest: string): Promise<string> {
    const stringToSign = await this.createStringToSign(canonicalRequest);
    const currentTime = this.getCurrentTimeZoneFormatted();
    const signingKey = await this.deriveSigningKey(currentTime.substring(0, 8));
    const signatureArrayBuffer = await this.hmacSHA256(stringToSign, signingKey);
    return this.bufferToHex(signatureArrayBuffer);
  }

View on GitHub (pinned to 5ee1e990e7)

Solutions

  1. 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).
  2. Check console.error('decrypt aes error', exception) output for the specific WebCrypto error (OperationError usually indicates key/data mismatch).
  3. Ensure the encrypted value is a complete, uncorrupted base64 string with at least 12 bytes (nonce) + 16 bytes (tag).
  4. If the key rotated, re-encrypt the data with the new key or fall back to a default/empty value.

Example fix

// before
const decrypted = await crypto.decryptAes(encryptedValue);

// after
let decrypted = encryptedValue;
try {
  decrypted = await crypto.decryptAes(encryptedValue);
} catch (e) {
  console.warn('Decryption failed, using raw value as fallback');
}
Defensive patterns

Strategy: try-catch

Validate before calling

function isLikelyEncryptedAes(value: string | null): boolean {
  if (!value) return false;
  try {
    const decoded = atob(value);
    return decoded.length >= 28; // 12 (nonce) + 16 (tag) minimum
  } catch {
    return false;
  }
}

if (!isLikelyEncryptedAes(encryptedValue)) {
  return encryptedValue; // not encrypted, return as-is
}

Type guard

function isBase64String(v: unknown): v is string {
  if (typeof v !== 'string') return false;
  try { atob(v); return true; } catch { return false; }
}

Try / catch

let decrypted = encryptedValue;
try {
  decrypted = await crypto.decryptAes(encryptedValue);
} catch (e) {
  if (e instanceof Error && e.message === 'Decryption error') {
    // key mismatch or corrupt data — return raw value as fallback
    decrypted = encryptedValue;
  } else {
    throw e;
  }
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14). Data as JSON: /api/errors/e089049e5332aa35. Report an issue: GitHub.