From 296b961ad687c5fc722c432b3688cea265fe4611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Fri, 12 Jul 2024 17:46:18 +0200 Subject: [PATCH] backend: Use new sapphire-contracts SiweAuth --- .github/workflows/ci-test.yaml | 2 +- backend/contracts/DateTime.sol | 218 - backend/contracts/MessageBox.sol | 6 +- backend/contracts/SiweAuth.sol | 257 - backend/hardhat.config.ts | 18 +- backend/package.json | 2 +- backend/test/MessageBox.ts | 49 +- frontend/package.json | 2 + frontend/src/stores/ethereum.ts | 2 +- frontend/src/views/HomeView.vue | 23 +- pnpm-lock.yaml | 8266 +++++++++++++++++------------- 11 files changed, 4707 insertions(+), 4138 deletions(-) delete mode 100644 backend/contracts/DateTime.sol delete mode 100644 backend/contracts/SiweAuth.sol diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index ad378c3..c4ec201 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -24,7 +24,7 @@ jobs: name: Install pnpm id: pnpm-install with: - version: 8 + version: 9 run_install: false - name: Get pnpm store directory diff --git a/backend/contracts/DateTime.sol b/backend/contracts/DateTime.sol deleted file mode 100644 index 55aab86..0000000 --- a/backend/contracts/DateTime.sol +++ /dev/null @@ -1,218 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// https://github.com/pipermerriam/ethereum-datetime/blob/master/contracts/DateTime.sol -contract DateTime { - /* - * Date and Time utilities for ethereum contracts - * - */ - struct _DateTime { - uint16 year; - uint8 month; - uint8 day; - uint8 hour; - uint8 minute; - uint8 second; - uint8 weekday; - } - - uint constant DAY_IN_SECONDS = 86400; - uint constant YEAR_IN_SECONDS = 31536000; - uint constant LEAP_YEAR_IN_SECONDS = 31622400; - - uint constant HOUR_IN_SECONDS = 3600; - uint constant MINUTE_IN_SECONDS = 60; - - uint16 constant ORIGIN_YEAR = 1970; - - function isLeapYear(uint16 year) public pure returns (bool) { - if (year % 4 != 0) { - return false; - } - if (year % 100 != 0) { - return true; - } - if (year % 400 != 0) { - return false; - } - return true; - } - - function leapYearsBefore(uint year) public pure returns (uint) { - year -= 1; - return year / 4 - year / 100 + year / 400; - } - - function getDaysInMonth(uint8 month, uint16 year) public pure returns (uint8) { - if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) { - return 31; - } - else if (month == 4 || month == 6 || month == 9 || month == 11) { - return 30; - } - else if (isLeapYear(year)) { - return 29; - } - else { - return 28; - } - } - - function parseTimestamp(uint timestamp) internal pure returns (_DateTime memory dt) { - uint secondsAccountedFor = 0; - uint buf; - uint8 i; - - // Year - dt.year = getYear(timestamp); - buf = leapYearsBefore(dt.year) - leapYearsBefore(ORIGIN_YEAR); - - secondsAccountedFor += LEAP_YEAR_IN_SECONDS * buf; - secondsAccountedFor += YEAR_IN_SECONDS * (dt.year - ORIGIN_YEAR - buf); - - // Month - uint secondsInMonth; - for (i = 1; i <= 12; i++) { - secondsInMonth = DAY_IN_SECONDS * getDaysInMonth(i, dt.year); - if (secondsInMonth + secondsAccountedFor > timestamp) { - dt.month = i; - break; - } - secondsAccountedFor += secondsInMonth; - } - - // Day - for (i = 1; i <= getDaysInMonth(dt.month, dt.year); i++) { - if (DAY_IN_SECONDS + secondsAccountedFor > timestamp) { - dt.day = i; - break; - } - secondsAccountedFor += DAY_IN_SECONDS; - } - - // Hour - dt.hour = getHour(timestamp); - - // Minute - dt.minute = getMinute(timestamp); - - // Second - dt.second = getSecond(timestamp); - - // Day of week. - dt.weekday = getWeekday(timestamp); - } - - function getYear(uint timestamp) public pure returns (uint16) { - uint secondsAccountedFor = 0; - uint16 year; - uint numLeapYears; - - // Year - year = uint16(ORIGIN_YEAR + timestamp / YEAR_IN_SECONDS); - numLeapYears = leapYearsBefore(year) - leapYearsBefore(ORIGIN_YEAR); - - secondsAccountedFor += LEAP_YEAR_IN_SECONDS * numLeapYears; - secondsAccountedFor += YEAR_IN_SECONDS * (year - ORIGIN_YEAR - numLeapYears); - - while (secondsAccountedFor > timestamp) { - if (isLeapYear(uint16(year - 1))) { - secondsAccountedFor -= LEAP_YEAR_IN_SECONDS; - } - else { - secondsAccountedFor -= YEAR_IN_SECONDS; - } - year -= 1; - } - return year; - } - - function getMonth(uint timestamp) public pure returns (uint8) { - return parseTimestamp(timestamp).month; - } - - function getDay(uint timestamp) public pure returns (uint8) { - return parseTimestamp(timestamp).day; - } - - function getHour(uint timestamp) public pure returns (uint8) { - return uint8((timestamp / 60 / 60) % 24); - } - - function getMinute(uint timestamp) public pure returns (uint8) { - return uint8((timestamp / 60) % 60); - } - - function getSecond(uint timestamp) public pure returns (uint8) { - return uint8(timestamp % 60); - } - - function getWeekday(uint timestamp) public pure returns (uint8) { - return uint8((timestamp / DAY_IN_SECONDS + 4) % 7); - } - - function toTimestamp(uint16 year, uint8 month, uint8 day) public pure returns (uint timestamp) { - return toTimestamp(year, month, day, 0, 0, 0); - } - - function toTimestamp(uint16 year, uint8 month, uint8 day, uint8 hour) public pure returns (uint timestamp) { - return toTimestamp(year, month, day, hour, 0, 0); - } - - function toTimestamp(uint16 year, uint8 month, uint8 day, uint8 hour, uint8 minute) public pure returns (uint timestamp) { - return toTimestamp(year, month, day, hour, minute, 0); - } - - function toTimestamp(uint16 year, uint8 month, uint8 day, uint8 hour, uint8 minute, uint8 second) public pure returns (uint timestamp) { - uint16 i; - - // Year - for (i = ORIGIN_YEAR; i < year; i++) { - if (isLeapYear(i)) { - timestamp += LEAP_YEAR_IN_SECONDS; - } - else { - timestamp += YEAR_IN_SECONDS; - } - } - - // Month - uint8[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 += DAY_IN_SECONDS * monthDayCounts[i - 1]; - } - - // Day - timestamp += DAY_IN_SECONDS * (day - 1); - - // Hour - timestamp += HOUR_IN_SECONDS * (hour); - - // Minute - timestamp += MINUTE_IN_SECONDS * (minute); - - // Second - timestamp += second; - - return timestamp; - } -} diff --git a/backend/contracts/MessageBox.sol b/backend/contracts/MessageBox.sol index e045df3..d9ec9e8 100644 --- a/backend/contracts/MessageBox.sol +++ b/backend/contracts/MessageBox.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "./SiweAuth.sol"; +import "@oasisprotocol/sapphire-contracts/contracts/auth/SiweAuth.sol"; contract MessageBox is SiweAuth { string private _message; address public author; - modifier _authorOnly(bytes calldata bearer) { + modifier authorOnly(bytes calldata bearer) { if (authMsgSender(bearer) != author) { revert("not allowed"); } @@ -22,7 +22,7 @@ contract MessageBox is SiweAuth { author = msg.sender; } - function message(bytes calldata bearer) external view _authorOnly(bearer) returns (string memory) { + function message(bytes calldata bearer) external view authorOnly(bearer) returns (string memory) { return _message; } } diff --git a/backend/contracts/SiweAuth.sol b/backend/contracts/SiweAuth.sol deleted file mode 100644 index 567db3a..0000000 --- a/backend/contracts/SiweAuth.sol +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol"; -import "./DateTime.sol"; - -struct Sig { - uint8 v; - bytes32 r; - bytes32 s; -} - -struct Bearer { - string domain; - address userAddr; - uint256 validUntil; // in Unix timestamp. -} - -contract SiweAuth { - string _domain; - bytes32 _bearerEncKey; - address _authMsgSender; - DateTime _dateTime; - - uint constant DEFAULT_VALIDITY=24*3600; // in seconds. - - struct ParsedSiweMessage { - bytes schemeDomain; - address addr; - bytes statement; - bytes uri; - bytes version; - uint chainId; - bytes nonce; - bytes issuedAt; - bytes expirationTime; - bytes notBefore; - bytes requestId; - bytes[] resources; - } - - // Converts string containing hex address without 0x prefix to solidity address object. - function _hexStringToAddress(bytes memory s) private pure returns (address) { - require(s.length == 40, "Invalid address length"); - bytes memory r = new bytes(s.length/2); - for (uint i=0; i= 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; - } - - constructor(string memory in_domain) { - _bearerEncKey = bytes32(Sapphire.randomBytes(32, "")); - _domain = in_domain; - _dateTime = new DateTime(); - } - - // Substring. - function _substr(bytes memory str, uint startIndex, uint endIndex) private pure returns (bytes memory) { - bytes memory result = new bytes(endIndex-startIndex); - for(uint i = startIndex; i < endIndex && i _timestampFromIso(p.notBefore), "not before not reached yet"); - } - require(block.timestamp < _timestampFromIso(p.expirationTime), "expired"); - - if (p.expirationTime.length!=0) { - // Compute expected block number at expiration time. - b.validUntil = _timestampFromIso(p.expirationTime); - } else { - // Otherwise, just take the default validity. - b.validUntil = block.timestamp + DEFAULT_VALIDITY; - } - - bytes memory encB = Sapphire.encrypt(_bearerEncKey, 0, abi.encode(b), ""); - return encB; - } - - // Returns the domain associated with the dApp. - function domain() public view returns (string memory) { - return _domain; - } - - // Validates the bearer token and returns authenticated msg.sender. - function authMsgSender(bytes calldata bearer) internal view returns (address) { - bytes memory bearerEncoded = Sapphire.decrypt(_bearerEncKey, 0, bearer, ""); - Bearer memory b = abi.decode(bearerEncoded, (Bearer)); - require(keccak256(bytes(b.domain))==keccak256(bytes(_domain)), "invalid domain"); - require(b.validUntil>=block.timestamp, "expired"); - return b.userAddr; - } -} diff --git a/backend/hardhat.config.ts b/backend/hardhat.config.ts index dad3a98..8e83074 100644 --- a/backend/hardhat.config.ts +++ b/backend/hardhat.config.ts @@ -5,10 +5,11 @@ import '@nomicfoundation/hardhat-ethers'; import '@oasisprotocol/sapphire-hardhat'; import '@typechain/hardhat'; import canonicalize from 'canonicalize'; -import {JsonRpcProvider, toUtf8String} from "ethers"; +import {JsonRpcProvider} from "ethers"; import 'hardhat-watcher'; import { TASK_COMPILE } from 'hardhat/builtin-tasks/task-names'; import { HardhatUserConfig, task } from 'hardhat/config'; +import {SiweMessage} from 'siwe'; import 'solidity-coverage'; const TASK_EXPORT_ABIS = 'export-abis'; @@ -60,8 +61,19 @@ task('message') await hre.run('compile'); const messageBox = await hre.ethers.getContractAt('MessageBox', args.address); - const auth = hre.ethers.Signature.from(await (await hre.ethers.getSigners())[0].signMessage(toUtf8String(await messageBox.getSiweMsg()))); - const message = await messageBox.message(auth); + const domain = await messageBox.domain(); + + const acc = new hre.ethers.Wallet(accounts[0], hre.ethers.provider); + const siweMsg = new SiweMessage({ + domain, + address: await acc.getAddress(), + uri: domain.includes(':')?domain:`http://${domain}`, + version: "1", + chainId: Number((await hre.ethers.provider.getNetwork()).chainId) + }).toMessage(); + const sig = hre.ethers.Signature.from(await acc.signMessage(siweMsg)); + const bearer = await messageBox.login(siweMsg, sig); + const message = await messageBox.message(bearer); const author = await messageBox.author(); console.log(`The message is: ${message}, author: ${author}`); }); diff --git a/backend/package.json b/backend/package.json index 2a76e19..000a997 100644 --- a/backend/package.json +++ b/backend/package.json @@ -43,7 +43,7 @@ "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.7", "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@oasisprotocol/sapphire-contracts": "^0.2.7", + "@oasisprotocol/sapphire-contracts": "github:oasisprotocol/sapphire-paratime#path:contracts&matevz/sapphire-contracts-auth", "@oasisprotocol/sapphire-hardhat": "link:../../sapphire-paratime/integrations/hardhat", "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", diff --git a/backend/test/MessageBox.ts b/backend/test/MessageBox.ts index 0f6e531..2e10507 100644 --- a/backend/test/MessageBox.ts +++ b/backend/test/MessageBox.ts @@ -2,7 +2,6 @@ import { expect } from "chai"; import { config, ethers } from "hardhat"; import {SiweMessage} from "siwe"; import "@nomicfoundation/hardhat-chai-matchers"; -import {MessagePrefix, hashMessage, concat, toUtf8Bytes, toUtf8String} from "ethers"; describe("MessageBox", function () { async function deployMessageBox() { @@ -13,17 +12,16 @@ describe("MessageBox", function () { } async function siweMsg(): Promise { - return new SiweMessage({ - domain: "localhost", - address: await (await ethers.provider.getSigner(0)).getAddress(), - statement: "I accept the ExampleOrg Terms of Service: http://localhost/tos", - uri: "http://localhost:5173", - version: "1", - chainId: config.networks.hardhat.chainId, - }).toMessage(); + return ; } - it("Should set message", async function () { + it("Should set message authenticated", async function () { + // Skip this test on non-sapphire chains. + // On-chain encryption and/or signing required for SIWE. + if ((await ethers.provider.getNetwork()).chainId == 1337n) { + this.skip(); + } + const {messageBox} = await deployMessageBox(); await messageBox.setMessage("hello world"); @@ -31,18 +29,33 @@ describe("MessageBox", function () { // Check, if author is correctly set. expect(await messageBox.author()).to.equal(await (await ethers.provider.getSigner(0)).getAddress()); - // Author should read a message. + // Author should be able to read a message. const accounts = config.networks.hardhat.accounts; - const acc = ethers.HDNodeWallet.fromMnemonic(ethers.Mnemonic.fromPhrase(accounts.mnemonic), accounts.path+'/0'); - const sig = ethers.Signature.from(await acc.signMessage(siweMsg())); - const bearer = messageBox.login(await siweMsg(), sig); - expect(await messageBox.message(bearer)).to.equal("hello world"); + const account = ethers.HDNodeWallet.fromMnemonic(ethers.Mnemonic.fromPhrase(accounts.mnemonic), accounts.path+'/0'); + const siweMsg = new SiweMessage({ + domain: "localhost", + address: await account.getAddress(), + statement: "I accept the ExampleOrg Terms of Service: http://localhost/tos", + uri: "http://localhost:5173", + version: "1", + chainId: Number((await ethers.provider.getNetwork()).chainId), + }).toMessage(); + const sig = ethers.Signature.from(await account.signMessage(siweMsg)); + const bearer = await messageBox.login(siweMsg, sig); + await expect(await messageBox.message(bearer)).to.be.equal("hello world"); // Anyone else trying to read the message should fail. const acc2 = ethers.HDNodeWallet.fromMnemonic(ethers.Mnemonic.fromPhrase(accounts.mnemonic), accounts.path+'/1'); - const sig2 = ethers.Signature.from(await acc2.signMessage(siweMsg())) - const bearer2 = messageBox.login(await siweMsg(), sig2); + const siweMsg2 = new SiweMessage({ + domain: "localhost", + address: await acc2.getAddress(), + statement: "I accept the ExampleOrg Terms of Service: http://localhost/tos", + uri: "http://localhost:5173", + version: "1", + chainId: Number((await ethers.provider.getNetwork()).chainId), + }).toMessage() + const sig2 = ethers.Signature.from(await acc2.signMessage(siweMsg2)) + const bearer2 = await messageBox.login(siweMsg2, sig2); await expect(messageBox.message(bearer2)).to.be.reverted; - }); }); diff --git a/frontend/package.json b/frontend/package.json index fb9f648..08b4a7e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,9 +20,11 @@ "@metamask/detect-provider": "^2.0.0", "@metamask/jazzicon": "^2.0.0", "@oasisprotocol/demo-starter-backend": "workspace:^", + "@oasisprotocol/sapphire-contracts": "github:oasisprotocol/sapphire-paratime#path:contracts&matevz/sapphire-contracts-auth", "@oasisprotocol/sapphire-paratime": "link:../../sapphire-paratime/clients/js", "ethers": "^6.10.0", "pinia": "^2.0.28", + "siwe": "^2.3.2", "vue": "^3.2.45", "vue-content-loader": "^2.0.1", "vue-router": "^4.1.6" diff --git a/frontend/src/stores/ethereum.ts b/frontend/src/stores/ethereum.ts index 94d8417..91152ca 100644 --- a/frontend/src/stores/ethereum.ts +++ b/frontend/src/stores/ethereum.ts @@ -103,7 +103,7 @@ export const useEthereumStore = defineStore('ethereum', () => { const isSapphire = sapphire.NETWORKS[chainId]; provider.value = isSapphire - ? markRaw(sapphire.wrapEthereumProvider(browserProvider.provider)) + ? markRaw(sapphire.wrapEthereumProvider(browserProvider.provider as unknown as sapphire.EIP2696_EthereumProvider)) as unknown as Provider : browserProvider.provider; unwrappedProvider.value = browserProvider.provider; unwrappedSigner.value = await browserProvider.getSigner(addr); diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index b4eeae4..3bde07a 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,23 +1,23 @@