diff --git a/packages/guard/src/signer.ts b/packages/guard/src/signer.ts index e9611332b..13e96cbb0 100644 --- a/packages/guard/src/signer.ts +++ b/packages/guard/src/signer.ts @@ -1,7 +1,9 @@ -import { signers, Status } from '@0xsequence/signhub' -import { BytesLike, ethers } from 'ethers' -import { Guard } from './guard.gen' +import { Account } from '@0xsequence/account' import { commons, universal } from '@0xsequence/core' +import { signers, Status } from '@0xsequence/signhub' +import { TypedData } from '@0xsequence/utils' +import { BytesLike, ethers, TypedDataDomain } from 'ethers' +import { AuthMethodsReturn, Guard } from './guard.gen' const fetch = typeof global === 'object' ? global.fetch : window.fetch @@ -62,6 +64,63 @@ export class GuardSigner implements signers.SapientSigner { this.evaluateRequest(id, status.message, status, metadata) } + async getAuthMethods(token: GetAuthMethodsToken): Promise { + let response: AuthMethodsReturn + + if ('jwt' in token) { + response = await this.guard.authMethods({}, { Authorization: `BEARER ${token.jwt}` }) + } else { + const signedToken = await signGetAuthMethodsToken(token) + response = await this.guard.authMethods({ request: { ...signedToken, timestamp: signedToken.timestamp.getTime() } }) + } + + return response.methods.map(parseAuthMethod) + } + + async setPin(pin: string | undefined, token: UpdateAuthMethodToken): Promise { + const signedToken = await signUpdateAuthMethodToken(token) + + if (pin === undefined) { + await this.guard.resetPIN( + { timestamp: signedToken.timestamp.getTime(), signature: signedToken.signature }, + { Authorization: `BEARER ${token.jwt}` } + ) + } else { + await this.guard.setPIN( + { pin, timestamp: signedToken.timestamp.getTime(), signature: signedToken.signature }, + { Authorization: `BEARER ${token.jwt}` } + ) + } + } + + resetPin(token: UpdateAuthMethodToken): Promise { + return this.setPin(undefined, token) + } + + async createTotp(token: UpdateAuthMethodToken): Promise { + const signedToken = await signUpdateAuthMethodToken(token) + + const { uri } = await this.guard.createTOTP( + { timestamp: signedToken.timestamp.getTime(), signature: signedToken.signature }, + { Authorization: `BEARER ${token.jwt}` } + ) + + return new URL(uri) + } + + async commitTotp(token: string, jwt: string): Promise { + await this.guard.commitTOTP({ token }, { Authorization: `BEARER ${jwt}` }) + } + + async resetTotp(token: UpdateAuthMethodToken): Promise { + const signedToken = await signUpdateAuthMethodToken(token) + + await this.guard.resetTOTP( + { timestamp: signedToken.timestamp.getTime(), signature: signedToken.signature }, + { Authorization: `BEARER ${token.jwt}` } + ) + } + private packMsgAndSig(address: string, msg: BytesLike, sig: BytesLike, chainId: ethers.BigNumberish): string { return ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes', 'bytes'], [address, chainId, msg, sig]) } @@ -115,3 +174,109 @@ export class GuardSigner implements signers.SapientSigner { return this.appendSuffix ? [3] : [] } } + +export enum AuthMethod { + Pin = 'PIN', + Totp = 'TOTP' +} + +function parseAuthMethod(method: string): AuthMethod { + switch (method) { + case AuthMethod.Pin: + case AuthMethod.Totp: + return method + default: + throw new Error(`unknown auth method '${method}'`) + } +} + +export type GetAuthMethodsToken = + | { jwt: string } + | { + wallet: string + timestamp: Date + signer: string + signature: string + } + | { + wallet: string + signer: ethers.Wallet + } + +function isSignedGetAuthMethodsToken( + token: GetAuthMethodsToken +): token is { wallet: string; timestamp: Date; signer: string; signature: string } { + return 'signer' in token && typeof token.signer === 'string' +} + +async function signGetAuthMethodsToken( + token: Exclude +): Promise<{ wallet: string; timestamp: Date; signer: string; signature: string }> { + if (isSignedGetAuthMethodsToken(token)) { + return token + } else { + const timestamp = new Date() + const typedData = getTypedDataForGetAuthMethods(token.wallet, timestamp) + + const signature = await token.signer._signTypedData(typedData.domain, typedData.types, typedData.message) + + return { wallet: token.wallet, timestamp, signer: token.signer.address, signature } + } +} + +export type UpdateAuthMethodToken = { jwt: string } & ({ timestamp: Date; signature: string } | { wallet: Account }) + +async function signUpdateAuthMethodToken( + token: UpdateAuthMethodToken +): Promise<{ jwt: string; timestamp: Date; signature: string }> { + if ('wallet' in token) { + const timestamp = new Date() + const typedData = getTypedDataForUpdateAuthMethod(timestamp) + + const signature = await token.wallet.signTypedData( + typedData.domain, + typedData.types, + typedData.message, + typedData.domain.chainId ?? 1, + 'eip6492' + ) + + return { jwt: token.jwt, timestamp, signature } + } else { + return token + } +} + +export function getTypedDataForGetAuthMethods(wallet: string, timestamp: Date): TypedData { + return { + domain, + types: { + AuthMethods: [ + { name: 'wallet', type: 'address' }, + { name: 'timestamp', type: 'string' } + ] + }, + message: { + wallet: ethers.utils.getAddress(wallet), + timestamp: timestamp.toUTCString() + } + } +} + +export function getTypedDataForUpdateAuthMethod(timestamp: Date): TypedData { + return { + domain, + types: { + AuthUpdate: [{ name: 'timestamp', type: 'string' }] + }, + message: { + timestamp: timestamp.toUTCString() + } + } +} + +const domain: TypedDataDomain = { + name: 'Sequence Guard', + version: '1', + chainId: 1 +}