Skip to main content
Internal Documentation Only: If you’re not a Null Tools developer, you can close this documentation or visit the Apps section to learn more about using Null Pass in your applications.

Overview

IP addresses in audit logs are encrypted using AES-256-GCM encryption with user-specific keys. This ensures that IP addresses are stored securely and can only be decrypted by the user who owns the log entry.

Security Features

  • User-specific encryption: Each user’s IP addresses are encrypted with a key derived from their user ID
  • AES-256-GCM: Industry-standard authenticated encryption
  • PBKDF2 key derivation: 100,000 iterations with SHA-256
  • Automatic decryption: IP addresses are automatically decrypted when retrieved via the audit logs API

Implementation

Code Reference

import { NextRequest } from 'next/server'
import crypto from 'crypto'

const IP_ENCRYPTION_SECRET = process.env.IP_ENCRYPTION_SECRET
const ALGORITHM = 'aes-256-gcm'
const IV_LENGTH = 16

function deriveKey(userId: string): Buffer {
  const keyMaterial = `${userId}:${IP_ENCRYPTION_SECRET}`
  return crypto.pbkdf2Sync(keyMaterial, 'ip-encryption-salt', 100000, 32, 'sha256')
}

export function encryptIp(ip: string, userId: string): string {
  if (ip === 'unknown') {
    return 'unknown'
  }

  try {
    const key = deriveKey(userId)
    const iv = crypto.randomBytes(IV_LENGTH)
    const cipher = crypto.createCipheriv(ALGORITHM, key, iv)

    let encrypted = cipher.update(ip, 'utf8', 'hex')
    encrypted += cipher.final('hex')
    const tag = cipher.getAuthTag()

    return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`
  } catch (error) {
    return 'unknown'
  }
}

export function decryptIp(encryptedIp: string, userId: string): string {
  if (encryptedIp === 'unknown' || !encryptedIp.includes(':')) {
    return encryptedIp
  }

  try {
    const parts = encryptedIp.split(':')
    if (parts.length !== 3) {
      return 'unknown'
    }

    const [ivHex, tagHex, encrypted] = parts
    const iv = Buffer.from(ivHex, 'hex')
    const tag = Buffer.from(tagHex, 'hex')
    const key = deriveKey(userId)

    const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
    decipher.setAuthTag(tag)

    let decrypted = decipher.update(encrypted, 'hex', 'utf8')
    decrypted += decipher.final('utf8')

    return decrypted
  } catch (error) {
    return 'unknown'
  }
}

export function getClientIpFromHeaders(getHeader: (name: string) => string | null): string {
  const clientIp = getHeader('x-client-ip')
  if (clientIp) {
    if (/^(\d{1,3}\.){3}\d{1,3}$/.test(clientIp)) {
      return clientIp
    }
    return clientIp
  }
  
  const forwardedFor = getHeader('x-forwarded-for')
  const realIp = getHeader('x-real-ip')
  
  if (forwardedFor) {
    const ips = forwardedFor.split(',').map(ip => ip.trim())
    
    const ipv4 = ips.find(ip => {
      return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip)
    })
    
    if (ipv4) {
      return ipv4
    }
    
    if (ips.length > 0) {
      return ips[0]
    }
  }
  
  if (realIp) {
    if (/^(\d{1,3}\.){3}\d{1,3}$/.test(realIp)) {
      return realIp
    }
    return realIp
  }
  
  return 'unknown'
}

export function getClientIp(request: NextRequest): string {
  return getClientIpFromHeaders((name: string) => request.headers.get(name))
}

export function getClientIpForStorageFromHeaders(
  getHeader: (name: string) => string | null,
  userId: string
): string {
  const rawIp = getClientIpFromHeaders(getHeader)
  return encryptIp(rawIp, userId)
}

export function getClientIpForStorage(request: NextRequest, userId: string): string {
  return getClientIpForStorageFromHeaders((name: string) => request.headers.get(name), userId)
}

Functions

encryptIp(ip: string, userId: string): string

Encrypts an IP address using a user-specific key. Parameters:
  • ip: The IP address to encrypt (or 'unknown')
  • userId: The user ID used for key derivation
Returns: Encrypted IP string in format iv:tag:encrypted or 'unknown' Example:
const encrypted = encryptIp('192.168.1.1', 'user_123')
// Returns: "a1b2c3d4...:e5f6g7h8...:i9j0k1l2..."

decryptIp(encryptedIp: string, userId: string): string

Decrypts an encrypted IP address using a user-specific key. Parameters:
  • encryptedIp: The encrypted IP string (format iv:tag:encrypted)
  • userId: The user ID used for key derivation
Returns: Decrypted IP address or 'unknown' if decryption fails Example:
const decrypted = decryptIp('a1b2c3d4...:e5f6g7h8...:i9j0k1l2...', 'user_123')
// Returns: "192.168.1.1"

getClientIp(request: NextRequest): string

Extracts the client IP address from request headers. Priority order:
  1. x-client-ip header
  2. First IPv4 address from x-forwarded-for header
  3. First address from x-forwarded-for header
  4. x-real-ip header
  5. 'unknown' if no IP found
Example:
const ip = getClientIp(request)
// Returns: "192.168.1.1" or "unknown"

getClientIpForStorage(request: NextRequest, userId: string): string

Extracts and encrypts the client IP address for database storage. Example:
const encryptedIp = getClientIpForStorage(request, userId)
// Returns encrypted IP ready for storage

Encryption Format

Encrypted IP addresses are stored in the format:
{iv}:{tag}:{encrypted}
Where:
  • iv: Initialization vector (hex, 16 bytes)
  • tag: Authentication tag (hex, 16 bytes)
  • encrypted: Encrypted IP address (hex)

Key Derivation

Keys are derived using PBKDF2:
  • Algorithm: SHA-256
  • Iterations: 100,000
  • Key length: 32 bytes (256 bits)
  • Salt: 'ip-encryption-salt'
  • Key material: {userId}:{IP_ENCRYPTION_SECRET}

Environment Variables

IP_ENCRYPTION_SECRET
string
required
Secret key used for IP encryption key derivation. Must be set in production and kept secure.

Usage in Audit Logs

IP addresses are automatically encrypted when stored in audit logs:
const encryptedIp = getClientIpForStorage(request, userId)
await createAuditLog({
  userId,
  action: 'USER_LOGIN',
  data: {
    ip: encryptedIp, // Encrypted IP stored in database
    // ... other data
  }
})
When retrieving audit logs via the API, IP addresses are automatically decrypted:
const logs = await prisma.auditLog.findMany({ where: { userId } })
const logsWithDecryptedIp = logs.map(log => {
  const data = log.data as any
  if (data?.ip) {
    return {
      ...log,
      data: {
        ...data,
        ip: decryptIp(data.ip, userId), // Decrypted for API response
      }
    }
  }
  return log
})

Security Considerations

  • User isolation: Each user’s IP addresses are encrypted with a unique key derived from their user ID
  • Forward secrecy: If IP_ENCRYPTION_SECRET is compromised, existing encrypted IPs cannot be decrypted without the user ID
  • Authentication: GCM mode provides authentication, ensuring encrypted IPs cannot be tampered with
  • Fallback: Invalid or corrupted encrypted IPs return 'unknown' instead of throwing errors
IP addresses in audit log API responses are automatically decrypted. You don’t need to manually decrypt them when using the audit logs endpoint.