OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-lib / auth.cjs

145 lines · javascript ref:
/**
 * FILE: lib/auth.cjs
 * STATUS: ACTIVE
 * RUNTIME: Bare
 * MODULE: CommonJS
 *
 * PURPOSE:
 * API authentication system for wallet-server.cjs
 * Generates and verifies Bearer tokens for HTTP API access
 *
 * USED BY:
 * - wallet-server.cjs (authentication middleware)
 *
 * KEY EXPORTS:
 * - generateToken() - Generate secure random token
 * - saveToken() - Save token to file
 * - loadToken() - Load token from file
 * - verifyToken() - Verify Bearer token from request
 *
 * SECURITY:
 * - 32-byte random tokens (256-bit security)
 * - Token file mode 0600 (owner read/write only)
 * - Bearer token authentication (RFC 6750)
 */

const crypto = require('hypercore-crypto')
const fs = require('bare-fs')
const path = require('bare-path')
const os = require('bare-os')

const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
const TOKEN_FILE = path.join(STORAGE_DIR, '.api-token')

/**
 * Generate secure random API token
 * Uses hypercore-crypto for cryptographically secure random bytes
 *
 * @returns {string} - Hex-encoded 32-byte token (64 hex characters)
 */
function generateToken() {
  const randomBytes = crypto.randomBytes(32)
  return randomBytes.toString('hex')
}

/**
 * Save API token to file with secure permissions
 *
 * @param {string} token - API token to save
 */
function saveToken(token) {
  // Ensure storage directory exists
  if (!fs.existsSync(STORAGE_DIR)) {
    fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 })
  }

  // Write token with mode 0600 (owner read/write only)
  fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 })
}

/**
 * Load API token from file
 *
 * @returns {string|null} - Token if exists, null otherwise
 */
function loadToken() {
  if (!fs.existsSync(TOKEN_FILE)) {
    return null
  }

  try {
    return fs.readFileSync(TOKEN_FILE, 'utf8').trim()
  } catch (err) {
    console.error('Error loading API token:', err.message)
    return null
  }
}

/**
 * Verify Bearer token from HTTP request
 *
 * @param {object} req - HTTP request object
 * @returns {boolean} - True if token is valid, false otherwise
 */
function verifyToken(req) {
  // Get Authorization header
  const authHeader = req.headers.authorization

  // Check if header exists and has Bearer scheme
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return false
  }

  // Extract token (remove "Bearer " prefix)
  const token = authHeader.slice(7)

  // Load stored token
  const storedToken = loadToken()

  // Verify token matches
  if (!storedToken) {
    return false
  }

  // Use constant-time comparison to prevent timing attacks
  // Note: crypto.timingSafeEqual doesn't exist in Bare runtime
  // Implement manual constant-time comparison
  if (token.length !== storedToken.length) {
    return false
  }

  let diff = 0
  for (let i = 0; i < token.length; i++) {
    diff |= token.charCodeAt(i) ^ storedToken.charCodeAt(i)
  }

  return diff === 0
}

/**
 * Delete API token (for security - regenerate on demand)
 */
function deleteToken() {
  if (fs.existsSync(TOKEN_FILE)) {
    fs.unlinkSync(TOKEN_FILE)
  }
}

/**
 * Check if API token exists
 *
 * @returns {boolean} - True if token file exists
 */
function tokenExists() {
  return fs.existsSync(TOKEN_FILE)
}

module.exports = {
  generateToken,
  saveToken,
  loadToken,
  verifyToken,
  deleteToken,
  tokenExists
}