|
| 1 | +// |
| 2 | +// EIP4361.swift |
| 3 | +// |
| 4 | +// Created by JeneaVranceanu at 19.09.2022. |
| 5 | +// |
| 6 | + |
| 7 | +import Foundation |
| 8 | +import BigInt |
| 9 | +import Core |
| 10 | + |
| 11 | +public typealias SIWE = EIP4361 |
| 12 | + |
| 13 | +fileprivate let datetimePattern = "[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))" |
| 14 | +fileprivate let uriPattern = "(([^:?#]+):)?(([^?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?" |
| 15 | + |
| 16 | +/// Sign-In with Ethereum protocol and parser implementation. |
| 17 | +/// EIP-4361: |
| 18 | +/// - https://eips.ethereum.org/EIPS/eip-4361 |
| 19 | +/// - https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4361.md |
| 20 | +/// |
| 21 | +/// Thanks to spruceid for SIWE implementation that was rewritten here in Swift: https://github.com/spruceid/siwe/blob/main/packages/siwe-parser/lib/regex.ts |
| 22 | +public final class EIP4361 { |
| 23 | + |
| 24 | + private static let domain = "(?<domain>([^?#]*)) wants you to sign in with your Ethereum account:" |
| 25 | + private static let address = "\\n(?<address>0x[a-zA-Z0-9]{40})\\n\\n" |
| 26 | + private static let statementParagraph = "((?<statement>[^\\n]+)\\n)?" |
| 27 | + private static let uri = "\\nURI: (?<uri>(\(uriPattern))?)" |
| 28 | + private static let version = "\\nVersion: (?<version>1)" |
| 29 | + private static let chainId = "\\nChain ID: (?<chainId>[0-9a-fA-F]+)" |
| 30 | + private static let nonce = "\\nNonce: (?<nonce>[a-zA-Z0-9]{8,})" |
| 31 | + private static let issuedAt = "\\nIssued At: (?<issuedAt>(\(datetimePattern)))" |
| 32 | + private static let expirationTime = "(\\nExpiration Time: (?<expirationTime>(\(datetimePattern))))?" |
| 33 | + private static let notBefore = "(\\nNot Before: (?<notBefore>(\(datetimePattern))))?" |
| 34 | + private static let requestId = "(\\nRequest ID: (?<requestId>[-._~!$&'()*+,;=:@%a-zA-Z0-9]*))?" |
| 35 | + private static let resourcesParagraph = "(\\nResources:(?<resources>(\\n- (\(uriPattern))?)+))?" |
| 36 | + |
| 37 | + private static var eip4361Pattern: String { |
| 38 | + "^\(domain)\(address)\(statementParagraph)\(uri)\(version)\(chainId)\(nonce)\(issuedAt)\(expirationTime)\(notBefore)\(requestId)\(resourcesParagraph)$" |
| 39 | + } |
| 40 | + |
| 41 | + /// `domain` is the RFC 3986 authority that is requesting the signing. |
| 42 | + public let domain: String |
| 43 | + /// `address` is the Ethereum address performing the signing conformant to capitalization encoded checksum specified in EIP-55 where applicable. |
| 44 | + public let address: EthereumAddress |
| 45 | + /// `statement` (optional) is a human-readable ASCII assertion that the user will sign, and it must not contain '\n' (the byte 0x0a). |
| 46 | + public let statement: String? |
| 47 | + /// `uri` is an RFC 3986 URI referring to the resource that is the subject of the signing (as in the subject of a claim). |
| 48 | + public let uri: URL |
| 49 | + /// `version` is the current version of the message, which MUST be 1 for this specification. |
| 50 | + public let version: BigUInt |
| 51 | + /// `chain-id` is the EIP-155 Chain ID to which the session is bound, and the network where Contract Accounts MUST be resolved. |
| 52 | + public let chainId: BigUInt |
| 53 | + /// `nonce` is a randomized token typically chosen by the relying party and used to prevent replay attacks, at least 8 alphanumeric characters. |
| 54 | + public let nonce: String |
| 55 | + /// `issued-at` is the ISO 8601 datetime string of the current time. |
| 56 | + public let issuedAt: Date |
| 57 | + /// `expiration-time` (optional) is the ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid. |
| 58 | + public let expirationTime: Date? |
| 59 | + /// `not-before` (optional) is the ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid. |
| 60 | + public let notBefore: Date? |
| 61 | + /// `request-id` (optional) is an system-specific identifier that may be used to uniquely refer to the sign-in request. |
| 62 | + public let requestId: String? |
| 63 | + /// `resources` (optional) is a list of information or references to information the user wishes to have resolved |
| 64 | + /// as part of authentication by the relying party. They are expressed as RFC 3986 URIs separated by "\n- " where \n is the byte 0x0a. |
| 65 | + public let resources: [URL]? |
| 66 | + |
| 67 | + public init?(_ message: String) { |
| 68 | + let eip4361Regex = try! NSRegularExpression(pattern: EIP4361.eip4361Pattern) |
| 69 | + let groups = eip4361Regex.captureGroups(string: message) |
| 70 | + let dateFormatter = ISO8601DateFormatter() |
| 71 | + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] |
| 72 | + guard let domain = groups["domain"], |
| 73 | + let rawAddress = groups["address"], |
| 74 | + let address = EthereumAddress(rawAddress), |
| 75 | + let rawUri = groups["uri"], |
| 76 | + let uri = URL(string: rawUri), |
| 77 | + let rawVersion = groups["version"], |
| 78 | + let version = BigUInt(rawVersion, radix: 10) ?? BigUInt(rawVersion, radix: 16), |
| 79 | + let rawChainId = groups["chainId"], |
| 80 | + let chainId = BigUInt(rawChainId, radix: 10) ?? BigUInt(rawChainId, radix: 16), |
| 81 | + let nonce = groups["nonce"], |
| 82 | + let rawIssuedAt = groups["issuedAt"], |
| 83 | + let issuedAt = dateFormatter.date(from: rawIssuedAt) |
| 84 | + else { |
| 85 | + return nil |
| 86 | + } |
| 87 | + |
| 88 | + self.domain = domain |
| 89 | + self.address = address |
| 90 | + self.statement = groups["statement"] |
| 91 | + self.uri = uri |
| 92 | + self.version = version |
| 93 | + self.chainId = chainId |
| 94 | + self.nonce = nonce |
| 95 | + self.issuedAt = issuedAt |
| 96 | + expirationTime = dateFormatter.date(from: groups["expirationTime"] ?? "") |
| 97 | + notBefore = dateFormatter.date(from: groups["notBefore"] ?? "") |
| 98 | + requestId = groups["requestId"] |
| 99 | + if let rawResources = groups["resources"] { |
| 100 | + resources = rawResources.components(separatedBy: "\n- ").compactMap { URL(string: $0) } |
| 101 | + } else { |
| 102 | + resources = nil |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + public var description: String { |
| 107 | + var descriptionParts = [String]() |
| 108 | + descriptionParts.append("\(domain) wants you to sign in with your Ethereum account:") |
| 109 | + descriptionParts.append("\n\(address.address)") |
| 110 | + if let statement = statement { |
| 111 | + descriptionParts.append("\n\n\(statement)") |
| 112 | + } |
| 113 | + descriptionParts.append("\n\nURI: \(uri)") |
| 114 | + descriptionParts.append("\nVersion: \(version.description)") |
| 115 | + descriptionParts.append("\nChain ID: \(chainId.description)") |
| 116 | + descriptionParts.append("\nNonce: \(nonce)") |
| 117 | + let dateFormatter = ISO8601DateFormatter() |
| 118 | + dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] |
| 119 | + descriptionParts.append("\nIssued At: \(dateFormatter.string(from: issuedAt))") |
| 120 | + if let expirationTime = expirationTime { |
| 121 | + descriptionParts.append("\nExpiration Time: \(dateFormatter.string(from: expirationTime))") |
| 122 | + } |
| 123 | + if let notBefore = notBefore { |
| 124 | + descriptionParts.append("\nNot Before: \(dateFormatter.string(from: notBefore))") |
| 125 | + } |
| 126 | + if let requestId = requestId { |
| 127 | + descriptionParts.append("\nRequest ID: \(requestId)") |
| 128 | + } |
| 129 | + if let resources = resources, !resources.isEmpty { |
| 130 | + descriptionParts.append("\nResources:") |
| 131 | + descriptionParts.append(contentsOf: resources.map { "\n- \($0.absoluteString)" }) |
| 132 | + } |
| 133 | + return descriptionParts.joined() |
| 134 | + } |
| 135 | +} |
| 136 | + |
0 commit comments