FuelLabs/fuels-ts · error · FuelError

INVALID_CREDENTIALS

INVALID_CREDENTIALS

Error message

Invalid credentials.

What it means

Thrown by the Node AES-256-CTR decrypt path when JSON.parse of the decrypted plaintext fails. A failed parse means the ciphertext did not decode to valid JSON, which happens when the derived key is wrong (wrong password) or the keystore blob is not the one originally encrypted. The SDK deliberately collapses both parse failure and authentication failure into a single INVALID_CREDENTIALS error so as not to leak which step failed.

Source

Thrown at packages/crypto/src/node/aes-ctr.ts:74

 */
export const decrypt: CryptoApi['decrypt'] = async <T>(
  password: string,
  keystore: Keystore
): Promise<T> => {
  const iv = bufferFromString(keystore.iv);
  const salt = bufferFromString(keystore.salt);
  const secret = keyFromPassword(password, salt);
  const encryptedText = bufferFromString(keystore.data);

  const decipher = await crypto.createDecipheriv(ALGORITHM, secret, iv);
  const decrypted = decipher.update(encryptedText);
  const deBuff = Buffer.concat([decrypted, decipher.final()]);
  const decryptedData = Buffer.from(deBuff).toString('utf-8');

  try {
    return JSON.parse(decryptedData);
  } catch {
    throw new FuelError(ErrorCode.INVALID_CREDENTIALS, 'Invalid credentials.');
  }
};

View on GitHub (pinned to b3f37c91ac)

Solutions

  1. Re-enter the password carefully (the failure most often is a wrong password, not corruption).
  2. Confirm the keystore object is the exact one returned by `encrypt()` — match `data`, `iv`, and `salt`.
  3. If migrating from another tool, re-encrypt with `encrypt(password, data)` from this SDK first.
  4. Restore the keystore from a backup if the file was edited or partially overwritten.

Example fix

// before
const wallet = await decrypt(typedPassword, keystore);
// after — verify keystore shape and source first
if (!keystore.data || !keystore.iv || !keystore.salt) {
  throw new Error('Keystore is incomplete');
}
try {
  const wallet = await decrypt(typedPassword, keystore);
} catch (e) {
  if (e instanceof FuelError && e.code === FuelError.CODES.INVALID_CREDENTIALS) {
    // prompt the user to re-enter the password
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

import { encrypt } from '@fuel-ts/crypto';
// verify keystore shape and source before decrypt
function isKeystore(o: unknown): o is { data: string; iv: string; salt: string } {
  return !!o && typeof o === 'object' &&
    typeof (o as any).data === 'string' &&
    typeof (o as any).iv === 'string' &&
    typeof (o as any).salt === 'string';
}

Type guard

import type { Keystore } from '@fuel-ts/crypto';
const isKeystore = (o: unknown): o is Keystore =>
  !!o && typeof o === 'object' &&
  ['data', 'iv', 'salt'].every((k) => typeof (o as any)[k] === 'string');

Try / catch

try {
  const data = await decrypt(password, keystore);
} catch (e) {
  if (e instanceof FuelError && e.code === FuelError.CODES.INVALID_CREDENTIALS) {
    // re-prompt for password; do NOT loop silently
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `decrypt(password, keystore)` from @fuel-ts/crypto with a password whose PBKDF2-derived key does not reproduce the original ciphertext, or with a keystore whose `data`, `iv`, or `salt` fields were altered.

Common situations: Typo in the wallet password, loading the wrong keystore file, a keystore generated by a different SDK/version or a non-Fuel tool, keystore truncated or corrupted on disk, or copy/paste of keystore fields that dropped characters.

Understand the failure class

Related errors


AI-assisted analysis of FuelLabs/fuels-ts@b3f37c91ac (2026-08-12). Data as JSON: /api/errors/7ad49b24d4451964. Report an issue: GitHub.