-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #332 from oasisprotocol/matevz/sapphire-contracts-…
…auth sapphire-contracts: Add EIP-4361 SIWE parser and base contract
- Loading branch information
Showing
13 changed files
with
1,293 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.0; | ||
|
||
/** | ||
* @title Utility for converting date and time to timestamp | ||
* @notice Considers leap year, but not leap second. | ||
* @custom:attribution https://github.com/pipermerriam/ethereum-datetime/blob/master/contracts/DateTime.sol | ||
*/ | ||
library DateTime { | ||
uint16 private constant ORIGIN_YEAR = 1970; | ||
|
||
function isLeapYear(uint16 year) internal pure returns (bool) { | ||
if (year % 4 != 0) { | ||
return false; | ||
} | ||
if (year % 100 != 0) { | ||
return true; | ||
} | ||
if (year % 400 != 0) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* @notice Convert year, month, day, hour, minute, second to Unix timestamp. | ||
* @dev Leap second is not supported. | ||
*/ | ||
function toTimestamp( | ||
uint16 year, | ||
uint8 month, | ||
uint8 day, | ||
uint8 hour, | ||
uint8 minute, | ||
uint8 second | ||
) internal pure returns (uint256 timestamp) { | ||
uint16 i; | ||
|
||
// Year | ||
// TODO: Rewrite to O(1) time implementation. | ||
for (i = ORIGIN_YEAR; i < year; i++) { | ||
if (isLeapYear(i)) { | ||
timestamp += 366 days; | ||
} else { | ||
timestamp += 365 days; | ||
} | ||
} | ||
|
||
// Month | ||
// TODO: Use constants for monthDayCounts (hex-encoded string?), rewrite to O(1) time implementation. | ||
uint32[12] memory monthDayCounts; | ||
monthDayCounts[0] = 31; | ||
if (isLeapYear(year)) { | ||
monthDayCounts[1] = 29; | ||
} else { | ||
monthDayCounts[1] = 28; | ||
} | ||
monthDayCounts[2] = 31; | ||
monthDayCounts[3] = 30; | ||
monthDayCounts[4] = 31; | ||
monthDayCounts[5] = 30; | ||
monthDayCounts[6] = 31; | ||
monthDayCounts[7] = 31; | ||
monthDayCounts[8] = 30; | ||
monthDayCounts[9] = 31; | ||
monthDayCounts[10] = 30; | ||
monthDayCounts[11] = 31; | ||
|
||
for (i = 1; i < month; i++) { | ||
timestamp += monthDayCounts[i - 1] * 1 days; | ||
} | ||
|
||
// Day | ||
timestamp += uint32(day - 1) * 1 days; | ||
|
||
// Hour | ||
timestamp += uint32(hour) * 1 hours; | ||
|
||
// Minute | ||
timestamp += uint16(minute) * 1 minutes; | ||
|
||
// Second | ||
timestamp += second; | ||
|
||
return timestamp; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.0; | ||
|
||
import {DateTime} from "./DateTime.sol"; | ||
|
||
struct ParsedSiweMessage { | ||
bytes schemeDomain; | ||
address addr; | ||
bytes statement; | ||
bytes uri; | ||
bytes version; | ||
uint256 chainId; | ||
bytes nonce; | ||
bytes issuedAt; | ||
bytes expirationTime; | ||
bytes notBefore; | ||
bytes requestId; | ||
bytes[] resources; | ||
} | ||
|
||
/** | ||
* @title On-chain parser for EIP-4361 SIWE message | ||
* @notice Call parseSiweMsg() and provide the EIP-4361 SIWE message. The parser | ||
* will generate the ParsedSiweMessage struct which you can then use to | ||
* extract the authentication information in your on-chain contract. | ||
*/ | ||
library SiweParser { | ||
/// Invalid length of the hex-encoded address | ||
error InvalidAddressLength(); | ||
/// Invalid length of the nonce | ||
error InvalidNonce(); | ||
|
||
/** | ||
* @notice Convert string containing hex address without 0x prefix to solidity address object. | ||
*/ | ||
function _hexStringToAddress(bytes memory s) | ||
internal | ||
pure | ||
returns (address) | ||
{ | ||
if (s.length != 40) { | ||
revert InvalidAddressLength(); | ||
} | ||
|
||
bytes memory r = new bytes(s.length / 2); | ||
for (uint256 i = 0; i < s.length / 2; ++i) { | ||
r[i] = bytes1( | ||
_fromHexChar(uint8(s[2 * i])) * | ||
16 + | ||
_fromHexChar(uint8(s[2 * i + 1])) | ||
); | ||
} | ||
return address(bytes20(r)); | ||
} | ||
|
||
function _fromHexChar(uint8 c) internal pure returns (uint8) { | ||
if (bytes1(c) >= bytes1("0") && bytes1(c) <= bytes1("9")) { | ||
return c - uint8(bytes1("0")); | ||
} | ||
if (bytes1(c) >= bytes1("a") && bytes1(c) <= bytes1("f")) { | ||
return 10 + c - uint8(bytes1("a")); | ||
} | ||
if (bytes1(c) >= bytes1("A") && bytes1(c) <= bytes1("F")) { | ||
return 10 + c - uint8(bytes1("A")); | ||
} | ||
return 0; | ||
} | ||
|
||
/** | ||
* @notice Substring. | ||
*/ | ||
function _substr( | ||
bytes memory str, | ||
uint256 startIndex, | ||
uint256 endIndex | ||
) internal pure returns (bytes memory) { | ||
bytes memory result = new bytes(endIndex - startIndex); | ||
for (uint256 i = startIndex; i < endIndex && i < str.length; i++) { | ||
result[i - startIndex] = str[i]; | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* @notice String to Uint using decimal format. No error handling. | ||
*/ | ||
function _parseUint(bytes memory b) internal pure returns (uint256) { | ||
uint256 result = 0; | ||
for (uint256 i = 0; i < b.length; i++) { | ||
result = result * 10 + (uint256(uint8(b[i])) - 0x30); | ||
} | ||
return (result); | ||
} | ||
|
||
/** | ||
* @notice Parse "NAME: VALUE" in str starting at index i and ending at \n or end of bytes. | ||
* @return VALUE and new i, if NAME matched; otherwise empty value and old i. | ||
*/ | ||
function _parseField( | ||
bytes calldata str, | ||
string memory name, | ||
uint256 i | ||
) internal pure returns (bytes memory, uint256) { | ||
uint256 j = i; | ||
for (; j < str.length; j++) { | ||
if (str[j] == ":") { | ||
// Delimiter found, check the name. | ||
if (keccak256(_substr(str, i, j)) != keccak256(bytes(name))) { | ||
return ("", i); | ||
} | ||
|
||
// Skip : | ||
j++; | ||
if (j < str.length && str[j] == " ") { | ||
// Skip blank | ||
j++; | ||
} | ||
|
||
i = j; | ||
break; | ||
} | ||
} | ||
|
||
for (; j < str.length; j++) { | ||
if (str[j] == 0x0a) { | ||
return (_substr(str, i, j), j + 1); | ||
} | ||
} | ||
return (_substr(str, i, j), j); | ||
} | ||
|
||
/** | ||
* @notice Parse bullets, one per line in str starting at i. | ||
* @return Array of parsed values and a new i. | ||
*/ | ||
function _parseArray(bytes calldata str, uint256 i) | ||
internal | ||
pure | ||
returns (bytes[] memory, uint256) | ||
{ | ||
// First count the number of resources. | ||
uint256 j = i; | ||
uint256 count = 0; | ||
for (; j < str.length - 1; j++) { | ||
if (str[j] == "-" && str[j + 1] == " ") { | ||
j += 2; | ||
count++; | ||
} else { | ||
break; | ||
} | ||
while (j < str.length && str[j] != 0x0a) { | ||
j++; | ||
} | ||
} | ||
|
||
// Then build an array. | ||
bytes[] memory values = new bytes[](count); | ||
j = i; | ||
for (uint256 c = 0; j < str.length - 1 && c != count; j++) { | ||
if (str[j] == "-" && str[j + 1] == " ") { | ||
i = j + 2; | ||
} | ||
while (j < str.length && str[j] != 0x0a) { | ||
j++; | ||
} | ||
values[c] = _substr(str, i, j); | ||
c++; | ||
if (j == str.length) { | ||
j--; // Subtract 1 because of the outer loop. | ||
} | ||
} | ||
return (values, j); | ||
} | ||
|
||
/** | ||
* @notice Parse SIWE message. | ||
* @return ParsedSiweMessage struct with populated fields from the message. | ||
*/ | ||
function parseSiweMsg(bytes calldata siweMsg) | ||
internal | ||
pure | ||
returns (ParsedSiweMessage memory) | ||
{ | ||
ParsedSiweMessage memory p; | ||
uint256 i = 0; | ||
|
||
// dApp Domain. | ||
for (; i < siweMsg.length; i++) { | ||
if (siweMsg[i] == " ") { | ||
p.schemeDomain = _substr(siweMsg, 0, i); | ||
break; | ||
} | ||
} | ||
|
||
i += 50; // " wants you to sign in with your Ethereum account:\n" | ||
|
||
// Address. | ||
// TODO: Verify the mixed-case checksum. | ||
p.addr = _hexStringToAddress(_substr(siweMsg, i += 2, i += 40)); | ||
i += 2; // End of address new line + New line. | ||
|
||
// (Optional) statement. | ||
if (i < siweMsg.length && siweMsg[i] != "\n") { | ||
for (uint256 j = i; j < siweMsg.length; j++) { | ||
if (siweMsg[j] == 0x0a) { | ||
p.statement = _substr(siweMsg, i, j); | ||
i = j + 1; // End of statement new line. | ||
break; | ||
} | ||
} | ||
} | ||
|
||
i++; // New line. | ||
|
||
(p.uri, i) = _parseField(siweMsg, "URI", i); | ||
(p.version, i) = _parseField(siweMsg, "Version", i); | ||
bytes memory chainId; | ||
(chainId, i) = _parseField(siweMsg, "Chain ID", i); | ||
p.chainId = _parseUint(chainId); | ||
(p.nonce, i) = _parseField(siweMsg, "Nonce", i); | ||
if (p.nonce.length < 8) { | ||
revert InvalidNonce(); | ||
} | ||
(p.issuedAt, i) = _parseField(siweMsg, "Issued At", i); | ||
(p.expirationTime, i) = _parseField(siweMsg, "Expiration Time", i); | ||
(p.notBefore, i) = _parseField(siweMsg, "Not Before", i); | ||
(p.requestId, i) = _parseField(siweMsg, "Request ID", i); | ||
|
||
// Parse resources, if they exist. | ||
uint256 newI; | ||
(, newI) = _parseField(siweMsg, "Resources", i); | ||
if (newI != i) { | ||
(p.resources, i) = _parseArray(siweMsg, newI); | ||
} | ||
|
||
return p; | ||
} | ||
|
||
/** | ||
* @notice Parse RFC 3339 (ISO 8601) string to timestamp. | ||
*/ | ||
function timestampFromIso(bytes memory str) | ||
internal | ||
pure | ||
returns (uint256) | ||
{ | ||
return | ||
DateTime.toTimestamp( | ||
uint16(_parseUint(_substr(str, 0, 4))), | ||
uint8(_parseUint(_substr(str, 5, 7))), | ||
uint8(_parseUint(_substr(str, 8, 10))), | ||
uint8(_parseUint(_substr(str, 11, 13))), | ||
uint8(_parseUint(_substr(str, 14, 16))), | ||
uint8(_parseUint(_substr(str, 17, 19))) | ||
); | ||
} | ||
} |
Oops, something went wrong.