or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.mdpassword-hashing.mdpassword-verification.mdsalt-generation.mdutility-functions.md
tile.json

utility-functions.mddocs/

Utility Functions

Helper functions for working with bcrypt hashes, password validation, algorithm configuration, and base64 encoding operations using bcrypt's custom alphabet.

Capabilities

Hash Information Extraction

Get Rounds from Hash

Extracts the number of rounds used to encrypt a bcrypt hash.

/**
 * Gets the number of rounds used to encrypt the specified hash.
 * @param hash Hash to extract the used number of rounds from
 * @returns Number of rounds used
 */
function getRounds(hash: string): number;

Usage Examples:

import { getRounds } from "bcryptjs";

const hash = "$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy";
const rounds = getRounds(hash);
console.log(rounds); // 12

// Check if hash meets minimum security requirements
function validateHashSecurity(hash, minRounds = 10) {
  const rounds = getRounds(hash);
  if (rounds < minRounds) {
    console.warn(`Hash uses only ${rounds} rounds, minimum is ${minRounds}`);
    return false;
  }
  return true;
}

// Audit existing password hashes
function auditPasswordHashes(hashes) {
  const results = hashes.map(hash => ({
    hash,
    rounds: getRounds(hash),
    secure: getRounds(hash) >= 10
  }));
  
  const insecure = results.filter(r => !r.secure);
  if (insecure.length > 0) {
    console.log(`Found ${insecure.length} hashes with insufficient rounds`);
  }
  
  return results;
}

Get Salt from Hash

Extracts the salt portion from a bcrypt hash without validation.

/**
 * Gets the salt portion from a hash. Does not validate the hash.
 * @param hash Hash to extract the salt from
 * @returns Extracted salt part
 */
function getSalt(hash: string): string;

Usage Examples:

import { getSalt } from "bcryptjs";

const hash = "$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy";
const salt = getSalt(hash);
console.log(salt); // "$2b$12$N9qo8uLOickgx2ZMRZoMye"

// Reuse salt for related operations (generally not recommended)
function extractSaltForDebugging(hash) {
  const salt = getSalt(hash);
  const rounds = getRounds(hash);
  
  return {
    salt,
    rounds,
    algorithm: salt.startsWith('$2b$') ? '2b' : 'other'
  };
}

// Compare salts between hashes
function compareSalts(hash1, hash2) {
  const salt1 = getSalt(hash1);
  const salt2 = getSalt(hash2);
  
  return {
    same: salt1 === salt2,
    salt1,
    salt2
  };
}

Password Validation

Password Truncation Check

Tests if a password will be truncated when hashed due to UTF-8 encoding exceeding 72 bytes.

/**
 * Tests if a password will be truncated when hashed, that is its length is
 * greater than 72 bytes when converted to UTF-8.
 * @param password The password to test
 * @returns true if truncated, otherwise false
 */
function truncates(password: string): boolean;

Usage Examples:

import { truncates } from "bcryptjs";

// Test various passwords
console.log(truncates("shortpass"));     // false
console.log(truncates("a".repeat(72)));  // false (exactly 72 bytes)
console.log(truncates("a".repeat(73)));  // true (exceeds 72 bytes)

// Unicode characters use multiple bytes
const unicodePassword = "🔒".repeat(20); // Each emoji is 4 bytes
console.log(truncates(unicodePassword)); // true (80 bytes total)

// Validation function for user registration
function validatePassword(password) {
  const errors = [];
  
  if (password.length < 8) {
    errors.push("Password must be at least 8 characters long");
  }
  
  if (truncates(password)) {
    errors.push("Password is too long and will be truncated (max 72 UTF-8 bytes)");
  }
  
  return {
    valid: errors.length === 0,
    errors
  };
}

// Form validation with user feedback
function checkPasswordLength(password) {
  if (truncates(password)) {
    return {
      warning: true,
      message: "Password exceeds 72 bytes and will be truncated. Consider using a shorter password."
    };
  }
  return { warning: false };
}

// Express.js middleware for password validation
const validatePasswordMiddleware = (req, res, next) => {
  const { password } = req.body;
  
  if (truncates(password)) {
    return res.status(400).json({
      error: "Password too long",
      message: "Password exceeds 72 UTF-8 bytes and will be truncated"
    });
  }
  
  next();
};

Random Number Generator Configuration

Set Random Fallback

Sets a fallback pseudo-random number generator when neither Web Crypto API nor Node.js crypto are available.

/**
 * Sets the pseudo random number generator to use as a fallback if neither 
 * Web Crypto API nor Node.js crypto are available.
 * @param random Function taking the number of bytes to generate, returning 
 *               cryptographically secure random byte values
 */
function setRandomFallback(random: RandomFallback): void;

Usage Examples:

import { setRandomFallback } from "bcryptjs";

// Set a secure random fallback using a crypto library
import { randomBytes } from 'some-crypto-library';

setRandomFallback((length) => {
  const bytes = randomBytes(length);
  return Array.from(bytes); // Convert to array of numbers
});

// Browser environment with Web Crypto API polyfill
if (typeof crypto === 'undefined') {
  // Import a crypto polyfill
  import('crypto-polyfill').then(cryptoPolyfill => {
    setRandomFallback((length) => {
      const array = new Uint8Array(length);
      cryptoPolyfill.getRandomValues(array);
      return Array.from(array);
    });
  });
}

// Custom secure random implementation (example only - use proven libraries)
function customSecureRandom(length) {
  // This is just an example - in practice, use established crypto libraries
  const bytes = [];
  for (let i = 0; i < length; i++) {
    bytes.push(Math.floor(Math.random() * 256)); // NOT SECURE - example only
  }
  return bytes;
}

// DON'T DO THIS - insecure example
setRandomFallback(customSecureRandom);

// Proper implementation with a secure library
import { webcrypto } from 'node:crypto';

setRandomFallback((length) => {
  const array = new Uint8Array(length);
  webcrypto.getRandomValues(array);
  return Array.from(array);
});

Base64 Encoding Functions

Encode Base64

Encodes a byte array to base64 using bcrypt's custom alphabet.

/**
 * Encodes a byte array to base64 with up to len bytes of input, 
 * using the custom bcrypt alphabet.
 * @param b Byte array to encode
 * @param len Maximum input length
 * @returns Base64 encoded string using bcrypt alphabet
 */
function encodeBase64(b: Readonly<ArrayLike<number>>, len: number): string;

Usage Examples:

import { encodeBase64 } from "bcryptjs";

// Encode byte array
const bytes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
const encoded = encodeBase64(bytes, bytes.length);
console.log(encoded); // "..CA.uOD/ea"

// Encode with length limit
const longBytes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09];
const partialEncoded = encodeBase64(longBytes, 5); // Only encode first 5 bytes
console.log(partialEncoded); // "..CA.u"

// Working with Uint8Array
const uint8Array = new Uint8Array([65, 66, 67, 68]); // "ABCD"
const encodedUint8 = encodeBase64(uint8Array, 4);
console.log(encodedUint8);

// Custom encoding function
function encodeBytesToBcryptBase64(bytes) {
  if (bytes.length === 0) return '';
  return encodeBase64(bytes, bytes.length);
}

Decode Base64

Decodes a base64 string using bcrypt's custom alphabet.

/**
 * Decodes a base64 encoded string to up to len bytes of output, 
 * using the custom bcrypt alphabet.
 * @param s String to decode
 * @param len Maximum output length
 * @returns Array of decoded bytes
 */
function decodeBase64(s: string, len: number): number[];

Usage Examples:

import { decodeBase64 } from "bcryptjs";

// Decode base64 string
const encoded = "..CA.uOD/ea";
const decoded = decodeBase64(encoded, 8);
console.log(decoded); // [0, 1, 2, 3, 4, 5, 6, 7]

// Decode with length limit
const partialDecoded = decodeBase64(encoded, 4); // Only decode to 4 bytes
console.log(partialDecoded); // [0, 1, 2, 3]

// Round-trip encoding/decoding test
function testRoundTrip(originalBytes) {
  const encoded = encodeBase64(originalBytes, originalBytes.length);
  const decoded = decodeBase64(encoded, originalBytes.length);
  
  const matches = originalBytes.every((byte, index) => byte === decoded[index]);
  console.log(`Round trip successful: ${matches}`);
  
  return { encoded, decoded, matches };
}

// Convert decoded bytes back to Uint8Array
function decodeToUint8Array(encodedString, length) {
  const decoded = decodeBase64(encodedString, length);
  return new Uint8Array(decoded);
}

// Utility for working with bcrypt's base64 format
class BcryptBase64 {
  static encode(bytes, length = bytes.length) {
    return encodeBase64(bytes, length);
  }
  
  static decode(str, length) {
    return decodeBase64(str, length);
  }
  
  static roundTrip(bytes) {
    const encoded = this.encode(bytes);
    const decoded = this.decode(encoded, bytes.length);
    return { encoded, decoded };
  }
}

Type Definitions

These utility functions work with the following types:

/**
 * Generic callback type for error-first callbacks
 */
type Callback<T> = (err: Error | null, result?: T) => void;

/**
 * Progress callback for long-running operations
 */
type ProgressCallback = (percentage: number) => void;

/**
 * Random number generator fallback function
 */
type RandomFallback = (length: number) => number[];

Security Considerations

Random Fallback Security

// CRITICAL: Random fallback must be cryptographically secure
// Good examples:
import { randomBytes } from 'node:crypto';
setRandomFallback((len) => Array.from(randomBytes(len)));

// Bad examples (DO NOT USE):
setRandomFallback((len) => 
  Array.from({length: len}, () => Math.floor(Math.random() * 256))
); // Math.random() is NOT cryptographically secure

setRandomFallback((len) => 
  Array.from({length: len}, () => Date.now() % 256)
); // Predictable and NOT secure

Password Truncation Implications

// Be aware of truncation implications
const longPassword = "very-long-password-that-might-be-truncated-by-bcrypt";

if (truncates(longPassword)) {
  console.warn("Password will be truncated - consider warning the user");
  // The effective password becomes only the first 72 UTF-8 bytes
}

// Good practice: Check before hashing
function safeHashPassword(password, rounds = 10) {
  if (truncates(password)) {
    throw new Error("Password too long and will be truncated");
  }
  return hashSync(password, rounds);
}

Common Patterns

// Hash analysis utility
function analyzeHash(hash) {
  return {
    rounds: getRounds(hash),
    salt: getSalt(hash),
    format: hash.startsWith('$2b$') ? 'bcrypt 2b' : 'other',
    length: hash.length
  };
}

// Password strength validation
function validatePasswordStrength(password) {
  const issues = [];
  
  if (password.length < 8) {
    issues.push('Too short (minimum 8 characters)');
  }
  
  if (truncates(password)) {
    issues.push('Too long (will be truncated at 72 UTF-8 bytes)');
  }
  
  if (!/[A-Z]/.test(password)) {
    issues.push('Missing uppercase letter');
  }
  
  if (!/[a-z]/.test(password)) {
    issues.push('Missing lowercase letter');
  }
  
  if (!/\d/.test(password)) {
    issues.push('Missing number');
  }
  
  return {
    valid: issues.length === 0,
    issues
  };
}

// Development utilities
const bcryptUtils = {
  analyzeHash,
  validatePasswordStrength,
  testTruncation: truncates,
  extractRounds: getRounds,
  extractSalt: getSalt
};