@hash — Balloon & PBKDF2 Hashing
One-way hashes a string field before writing to the database. Defaults to Balloon Hashing (memory-hard, SHA-256 based) since v1.9.5. Falls back to PBKDF2 with :(algo=pbkdf2). Exposes a .verify(plaintext) method for constant-time comparison on read.
Syntax
src/interfaces.ts
// Balloon Hashing (default — memory-hard, SHA-256)
// space=65536 (2MB), time=3, delta=3
// @hash
password?: string;
// Explicit Balloon with custom params
// @hash:(algo=balloon,space=4096,time=2,delta=2)
// PBKDF2 opt-in (backward compat)
// @hash:(algo=pbkdf2,iterations=600000)
pinHash?: string;Stored Format
(database column value)
// Balloon (default since v1.9.5):
balloon$<space>$<time>$<delta>$<salt>$<hash>
// Example:
balloon$65536$3$3$Ys0HFPkNgc1aGRp6Y8reug==$uWLzg3CQ...
// PBKDF2 (opt-in):
pbkdf2$<iterations>$<salt>$<hash>
// Example:
pbkdf2$600000$Ys0HFPkNgc1aGRp6Y8reug==$uWLzg3CQtcDZ37JGoVGyor8Qg7yh9BU9sM2jTdilCN0=- Balloon — space/time/delta encoded inline; backward-compatible — old PBKDF2 hashes still verify
- PBKDF2 — iterations encoded inline so raising the constant never invalidates existing hashes
- salt — 16 random bytes, base64-encoded
- hash — 256-bit derived key, base64-encoded
Usage
src/auth.ts
// Insert — auto-hashed (Balloon) before write
const user = await User.insert({
email: "alice@example.com",
password: "correct-horse-battery-staple",
});
// DB stores: balloon$65536$3$3$... (never the plaintext)
// Fetch — .verify() attached to the field
const fetched = await User.get({ email: "alice@example.com" });
const match = await fetched.password.verify("correct-horse-battery-staple");
// true
const wrong = await fetched.password.verify("wrong-password");
// falseGotcha: .verify() returns a wrapper object
src/auth.ts
const user = await User.get({ id: 1 });
typeof user.password; // "string" at runtime (object coercion)
user.password === "balloon$..."; // true — comparison works via valueOf()
user.password.verify("secret"); // ✅ works
// BUT direct === against a plain string literal fails:
if (user.password === "balloon$...") {} // false — different object identity
// Use .verify() instead, or compare via toString()Errors
| Error | Cause | Fix |
|---|---|---|
Expected 'balloon$...' or 'pbkdf2$...' format, got '...' | Stored value doesn't start with recognized prefix | Verify the column contains a properly hashed value |
Expected 'pbkdf2$iterations$salt$hash' (new) or 'pbkdf2$salt$hash' (legacy), got N parts | Malformed PBKDF2 stored hash | Re-hash the field by writing to it again |
Conventions
- Always use
.verify()for password comparison — never compare the raw hash string - Use
@hash:(algo=pbkdf2,iterations=N)for PBKDF2 opt-in, or@hash:(algo=balloon,space=N,time=N,delta=N)to tune Balloon cost @hashfields are not excluded from query results — use@secretif you also need omit behavior- Parameters are per-field, encoded in the stored format — old hashes always verify regardless of current defaults