Class: Passlib::MD5Crypt
Overview
MD5-crypt is a legacy algorithm with a fixed, low round count. It is supported here for verifying existing hashes and migrating users to a stronger scheme. Do not use it for new hashes.
Handles MD5-crypt and Apache MD5-crypt password hashing.
MD5-crypt was designed by Poul-Henning Kamp for FreeBSD in 1994. It runs 1000 rounds of a complex MD5-based mixing function and was widely deployed on Linux systems as the default password scheme. Apache’s variant is identical except for using a different MCF identifier.
Two MCF identifiers are recognized:
-
$1$ — standard MD5-crypt (FreeBSD/Linux)
-
$apr1$ — Apache APR variant (functionally identical, different prefix)
New hashes created via MD5Crypt.create use $1$. To create APR hashes, pass variant: :apr.
Hash format: $1$<salt>$<checksum> or $apr1$<salt>$<checksum>
-
salt— 0-8 characters from./0-9A-Za-z(default: 8 random characters) -
checksum— 22 characters from./0-9A-Za-z
This format is compatible with the md5_crypt and apr_md5_crypt as implemented in Python’s passlib.
Constant Summary
Constants included from Internal::DSL
Instance Attribute Summary
Attributes inherited from Password
Class Method Summary collapse
-
.create(secret, **options) ⇒ MD5Crypt
Creates a new MD5-crypt hash.
Instance Method Summary collapse
-
#create_comparable(secret) ⇒ MD5Crypt
A new instance hashed with the same salt and variant.
-
#upgrade? ⇒ Boolean
MD5-crypt uses a fixed round count of 1000, so upgrade? is always false.
Methods inherited from Password
available?, #initialize, #inspect, load, #pretty_print, #verify
Methods included from Internal::DSL
Constructor Details
This class inherits a constructor from Passlib::Password
Class Method Details
.create(secret, **options) ⇒ MD5Crypt
Creates a new MD5-crypt hash.
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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/passlib/md5_crypt.rb', line 49 class MD5Crypt < Password register :md5_crypt, mcf: %w[1 apr1] :variant, :salt HASH_CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" MAX_SALT_LEN = 8 ROUNDS = 1000 MAGIC = { standard: "$1$", apr: "$apr1$" }.freeze # Byte-index groups for encoding the 16-byte MD5 digest into 22 characters. # The ordering matches the original FreeBSD md5crypt implementation by # Poul-Henning Kamp. The final group encodes only one byte as 2 characters. ENCODE_OFFSETS = [ [0, 6, 12, 4], [1, 7, 13, 4], [2, 8, 14, 4], [3, 9, 15, 4], [4, 10, 5, 4], [nil, nil, 11, 2], ].freeze LOAD_PATTERN = %r{\A(\$1\$|\$apr1\$)([./0-9A-Za-z]{0,8})\$([./0-9A-Za-z]{22})\z} private_constant :HASH_CHARS, :MAX_SALT_LEN, :ROUNDS, :MAGIC, :ENCODE_OFFSETS, :LOAD_PATTERN # @param secret [String] the plaintext password to re-hash # @return [MD5Crypt] a new instance hashed with the same salt and variant def create_comparable(secret) self.class.create(secret, salt: @salt, variant: @variant) end # MD5-crypt uses a fixed round count of 1000, so upgrade? is always false. # @return [Boolean] always +false+ def upgrade? false end private def create(secret) @variant = config.variant || :standard @magic = MAGIC.fetch(@variant) { raise ArgumentError, "unknown md5_crypt variant: #{@variant.inspect}" } @salt = (config.salt || random_salt)[0, MAX_SALT_LEN] "#{@magic}#{@salt}$#{md5_digest(secret)}" end def load(string) m = LOAD_PATTERN.match(string) or raise ArgumentError, "invalid md5_crypt hash: #{string.inspect}" @magic = m[1] @variant = MAGIC.key(@magic) @salt = m[2] string end def md5_digest(secret) pw = secret.encode("UTF-8").b s = @salt.b mag = @magic.b # Digest B = MD5(password + salt + password) digest_b = OpenSSL::Digest::MD5.digest(pw + s + pw) # Digest A = MD5(password + magic + salt + tiled_digest_b + bit_select) ctx_a = OpenSSL::Digest::MD5.new ctx_a.update(pw) ctx_a.update(mag) ctx_a.update(s) plen = pw.bytesize ctx_a.update(digest_b * (plen / 16)) if plen >= 16 ctx_a.update(digest_b[0, plen % 16]) if (plen % 16) > 0 len = plen while len > 0 ctx_a.update(len.odd? ? "\x00" : pw[0]) len >>= 1 end digest_a = ctx_a.digest # Main loop: 1000 rounds mixing the current digest with password and salt c = digest_a ROUNDS.times do |i| ctx_c = OpenSSL::Digest::MD5.new ctx_c.update(i.odd? ? pw : c) ctx_c.update(s) unless (i % 3).zero? ctx_c.update(pw) unless (i % 7).zero? ctx_c.update(i.odd? ? c : pw) c = ctx_c.digest end encode(c) end # Encodes the 16-byte MD5 digest into 22 characters using the FreeBSD # md5crypt byte ordering and the crypt64 alphabet. def encode(digest) b = digest.bytes ENCODE_OFFSETS.map { |w2, w1, w0, n| v = ((w2 ? b[w2] : 0) << 16) | ((w1 ? b[w1] : 0) << 8) | b[w0] n.times.map { c = HASH_CHARS[v & 0x3f]; v >>= 6; c }.join }.join end def random_salt Internal.random_bytes(6).bytes.map { HASH_CHARS[it & 63] }.join end end |
Instance Method Details
#create_comparable(secret) ⇒ MD5Crypt
Returns a new instance hashed with the same salt and variant.
74 75 76 |
# File 'lib/passlib/md5_crypt.rb', line 74 def create_comparable(secret) self.class.create(secret, salt: @salt, variant: @variant) end |
#upgrade? ⇒ Boolean
MD5-crypt uses a fixed round count of 1000, so upgrade? is always false.
80 81 82 |
# File 'lib/passlib/md5_crypt.rb', line 80 def upgrade? false end |