laurent22/joplin · error · Error

Authentication failed! ${error}

Error message

Authentication failed! ${error}

What it means

Thrown by the mobile E2EE crypto module when GCM authentication fails during decryption — i.e. `decipher.update`/`decipher.final` raises because the auth tag does not match the ciphertext. This means the data was tampered with, the wrong key was used, or the AAD/plaintextLength are inconsistent. The original error is interpolated into the message.

Source

Thrown at packages/app-mobile/services/e2ee/crypto.ts:55

	const authTag = cipher.getAuthTag();

	return Buffer.concat([encryptedData[0], encryptedData[1], authTag]);
};

const decryptRaw = (data: ArrayBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: ArrayBuffer, authTagLength: number, associatedData: CryptoBuffer) => {

	const decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as unknown as DecipherGCM;

	const plaintextLength = data.byteLength - authTagLength;
	const authTag = new Uint8Array(data, plaintextLength, authTagLength);
	const encryptedData = new Uint8Array(data, 0, plaintextLength);
	decipher.setAuthTag(authTag);
	decipher.setAAD(associatedData, { plaintextLength: plaintextLength });

	try {
		return Buffer.concat([decipher.update(encryptedData), decipher.final()]);
	} catch (error) {
		throw new Error(`Authentication failed! ${error}`);
	}
};

const crypto: Crypto = {

	randomBytes: async (size: number) => {
		return new Promise((resolve, reject) => {
			QuickCrypto.randomBytes(size, (error, result) => {
				if (error) {
					reject(error);
				} else {
					resolve(result);
				}
			});
		});
	},

	digest: async (algorithm: Digest, data: Uint8Array) => {

View on GitHub (pinned to 2654b33620)

Solutions

  1. Confirm the master key / password matches the one used to encrypt (re-derive and compare).
  2. Verify `data.byteLength > authTagLength` before slicing the auth tag.
  3. Ensure the same `associatedData` and `authTagLength` are passed that were used at encryption time.
  4. If the blob is from a partial download, re-fetch the resource before decrypting.

Example fix

// before
return Buffer.concat([decipher.update(encryptedData), decipher.final()]);

// after
if (data.byteLength <= authTagLength) {
  throw new Error('Ciphertext too short to contain an auth tag');
}
try {
  return Buffer.concat([decipher.update(encryptedData), decipher.final()]);
} catch (error) {
  throw new Error(`Authentication failed! ${error}`);
}
Defensive patterns

Strategy: validation

Validate before calling

if (data.byteLength <= authTagLength) {
  throw new Error('Ciphertext too short to contain an auth tag');
}
// Verify the derived key matches the encryption key before attempting decrypt.

Type guard

null

Try / catch

try {
  return await decrypt({ data, key, iv, associatedData, authTagLength });
} catch (error) {
  if (/Authentication failed/i.test(error.message)) {
    logger.warn('GCM auth tag mismatch — wrong key or corrupted blob');
    return null;
  }
  throw error;
}

Prevention

When it happens

Trigger: Decrypting a master-key-encrypted note/resource with the wrong key, a corrupted blob, an auth tag sliced from the wrong offset, or mismatched `associatedData`/`plaintextLength`. Reached via the `decrypt` helper in `packages/app-mobile/services/e2ee/crypto.ts` when `data.byteLength <= authTagLength` is also a risk (negative plaintextLength).

Common situations: User changed/recovered the master password on a different device so the derived key differs; sync payload truncated or partially written; an older client wrote the blob without AAD and a newer client expects it; concurrency during partial download of an encrypted resource.

Understand the failure class

Related errors


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/01bc59470dd3357e. Report an issue: GitHub.