Class: Siwe::Message

Inherits:
Object
  • Object
show all
Defined in:
lib/siwe/message.rb

Overview

Class that defines the EIP-4361 message fields and some utility methods to generate/validate the messages

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(domain, address, uri, version, options = {}) ⇒ Message

Returns a new instance of Message.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/siwe/message.rb', line 78

def initialize(domain, address, uri, version, options = {})
  @domain = domain
  @address = address
  @uri = uri
  @version = version
  @statement = options.fetch :statement, ""
  @issued_at = options.fetch :issued_at, Time.now.utc.iso8601
  @nonce = options.fetch :nonce, Siwe::Util.generate_nonce
  @chain_id = options.fetch :chain_id, 1
  @expiration_time = options.fetch :expiration_time, ""
  @not_before = options.fetch :not_before, ""
  @request_id = options.fetch :request_id, ""
  @resources = options.fetch :resources, []
  validate
end

Instance Attribute Details

#addressObject

Ethereum address performing the signing conformant to capitalization encoded checksum specified in EIP-55 where applicable.



37
38
39
# File 'lib/siwe/message.rb', line 37

def address
  @address
end

#chain_idObject

EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts must be resolved.



48
49
50
# File 'lib/siwe/message.rb', line 48

def chain_id
  @chain_id
end

#domainObject

RFC 4501 dns authority that is requesting the signing.



33
34
35
# File 'lib/siwe/message.rb', line 33

def domain
  @domain
end

#expiration_timeObject

ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid.



63
64
65
# File 'lib/siwe/message.rb', line 63

def expiration_time
  @expiration_time
end

#issued_atObject

ISO 8601 datetime string of the current time.



55
56
57
# File 'lib/siwe/message.rb', line 55

def issued_at
  @issued_at
end

#nonceObject

Randomized token used to prevent replay attacks, at least 8 alphanumeric characters.



52
53
54
# File 'lib/siwe/message.rb', line 52

def nonce
  @nonce
end

#not_beforeObject

ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid.



67
68
69
# File 'lib/siwe/message.rb', line 67

def not_before
  @not_before
end

#request_idObject

System-specific identifier that may be used to uniquely refer to the sign-in request.



71
72
73
# File 'lib/siwe/message.rb', line 71

def request_id
  @request_id
end

#resourcesObject

List of information or references to information the user wishes to have resolved as part of authentication by the relying party. They are expressed as RFC 3986 URIs separated by ‘n- `.



76
77
78
# File 'lib/siwe/message.rb', line 76

def resources
  @resources
end

#statementObject

Human-readable ASCII assertion that the user will sign, and it must not contain ‘n`.



59
60
61
# File 'lib/siwe/message.rb', line 59

def statement
  @statement
end

#uriObject

RFC 3986 URI referring to the resource that is the subject of the signing (as in the __subject__ of a claim).



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

def uri
  @uri
end

#versionObject

Current version of the message.



44
45
46
# File 'lib/siwe/message.rb', line 44

def version
  @version
end

Class Method Details

.from_json_string(str) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/siwe/message.rb', line 135

def self.from_json_string(str)
  obj = JSON.parse str, { symbolize_names: true }
  Siwe::Message.new(
    obj[:domain],
    obj[:address],
    obj[:uri],
    obj[:version], {
      chain_id: obj[:chain_id],
      nonce: obj[:nonce],
      issued_at: obj[:issued_at],
      statement: obj[:statement],
      expiration_time: obj[:expiration_time],
      not_before: obj[:not_before],
      request_id: obj[:request_id],
      resources: obj[:resources]
    }
  )
end

.from_message(msg) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/siwe/message.rb', line 94

def self.from_message(msg)
  message = msg.match SIWE_MESSAGE

  raise Siwe::UnableToParseMessage unless message.to_s == msg

  new(
    message[:domain],
    message[:address],
    message[:uri],
    message[:version],
    {
      statement: message[:statement],
      issued_at: message[:issued_at],
      nonce: message[:nonce],
      chain_id: message[:chain_id].to_i,
      expiration_time: message[:expiration_time],
      not_before: message[:not_before],
      request_id: message[:request_id],
      resources: message[:resources]&.split("\n- ")&.drop(1)
    }
  )
end

Instance Method Details

#prepare_messageObject



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/siwe/message.rb', line 222

def prepare_message
  greeting = "#{@domain} wants you to sign in with your Ethereum account:"
  address = @address
  statement = "\n#{@statement}\n"

  header = [greeting, address]

  if @statement.nil? || @statement.empty?
    header.push "\n"
  else
    header.push statement
  end

  header = header.join "\n"

  uri = "URI: #{@uri}"
  version = "Version: #{@version}"
  chain_id = "Chain ID: #{@chain_id}"
  nonce = "Nonce: #{@nonce}"
  issued_at = "Issued At: #{@issued_at}"

  body = [uri, version, chain_id, nonce, issued_at]

  expiration_time = "Expiration Time: #{@expiration_time}"
  not_before = "Not Before: #{@not_before}"
  request_id = "Request ID: #{@request_id}"

  body.push expiration_time unless @expiration_time.to_s.strip.empty?

  body.push not_before unless @not_before.to_s.strip.empty?

  body.push request_id unless @request_id.to_s.strip.empty?

  body.push "Resources:\n#{@resources.map { |x| "- #{x}" }.join "\n"}" unless @resources.nil? || @resources.empty?

  body = body.join "\n"

  [header, body].join "\n"
end

#to_json_stringObject



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/siwe/message.rb', line 117

def to_json_string
  obj = {
    domain: @domain,
    address: Eth::Address.new(@address).to_s,
    uri: @uri,
    version: @version,
    chain_id: @chain_id,
    nonce: @nonce,
    issued_at: @issued_at,
    statement: @statement,
    expiration_time: @expiration_time,
    not_before: @not_before,
    request_id: @request_id,
    resources: @resources
  }
  obj.to_json
end

#validateObject



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/siwe/message.rb', line 154

def validate
  # check domain
  raise Siwe::InvalidDomain unless @domain.match %r{[^/?#]*} || @domain.empty?

  # check address EIP-55
  raise Siwe::InvalidAddress unless Eth::Address.new(@address).to_s.eql? @address

  # check uri
  raise Siwe::InvalidURI unless URI.parse(@uri)

  # check version
  raise Siwe::InvalidMessageVersion unless @version == "1"

  # check if the nonce is alphanumeric and bigger then 8 characters
  raise Siwe::InvalidNonce unless @nonce.match(%r{[a-zA-Z0-9]{8,}})

  # check issued_at format
  begin
    Time.iso8601(@issued_at)
  rescue ArgumentError
    raise Siwe::InvalidTimeFormat, "issued_at"
  end

  # check exp_time
  begin
    Time.iso8601(@expiration_time) unless @expiration_time.nil? || @expiration_time.empty?
  rescue ArgumentError
    raise Siwe::InvalidTimeFormat, "expiration_time"
  end

  # check not_before
  begin
    Time.iso8601(@not_before) unless @not_before.nil? || @not_before.empty?
  rescue ArgumentError
    raise Siwe::InvalidTimeFormat, "not_before"
  end

  # check resources
  raise Siwe::InvalidURI unless @resources.nil? || @resources.empty? || @resources.each { |uri| URI.parse(uri) }
end

#verify(signature, domain, time, nonce) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/siwe/message.rb', line 195

def verify(signature, domain, time, nonce)
  raise Siwe::DomainMismatch unless domain.nil? || domain.eql?(@domain)

  raise Siwe::NonceMismatch unless nonce.nil? || nonce.eql?(@nonce)

  check_time = time.nil? ? Time.now.utc : Time.iso8601(time)

  raise Siwe::ExpiredMessage if (!@expiration_time.nil? && !@expiration_time.empty?) && check_time > Time.iso8601(@expiration_time)

  raise Siwe::NotValidMessage if (!@not_before.nil? && !@not_before.empty?) && check_time < Time.iso8601(@not_before)

  raise Siwe::InvalidSignature if signature.nil? && signature.empty?

  raise Siwe::InvalidAddress unless @address.eql?(Eth::Address.new(@address).to_s)

  begin
    pub_key = Eth::Signature.personal_recover prepare_message, signature
    signature_address = Eth::Util.public_key_to_address pub_key
  rescue StandardError
    raise Siwe::InvalidSignature
  end

  raise Siwe::InvalidSignature unless signature_address.to_s.downcase.eql? @address.to_s.downcase

  true
end