Class: Passlib::PHPass
Overview
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.
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
Instance Attribute Summary
Attributes inherited from Password
Class Method Summary collapse
-
.create(secret, **options) ⇒ PHPass
Creates a new phpass hash.
Instance Method Summary collapse
-
#upgrade? ⇒ Boolean
trueif the stored iteration count differs from the configured target. -
#verify(secret) ⇒ Boolean
Verifies
secretagainst the stored hash.
Methods inherited from Password
available?, #create_comparable, #initialize, #inspect, load, #pretty_print
Methods included from Internal::DSL
Constructor Details
This class inherits a constructor from Passlib::Password
Class Method Details
.create(secret, **options) ⇒ PHPass
Creates a new phpass hash.
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] :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.
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.
62 63 64 |
# File 'lib/passlib/phpass.rb', line 62 def verify(secret) Passlib.secure_compare(@string[-22..], phpass_digest(secret)) end |