Class: Passlib::PHPass

Inherits:
Password show all
Defined in:
lib/passlib/phpass.rb

Overview

Note:

phpass is considered a legacy algorithm. It is supported here for verifying existing hashes and migrating users to a stronger scheme. Do not use it for new hashes.

Handles phpass Portable Hash password hashing.

phpass was widely used by PHP applications (WordPress, Drupal, phpBB) as a fallback when bcrypt was unavailable. It applies iterated MD5 with a configurable round count and an 8-character salt, encoding the result in a custom base64 alphabet.

Two MCF identifiers are recognized:

  • $P$ — standard phpass portable hash

  • $H$ — phpBB3 variant (identical algorithm, different identifier)

New hashes are always produced with the $P$ identifier.

Hash format: $P$<rounds_char><salt8><checksum22>

This format is compatible with the phpass portable hash as implemented in Python’s passlib.

Examples:

hash = Passlib::PHPass.create("hunter2")
hash.verify("hunter2")  # => true
hash.to_s               # => "$P$H..."

Constant Summary collapse

ITOA64 =

Alphabet used for the rounds character, salt, and checksum encoding.

"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
DEFAULT_ROUNDS =
19
MIN_ROUNDS =
7
MAX_ROUNDS =
30

Constants included from Internal::DSL

Internal::DSL::Config

Instance Attribute Summary

Attributes inherited from Password

#config, #string

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Password

available?, #create_comparable, #initialize, #inspect, load, #pretty_print

Methods included from Internal::DSL

#identifier

Constructor Details

This class inherits a constructor from Passlib::Password

Class Method Details

.create(secret, **options) ⇒ PHPass

Creates a new phpass hash.

Parameters:

  • secret (String)

    the plaintext password

Options Hash (**options):

  • :rounds (Integer)

    base-2 log of the iteration count, 7–30 (default: 19, i.e. 2^19 = 524 288 iterations)

  • :salt (String)

    custom 8-character salt using the phpass alphabet ./0-9A-Za-z (normally auto-generated)

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/passlib/phpass.rb', line 40

class PHPass < Password
  register mcf: %w[P H]
  options :rounds, :salt

  # Alphabet used for the rounds character, salt, and checksum encoding.
  ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

  DEFAULT_ROUNDS = 19
  MIN_ROUNDS     = 7
  MAX_ROUNDS     = 30

  FORMAT = /\A\$[PH]\$([.\/0-9A-Za-z])([.\/0-9A-Za-z]{8})([.\/0-9A-Za-z]{22})\z/
  private_constant :FORMAT

  # Verifies +secret+ against the stored hash.
  #
  # Overrides the default +create_comparable+ approach so that +$H$+ hashes
  # (phpBB3 variant) are verified correctly: both identifiers produce the same
  # 22-character checksum, so only that portion is compared.
  #
  # @param secret [String] the plaintext password
  # @return [Boolean]
  def verify(secret)
    Passlib.secure_compare(@string[-22..], phpass_digest(secret))
  end

  # @return [Boolean] +true+ if the stored iteration count differs from the
  #   configured target
  def upgrade?
    @rounds != (config.rounds || DEFAULT_ROUNDS).clamp(MIN_ROUNDS, MAX_ROUNDS)
  end

  private

  def load(string)
    m = FORMAT.match(string) or raise ArgumentError, "invalid phpass hash: #{string.inspect}"
    @rounds = ITOA64.index(m[1])
    @salt   = m[2]
    string
  end

  def create(secret)
    @rounds = (config.rounds || DEFAULT_ROUNDS).clamp(MIN_ROUNDS, MAX_ROUNDS)
    @salt   = config.salt || generate_salt
    "$P$#{ITOA64[@rounds]}#{@salt}#{phpass_digest(secret)}"
  end

  # Computes the 22-character phpass checksum.
  #
  # 1. +digest = MD5(salt + UTF-8 password)+
  # 2. Repeat +2^rounds+ times: +digest = MD5(digest + UTF-8 password)+
  # 3. Encode the 16-byte result with the phpass base64 encoding
  def phpass_digest(secret)
    pw     = secret.encode("UTF-8").b
    digest = OpenSSL::Digest::MD5.digest(@salt + pw)
    (1 << @rounds).times { digest = OpenSSL::Digest::MD5.digest(digest + pw) }
    encode64(digest)
  end

  # Encodes +bytes+ using the phpass custom base64 scheme.
  #
  # Groups bytes in sets of three and packs them into four 6-bit characters
  # using LSB-first bit ordering (unlike standard base64 which is MSB-first).
  # For 16 bytes this yields exactly 22 characters; for 6 bytes, 8 characters.
  def encode64(bytes)
    result = +""
    i = 0
    while i < bytes.bytesize
      v = bytes.getbyte(i)
      result << ITOA64[v & 0x3f]
      v |= bytes.getbyte(i + 1) << 8 if i + 1 < bytes.bytesize
      result << ITOA64[(v >> 6) & 0x3f]
      break if i + 1 >= bytes.bytesize
      v |= bytes.getbyte(i + 2) << 16 if i + 2 < bytes.bytesize
      result << ITOA64[(v >> 12) & 0x3f]
      break if i + 2 >= bytes.bytesize
      result << ITOA64[(v >> 18) & 0x3f]
      i += 3
    end
    result
  end

  # Generates a random 8-character salt by encoding 6 random bytes.
  def generate_salt
    encode64(Internal.random_bytes(6))
  end
end

Instance Method Details

#upgrade?Boolean

Returns true if the stored iteration count differs from the configured target.

Returns:

  • (Boolean)

    true if the stored iteration count differs from the configured target



68
69
70
# File 'lib/passlib/phpass.rb', line 68

def upgrade?
  @rounds != (config.rounds || DEFAULT_ROUNDS).clamp(MIN_ROUNDS, MAX_ROUNDS)
end

#verify(secret) ⇒ Boolean

Verifies secret against the stored hash.

Overrides the default create_comparable approach so that $H$ hashes (phpBB3 variant) are verified correctly: both identifiers produce the same 22-character checksum, so only that portion is compared.

Parameters:

  • secret (String)

    the plaintext password

Returns:

  • (Boolean)


62
63
64
# File 'lib/passlib/phpass.rb', line 62

def verify(secret)
  Passlib.secure_compare(@string[-22..], phpass_digest(secret))
end