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:
x-client-ip header
- First IPv4 address from
x-forwarded-for header
- First address from
x-forwarded-for header
x-real-ip header
'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
Encrypted IP addresses are stored in the format:
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
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.