Class: Otto::Privacy::IPPrivacy

Inherits:
Object
  • Object
show all
Defined in:
lib/otto/privacy/ip_privacy.rb

Overview

Note:

All methods return UTF-8 encoded strings for Rack compatibility. See file:docs/ipaddr-encoding-quirk.md for details on IPAddr#to_s behavior.

IP address anonymization utilities

Provides methods for masking and hashing IP addresses to enhance privacy while maintaining the ability to track sessions and analyze traffic patterns.

Examples:

Mask an IPv4 address (1 octet)

IPPrivacy.mask_ip('192.168.1.100', 1)
# => '192.168.1.0'

Mask an IPv4 address (2 octets)

IPPrivacy.mask_ip('192.168.1.100', 2)
# => '192.168.0.0'

Hash an IP for session correlation

key = 'daily-rotation-key'
IPPrivacy.hash_ip('192.168.1.100', key)
# => 'a3f8b2...' (consistent for same IP+key, changes when key rotates)

Class Method Summary collapse

Class Method Details

.hash_ip(ip, key) ⇒ String

Hash an IP address for session correlation without storing the original

Uses HMAC-SHA256 with a daily-rotating key to create a consistent identifier for the same IP within a key rotation period, but different across rotations.

Parameters:

  • ip (String)

    IP address to hash

  • key (String)

    Secret key for HMAC (should rotate daily)

Returns:

  • (String)

    Hexadecimal hash string (64 characters)

Raises:

  • (ArgumentError)

    if IP or key is invalid



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/otto/privacy/ip_privacy.rb', line 78

def self.hash_ip(ip, key)
  return nil if ip.nil? || ip.empty?

  raise ArgumentError, 'Key cannot be nil or empty' if key.nil? || key.empty?

  # Normalize IP address format before hashing
  normalized_ip = begin
    IPAddr.new(ip).to_s
  rescue IPAddr::InvalidAddressError => e
    raise ArgumentError, "Invalid IP address: #{ip} - #{e.message}"
  end

  # Use HMAC-SHA256 for secure hashing with key
  OpenSSL::HMAC.hexdigest('SHA256', key, normalized_ip)
end

.mask_ip(ip, octet_precision = 1) ⇒ String

Mask an IP address by zeroing out the specified number of octets/bits

For IPv4:

  • octet_precision=1: Masks last octet (e.g., 192.168.1.100 → 192.168.1.0)

  • octet_precision=2: Masks last 2 octets (e.g., 192.168.1.100 → 192.168.0.0)

For IPv6:

  • octet_precision=1: Masks last 80 bits

  • octet_precision=2: Masks last 96 bits

Parameters:

  • ip (String)

    IP address to mask

  • octet_precision (Integer) (defaults to: 1)

    Number of trailing octets to mask (1 or 2, default: 1)

Returns:

  • (String)

    Masked IP address (UTF-8 encoded)

Raises:

  • (ArgumentError)

    if IP is invalid or octet_precision is not 1 or 2



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/otto/privacy/ip_privacy.rb', line 49

def self.mask_ip(ip, octet_precision = 1)
  return nil if ip.nil? || ip.empty?

  raise ArgumentError, "octet_precision must be 1 or 2, got: #{octet_precision}" unless [1,
                                                                                         2].include?(octet_precision)

  begin
    addr = IPAddr.new(ip)

    if addr.ipv4?
      mask_ipv4(addr, octet_precision)
    else
      mask_ipv6(addr, octet_precision)
    end
  rescue IPAddr::InvalidAddressError => e
    raise ArgumentError, "Invalid IP address: #{ip} - #{e.message}"
  end
end

.private_or_localhost?(ip) ⇒ Boolean

Check if an IP address is localhost or private (RFC 1918)

Private/localhost IPs are not masked for development convenience.

Parameters:

  • ip (String)

    IP address to check

Returns:

  • (Boolean)

    true if IP is localhost or private



113
114
115
116
117
118
119
120
# File 'lib/otto/privacy/ip_privacy.rb', line 113

def self.private_or_localhost?(ip)
  return false if ip.nil? || ip.empty?

  addr = IPAddr.new(ip)
  addr.private? || addr.loopback?
rescue IPAddr::InvalidAddressError
  false
end

.valid_ip?(ip) ⇒ Boolean

Check if an IP address is valid

Parameters:

  • ip (String)

    IP address to validate

Returns:

  • (Boolean)

    true if valid IPv4 or IPv6 address



98
99
100
101
102
103
104
105
# File 'lib/otto/privacy/ip_privacy.rb', line 98

def self.valid_ip?(ip)
  return false if ip.nil? || ip.empty?

  IPAddr.new(ip)
  true
rescue IPAddr::InvalidAddressError
  false
end