Class: Passlib::SHA2Crypt
- Defined in:
- lib/passlib/sha2_crypt.rb
Overview
Handles SHA-256 ($5$) and SHA-512 ($6$) crypt password hashing via OpenSSL.
Implements the SHA-crypt algorithm as specified by Ulrich Drepper (www.akkadia.org/drepper/SHA-crypt.txt). Both 256-bit and 512-bit variants are supported and auto-detected on load.
Constant Summary
Constants included from Internal::DSL
Instance Attribute Summary
Attributes inherited from Password
Class Method Summary collapse
-
.create(secret, **options) ⇒ SHA2Crypt
Creates a new SHA-crypt hash.
Instance Method Summary collapse
-
#create_comparable(secret) ⇒ SHA2Crypt
A new instance hashed with the same salt, rounds, and bits.
- #upgrade? ⇒ Boolean
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) ⇒ SHA2Crypt
Creates a new SHA-crypt hash.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/passlib/sha2_crypt.rb', line 24 class SHA2Crypt < Password HASH_CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" IMPLICIT_ROUNDS = 5_000 MIN_ROUNDS = 1_000 MAX_ROUNDS = 999_999_999 MAX_SALT_LEN = 16 VARIANTS = { 256 => { mcf_id: "5", digest: OpenSSL::Digest::SHA256, block_size: 32, default_rounds: 535_000, checksum_len: 43 }, 512 => { mcf_id: "6", digest: OpenSSL::Digest::SHA512, block_size: 64, default_rounds: 656_000, checksum_len: 86 }, }.freeze # Byte index groups for SHA-crypt base64 encoding. # nil entries encode as 0 (padding for the final partial group). ENCODE_GROUPS = { 256 => [ [0, 10, 20, 4], [21, 1, 11, 4], [12, 22, 2, 4], [3, 13, 23, 4], [24, 4, 14, 4], [15, 25, 5, 4], [6, 16, 26, 4], [27, 7, 17, 4], [18, 28, 8, 4], [9, 19, 29, 4], [nil, 31, 30, 3], ].freeze, 512 => [ [0, 21, 42, 4], [22, 43, 1, 4], [44, 2, 23, 4], [3, 24, 45, 4], [25, 46, 4, 4], [47, 5, 26, 4], [6, 27, 48, 4], [28, 49, 7, 4], [50, 8, 29, 4], [9, 30, 51, 4], [31, 52, 10, 4], [53, 11, 32, 4], [12, 33, 54, 4], [34, 55, 13, 4], [56, 14, 35, 4], [15, 36, 57, 4], [37, 58, 16, 4], [59, 17, 38, 4], [18, 39, 60, 4], [40, 61, 19, 4], [62, 20, 41, 4], [nil, nil, 63, 2], ].freeze, }.freeze MCF_TO_BITS = { "5" => 256, "6" => 512 }.freeze LOAD_PATTERN = %r{\A\$(5|6)\$(?:rounds=(\d+)\$)?([./0-9A-Za-z]{0,16})\$([./0-9A-Za-z]+)\z} private_constant :HASH_CHARS, :IMPLICIT_ROUNDS, :MIN_ROUNDS, :MAX_ROUNDS, :MAX_SALT_LEN, :VARIANTS, :ENCODE_GROUPS, :MCF_TO_BITS, :LOAD_PATTERN register :sha2_crypt, mcf: %w[5 6] :bits, :rounds, :salt # @param secret [String] the plaintext password to re-hash # @return [SHA2Crypt] a new instance hashed with the same salt, rounds, and bits def create_comparable(secret) self.class.create(secret, salt: @salt, rounds: @rounds, bits: @bits) end def upgrade? @rounds != (config.rounds || VARIANTS[@bits][:default_rounds]) end private def create(secret) @bits = config.bits || 512 variant = VARIANTS[@bits] or raise ArgumentError, "invalid bits: #{@bits.inspect}, must be 256 or 512" rounds = config.rounds || variant[:default_rounds] @rounds = rounds.clamp(MIN_ROUNDS, MAX_ROUNDS) @salt = (config.salt || random_salt)[0, MAX_SALT_LEN] crypt(secret.to_s.b, @salt.b, @rounds, variant) end def load(string) match = LOAD_PATTERN.match(string) or raise UnknownHashFormat, "invalid sha_crypt hash: #{string.inspect}" @bits = MCF_TO_BITS[match[1]] variant = VARIANTS[@bits] raise UnknownHashFormat, "invalid sha_crypt hash: #{string.inspect}" unless match[4].length == variant[:checksum_len] @rounds = match[2] ? match[2].to_i : IMPLICIT_ROUNDS @salt = match[3] string end def random_salt Internal.random_bytes(MAX_SALT_LEN).bytes.map { HASH_CHARS[it & 63] }.join end def crypt(password, salt, rounds, variant) digest_class = variant[:digest] block_size = variant[:block_size] mcf_id = variant[:mcf_id] # Steps 1-3: start digest A, add password and salt ctx_a = digest_class.new ctx_a.update(password) ctx_a.update(salt) # Steps 4-8: digest B = digest(password + salt + password) ctx_b = digest_class.new ctx_b.update(password) ctx_b.update(salt) ctx_b.update(password) digest_b = ctx_b.digest # Steps 9-10: add digest B to A in block_size-byte blocks, then the remainder plen = password.bytesize (plen / block_size).times { ctx_a.update(digest_b) } ctx_a.update(digest_b.b[0, plen % block_size]) if (plen % block_size) > 0 # Step 11: for each bit of plen (LSB first): 1-bit → add digest B, 0-bit → add password len = plen while len > 0 ctx_a.update(len.odd? ? digest_b : password) len >>= 1 end # Step 12: finish digest A digest_a = ctx_a.digest # Steps 13-16: P sequence — repeat password plen times through digest, tile to plen bytes ctx_p = digest_class.new plen.times { ctx_p.update(password) } digest_p = ctx_p.digest p_str = (digest_p * ((plen / block_size) + 1)).b[0, plen] # Steps 17-20: S sequence — repeat salt (16 + first byte of digest A) times, tile to slen bytes slen = salt.bytesize ctx_s = digest_class.new (16 + digest_a.getbyte(0)).times { ctx_s.update(salt) } digest_s = ctx_s.digest s_str = (digest_s * ((slen / block_size) + 1)).b[0, slen] # Step 21: main loop — rounds iterations mixing C, P, and S c = digest_a rounds.times do |i| ctx_c = digest_class.new ctx_c.update(i.odd? ? p_str : c) ctx_c.update(s_str) unless (i % 3).zero? ctx_c.update(p_str) unless (i % 7).zero? ctx_c.update(i.odd? ? c : p_str) c = ctx_c.digest end # Step 22: encode output with variant-specific byte ordering encoded = encode(c) rounds == IMPLICIT_ROUNDS ? "$#{mcf_id}$#{salt}$#{encoded}" : "$#{mcf_id}$rounds=#{rounds}$#{salt}$#{encoded}" end def encode(digest) b = digest.bytes ENCODE_GROUPS[@bits].map { |w2, w1, w0, n| b64_group(w2 ? b[w2] : 0, w1 ? b[w1] : 0, w0 ? b[w0] : 0, n) }.join end # Encode (w2, w1, w0) as n characters from HASH_CHARS, extracting 6-bit chunks LSB-first def b64_group(w2, w1, w0, n) v = (w2 << 16) | (w1 << 8) | w0 n.times.map { c = HASH_CHARS[v & 0x3f]; v >>= 6; c }.join end end |
Instance Method Details
#create_comparable(secret) ⇒ SHA2Crypt
Returns a new instance hashed with the same salt, rounds, and bits.
65 66 67 |
# File 'lib/passlib/sha2_crypt.rb', line 65 def create_comparable(secret) self.class.create(secret, salt: @salt, rounds: @rounds, bits: @bits) end |
#upgrade? ⇒ Boolean
69 70 71 |
# File 'lib/passlib/sha2_crypt.rb', line 69 def upgrade? @rounds != (config.rounds || VARIANTS[@bits][:default_rounds]) end |