Module: Siwe::Util

Defined in:
lib/siwe/util.rb

Constant Summary collapse

NONCE_LENGTH =
17
ISO8601_REGEX =
/
  \A
  (?<date>\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))
  [Tt]
  (?:[01]\d|2[0-3]):
  [0-5]\d:
  (?:[0-5]\d|60)
  (?:\.\d+)?
  (?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)
  \z
/x
HOST_CHAR_REGEX =

Character classes per RFC 3986. Anything outside these is rejected. unreserved + sub-delims + pct-encoded marker, used in reg-name.

/\A(?:[A-Za-z0-9\-._~!$&'()*+,;=]|%[0-9A-Fa-f]{2})*\z/
USERINFO_CHAR_REGEX =
/\A(?:[A-Za-z0-9\-._~!$&'()*+,;=:]|%[0-9A-Fa-f]{2})*\z/
PCHAR_REGEX =

pchar = unreserved / pct-encoded / sub-delims / “:” / “@”. Used for request-id (ABNF “request-id = *pchar”) and segments within a path.

/\A(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*\z/
PATH_REGEX =

path = *( pchar / “/” ). No “?” or “#”.

%r{\A(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*\z}
QUERY_FRAGMENT_REGEX =

query / fragment = *( pchar / “/” / “?” ).

%r{\A(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*\z}
PORT_REGEX =
/\A\d*\z/
SCHEME_REGEX =
/\A[A-Za-z][A-Za-z0-9+\-.]*\z/

Class Method Summary collapse

Class Method Details

.address_case(addr) ⇒ Object



69
70
71
72
73
74
75
76
77
78
# File 'lib/siwe/util.rb', line 69

def address_case(addr)
  return :invalid unless addr.is_a?(String) && addr.match?(/\A0x[0-9a-fA-F]{40}\z/)

  hex = addr[2..]
  return :lower if hex == hex.downcase
  return :upper if hex == hex.upcase

  eip55 = checksum_address(addr)
  eip55 == addr ? :checksum : :invalid_checksum
end

.checksum_address(addr) ⇒ Object



80
81
82
83
84
# File 'lib/siwe/util.rb', line 80

def checksum_address(addr)
  Eth::Address.new(addr).to_s
rescue StandardError
  nil
end

.generate_nonceObject



41
42
43
# File 'lib/siwe/util.rb', line 41

def generate_nonce
  SecureRandom.alphanumeric(NONCE_LENGTH)
end

.split_host_port(str) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/siwe/util.rb', line 162

def split_host_port(str)
  if str.start_with?("[")
    close = str.index("]")
    return [nil, nil] if close.nil?

    host = str[0..close]
    rest = str[(close + 1)..]
    if rest.empty?
      [host, nil]
    elsif rest.start_with?(":")
      [host, rest[1..]]
    else
      [nil, nil]
    end
  elsif (colon = str.rindex(":"))
    [str[0...colon], str[(colon + 1)..]]
  else
    [str, nil]
  end
end

.valid_address?(addr) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
67
# File 'lib/siwe/util.rb', line 60

def valid_address?(addr)
  return false if addr.nil? || addr.empty?

  Eth::Address.new(addr)
  true
rescue StandardError
  false
end

.valid_authority?(str) ⇒ Boolean

Validate an RFC 3986 authority: [userinfo “@”] host [“:” port]. Used for both the SIWE ‘domain` field and any URI’s authority component. Empty authority is valid (empty reg-name).

Returns:

  • (Boolean)


89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/siwe/util.rb', line 89

def valid_authority?(str)
  return false if str.nil?
  return true if str.empty?

  userinfo, host_port = str.include?("@") ? str.split("@", 2) : [nil, str]
  return false if userinfo && !USERINFO_CHAR_REGEX.match?(userinfo)

  host, port = split_host_port(host_port)
  return false if host.nil?
  return false if port && !PORT_REGEX.match?(port)

  valid_host?(host)
end

.valid_dotted_ipv4?(addr) ⇒ Boolean

Returns:

  • (Boolean)


155
156
157
158
159
160
# File 'lib/siwe/util.rb', line 155

def valid_dotted_ipv4?(addr)
  octets = addr.split(".", -1)
  return false unless octets.length == 4

  octets.all? { |o| o.match?(/\A\d{1,3}\z/) && o.to_i <= 255 }
end

.valid_hier_part?(hier) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
187
188
189
190
191
# File 'lib/siwe/util.rb', line 183

def valid_hier_part?(hier)
  if hier.start_with?("//")
    rest = hier[2..]
    authority_end = rest.index("/") || rest.length
    valid_authority?(rest[0...authority_end]) && PATH_REGEX.match?(rest[authority_end..])
  else
    PATH_REGEX.match?(hier)
  end
end

.valid_host?(host) ⇒ Boolean

host = IP-literal / IPv4address / reg-name. Distinguishes by leading “[”.

Returns:

  • (Boolean)


121
122
123
124
125
126
127
# File 'lib/siwe/util.rb', line 121

def valid_host?(host)
  if host.start_with?("[") && host.end_with?("]")
    valid_ip_literal?(host[1..-2])
  else
    HOST_CHAR_REGEX.match?(host)
  end
end

.valid_ip_literal?(content) ⇒ Boolean

IP-literal = IPv6address / IPvFuture (latter is rare and unused in SIWE — reject).

Returns:

  • (Boolean)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/siwe/util.rb', line 130

def valid_ip_literal?(content)
  return false if content.nil? || content.empty?
  return false if content.start_with?("v", "V") # IPvFuture not supported

  ipv6 = content
  if content.include?(".")
    last_colon = content.rindex(":")
    return false if last_colon.nil?

    ipv4 = content[(last_colon + 1)..]
    return false unless valid_dotted_ipv4?(ipv4)

    # IPAddr rejects leading zeros in IPv4 octets ("zero-filled ambiguous"), but the
    # SIWE test vectors treat them as valid. Rewrite the IPv4 tail as two h16 groups.
    octets = ipv4.split(".").map(&:to_i)
    g1 = format("%x", (octets[0] << 8) | octets[1])
    g2 = format("%x", (octets[2] << 8) | octets[3])
    ipv6 = "#{content[0..last_colon]}#{g1}:#{g2}"
  end

  IPAddr.new(ipv6).ipv6?
rescue IPAddr::InvalidAddressError
  false
end

.valid_iso8601?(str) ⇒ Boolean

Returns:

  • (Boolean)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/siwe/util.rb', line 45

def valid_iso8601?(str)
  return false if str.nil? || str.empty?

  m = ISO8601_REGEX.match(str)
  return false if m.nil?

  year, month, day = m[:date].split("-").map(&:to_i)
  return false unless Date.valid_date?(year, month, day)

  Time.iso8601(str)
  true
rescue ArgumentError
  false
end

.valid_uri?(str) ⇒ Boolean

Validate an absolute RFC 3986 URI: scheme “:” hier-part [ “?” query ] [ “#” fragment ].

Returns:

  • (Boolean)


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/siwe/util.rb', line 104

def valid_uri?(str)
  return false if str.nil? || str.empty?

  scheme, rest = str.split(":", 2)
  return false if rest.nil?
  return false unless SCHEME_REGEX.match?(scheme)

  m = rest.match(/\A(?<hier>[^?#]*)(?:\?(?<query>[^#]*))?(?:\#(?<frag>.*))?\z/)
  return false if m.nil?
  return false unless valid_hier_part?(m[:hier])
  return false if m[:query] && !QUERY_FRAGMENT_REGEX.match?(m[:query])
  return false if m[:frag]  && !QUERY_FRAGMENT_REGEX.match?(m[:frag])

  true
end