The achievements-manager library includes built-in anti-cheat mechanisms to detect when stored achievement data has been tampered with. This is accomplished through hash adapters that compute cryptographic checksums of stored data.
While this system provides tamper detection for casual modification attempts (e.g., manual localStorage edits), it is not cryptographically secure against determined attackers. For security-critical applications, validate achievements server-side.
By default, the engine uses the fnv1aHashAdapter, which implements the FNV-1a (Fowler-Noll-Vo) 32-bit hash algorithm:
// From packages/core/src/adapters.ts:37-48export function fnv1aHashAdapter(): HashAdapter { return { hash(data: string): string { let h = 2166136261; // FNV-1a offset basis for (let i = 0; i < data.length; i++) { h ^= data.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; // FNV prime, keep 32-bit unsigned } return h.toString(16); }, };}
FNV-1a is chosen for its speed and simplicity. It’s fast enough to run synchronously in the browser and provides sufficient tamper detection for most use cases.
When the engine loads, it checks the integrity of all three storage keys:
unlocked - The set of unlocked achievement IDs
progress - The progress values for each achievement
items - The collected items for each achievement
// From packages/core/src/engine.ts:51-59function verifyStoredIntegrity(key: string): boolean { const data = storage.get(key); const storedHash = storage.get(key + HASH_SUFFIX); // No hash stored yet (backward-compatible) — trust the data if (storedHash === null) return true; // Hash exists but data was removed — something is wrong if (data === null) return false; return storedHash === computeHash(data);}
This ensures that after a tamper is detected, the next load starts from a fully consistent clean slate—no partial state like items present but progress showing stale values.
// From packages/core/src/engine.ts:10-19export type Config<TId extends string> = { definitions: ReadonlyArray<AchievementDef<TId>>; storage?: StorageAdapter; /** Pluggable hash function for tamper detection. Defaults to FNV-1a (32-bit). */ hash?: HashAdapter; /** Called synchronously immediately after an achievement is unlocked. */ onUnlock?: (id: TId) => void; /** Called when stored data fails its integrity check. */ onTamperDetected?: (key: string) => void;};
Called when stored data fails its integrity check. The key parameter indicates which storage key was tampered with (e.g., "unlocked", "progress", or "items").
Note: The current HashAdapter type expects a synchronous hash() function. For async hashing, you’ll need to modify the type definition or use a synchronous wrapper.
Always configure onTamperDetected to log or alert when tampering is detected
Use server-side validation for security-critical achievements (e.g., in-app purchases)
Don’t rely on client-side checks alone for preventing exploits
Consider custom hash adapters if you need stronger cryptographic guarantees
Test your tamper detection by manually editing localStorage during development
// Example: Testing tamper detectionconst engine = createAchievements({ definitions: achievements, onTamperDetected: (key) => { console.error(`SECURITY: Tamper detected on ${key}`); // In production, might send to analytics or disable features },});// Simulate tampering (in dev tools console):// localStorage.setItem('unlocked', JSON.stringify(['fake-achievement']));// Then reload - tamper will be detected and storage wiped