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.



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

def initialize(domain, address, uri, version, options = {})
  @domain = domain
  begin
    @address = Eth::Address.new(address).to_s
  rescue StandardError
    raise Siwe::InvalidAddress
  end
  raise Siwe::InvalidAddress unless @address.eql? 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, []
end

Instance Attribute Details

#addressObject

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



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

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.



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

def chain_id
  @chain_id
end

#domainObject

RFC 4501 dns authority that is requesting the signing.



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

def domain
  @domain
end

#expiration_timeObject

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



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

def expiration_time
  @expiration_time
end

#issued_atObject

ISO 8601 datetime string of the current time.



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

def issued_at
  @issued_at
end

#nonceObject

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



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

def nonce
  @nonce
end

#not_beforeObject

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



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

def not_before
  @not_before
end

#request_idObject

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



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

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- `.



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

def resources
  @resources
end

#statementObject

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



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

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).



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

def uri
  @uri
end

#versionObject

Current version of the message.



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

def version
  @version
end

Class Method Details

.from_json_string(str) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/siwe/message.rb', line 140

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



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/siwe/message.rb', line 98

def self.from_message(msg)
  if (message = msg.match SIWE_MESSAGE)
    new(
      message[:domain],
      Eth::Address.new(message[:address]).to_s,
      message[:uri],
      message[:version],
      {
        statement: message[:statement] || "",
        issued_at: message[:issued_at],
        nonce: message[:nonce],
        chain_id: message[:chain_id],
        expiration_time: message[:expiration_time] || "",
        not_before: message[:not_before] || "",
        request_id: message[:request_id] || "",
        resources: message[:resources]&.split("\n- ")&.drop(1) || []
      }
    )

  else
    throw "Invalid message input."
  end
end

Instance Method Details

#prepare_messageObject



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/siwe/message.rb', line 177

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.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}"
  resources = "Resources:\n#{@resources.map { |x| "- #{x}" }.join "\n"}"

  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 unless @resources.empty?

  body = body.join "\n"

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

#to_json_stringObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/siwe/message.rb', line 122

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

#validate(signature) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/siwe/message.rb', line 159

def validate(signature)
  raise Siwe::ExpiredMessage if !@expiration_time.empty? && Time.now.utc > Time.parse(@expiration_time)
  raise Siwe::NotValidMessage if !@not_before.empty? && Time.now.utc < Time.parse(@not_before)

  raise Siwe::InvalidSignature if signature.empty?

  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