Class: Passlib::SHA1Crypt

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

Overview

Note:

SHA1-crypt is 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 SHA1-crypt password hashing (NetBSD’s crypt-sha1).

SHA1-crypt applies iterated HMAC-SHA1 to derive a password hash. It was designed by Simon Gerraty for NetBSD as an alternative to DES-crypt that supports long passwords. It is a legacy algorithm — supported here for verifying existing hashes and migrating users to a stronger scheme.

Hash format: $sha1$<rounds>$<salt>$<checksum>

  • rounds — decimal integer, iteration count (1–4,294,967,295)

  • salt — 1–64 characters from ./0-9A-Za-z (default: 8 random chars)

  • checksum — 28 characters from ./0-9A-Za-z

This format is compatible with the sha1_crypt as implemented in Python’s passlib.

Examples:

hash = Passlib::SHA1Crypt.create("hunter2")
hash.verify("hunter2")  # => true
hash.to_s               # => "$sha1$480000$...$..."

Constant Summary

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?, #initialize, #inspect, load, #pretty_print, #verify

Methods included from Internal::DSL

#identifier

Constructor Details

This class inherits a constructor from Passlib::Password

Class Method Details

.create(secret, **options) ⇒ SHA1Crypt

Creates a new SHA1-crypt hash.

Parameters:

  • secret (String)

    the plaintext password

Options Hash (**options):

  • :rounds (Integer)

    iteration count, 1–4,294,967,295 (default: 480,000)

  • :salt (String)

    custom salt string, up to 64 characters from ./0-9A-Za-z (default: 8 random characters)

Returns:



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
# File 'lib/passlib/sha1_crypt.rb', line 38

class SHA1Crypt < Password
  register :sha1_crypt, mcf: "sha1"
  options :rounds, :salt

  HASH_CHARS    = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  DEFAULT_ROUNDS = 480_000
  MIN_ROUNDS    = 1
  MAX_ROUNDS    = 4_294_967_295
  MAX_SALT_LEN  = 64

  # Byte-index groups for encoding the 20-byte SHA1 digest into 28 characters.
  # The last group wraps around to reuse byte 0, following the sha1crypt spec.
  ENCODE_OFFSETS = [
    [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11],
    [12, 13, 14], [15, 16, 17], [18, 19, 0],
  ].freeze

  LOAD_PATTERN = %r{\A\$sha1\$(\d+)\$([./0-9A-Za-z]{1,64})\$([./0-9A-Za-z]{28})\z}

  private_constant :HASH_CHARS, :DEFAULT_ROUNDS, :MIN_ROUNDS, :MAX_ROUNDS,
                   :MAX_SALT_LEN, :ENCODE_OFFSETS, :LOAD_PATTERN

  # @param secret [String] the plaintext password to re-hash
  # @return [SHA1Crypt] a new instance hashed with the same salt and rounds
  def create_comparable(secret)
    self.class.create(secret, salt: @salt, rounds: @rounds)
  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 create(secret)
    @rounds = (config.rounds || DEFAULT_ROUNDS).clamp(MIN_ROUNDS, MAX_ROUNDS)
    @salt   = (config.salt || random_salt)[0, MAX_SALT_LEN]
    "$sha1$#{@rounds}$#{@salt}$#{sha1_digest(secret)}"
  end

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

  def sha1_digest(secret)
    key    = secret.encode("UTF-8").b
    digest = OpenSSL::HMAC.digest("SHA1", key, "#{@salt}$sha1$#{@rounds}")
    (@rounds - 1).times { digest = OpenSSL::HMAC.digest("SHA1", key, digest) }
    encode(digest)
  end

  # Encodes the 20-byte digest into 28 characters using the sha1crypt
  # big-endian 24-bit grouping with wrap-around for the final group.
  def encode(digest)
    b = digest.bytes
    ENCODE_OFFSETS.map { |i, j, k|
      v = (b[i] << 16) | (b[j] << 8) | b[k]
      4.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) ⇒ SHA1Crypt

Returns a new instance hashed with the same salt and rounds.

Parameters:

  • secret (String)

    the plaintext password to re-hash

Returns:

  • (SHA1Crypt)

    a new instance hashed with the same salt and rounds



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

def create_comparable(secret)
  self.class.create(secret, salt: @salt, rounds: @rounds)
end

#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/sha1_crypt.rb', line 68

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