diff --git a/examples/taco/nodejs/src/index.ts b/examples/taco/nodejs/src/index.ts index ef4550ab5..a2c127269 100644 --- a/examples/taco/nodejs/src/index.ts +++ b/examples/taco/nodejs/src/index.ts @@ -13,10 +13,11 @@ import { } from '@nucypher/taco'; import { EIP4361AuthProvider, + SelfDelegateProvider, USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco-auth'; import * as dotenv from 'dotenv'; -import { ethers } from 'ethers'; +import { Wallet, ethers } from 'ethers'; dotenv.config(); @@ -73,13 +74,18 @@ const encryptToBytes = async (messageString: string) => { 'Condition requires authentication', ); + const ephemeralPrivateKey = Wallet.createRandom().privateKey; + const selfDelegateProvider = new SelfDelegateProvider(encryptorSigner); + + const appSideSigner = await selfDelegateProvider.createSelfDelegatedAppSideSigner(ephemeralPrivateKey); + const messageKit = await encrypt( provider, domain, message, hasPositiveBalance, ritualId, - encryptorSigner, + appSideSigner, ); return messageKit.toBytes(); diff --git a/packages/taco-auth/src/auth-sig.ts b/packages/taco-auth/src/auth-sig.ts index 03727842c..064d28686 100644 --- a/packages/taco-auth/src/auth-sig.ts +++ b/packages/taco-auth/src/auth-sig.ts @@ -5,12 +5,17 @@ import { EIP4361_AUTH_METHOD, EIP4361TypedDataSchema, } from './providers/eip4361/common'; +import { + ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD, + SelfDelegateTypedDataSchema, +} from './providers/encryptor/self-delegate'; +// TODO: Create two different schemas, rather than one with different field schemas export const authSignatureSchema = z.object({ signature: z.string(), address: EthAddressSchema, - scheme: z.enum([EIP4361_AUTH_METHOD]), - typedData: EIP4361TypedDataSchema, + scheme: z.enum([EIP4361_AUTH_METHOD, ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD]), + typedData: z.union([EIP4361TypedDataSchema, SelfDelegateTypedDataSchema]), }); export type AuthSignature = z.infer; diff --git a/packages/taco-auth/src/providers/encryptor/self-delegate.ts b/packages/taco-auth/src/providers/encryptor/self-delegate.ts new file mode 100644 index 000000000..e2a9f6146 --- /dev/null +++ b/packages/taco-auth/src/providers/encryptor/self-delegate.ts @@ -0,0 +1,80 @@ +import { ethers, Wallet } from 'ethers'; +import { z } from 'zod'; + +// import { Bytes } from "@ethersproject/bytes"; + +import { AuthSignature } from '../../auth-sig'; +import { LocalStorage } from '../../storage'; + +export const ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD = 'EncryptorSelfDelegate' + +export const SelfDelegateTypedDataSchema = z.string(); + + +// TODO: Create generic EncryptorSigner class/interface, which can be +// instantiated with ethers' Signers and Wallets, but also with our custom +// classes +export class DelegatedSigner extends Wallet { // TODO: extend from generic Signer + + authSignature?: AuthSignature; + + async authenticate(selfDelegateProvider: SelfDelegateProvider){ + const appSideSignerAddress = await this.getAddress(); + this.authSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + } + + override async signMessage(message: any): Promise { // TODO: Restrict input type to Bytes | string + if (typeof this.authSignature === 'undefined'){ + throw new Error('Encryptor must authenticate app signer first'); + } + const appSignature = await super.signMessage(message); + return appSignature.concat(this.authSignature.signature) + } +} + + +export class SelfDelegateProvider { + private readonly storage: LocalStorage; + + constructor(private readonly signer: ethers.Signer) { + this.storage = new LocalStorage(); + } + + public async createSelfDelegatedAppSideSigner( + ephemeralPrivateKey: any // TODO: Find a stricter type + ): Promise { + const appSideSigner = new DelegatedSigner(ephemeralPrivateKey); + await appSideSigner.authenticate(this); + return appSideSigner; + } + + public async getOrCreateAuthSignature( + ephemeralPublicKeyOrAddress: string + ): Promise { + const address = await this.signer.getAddress(); + const storageKey = `eth-${ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD}-${address}-${ephemeralPublicKeyOrAddress}`; + + // If we have a signature in localStorage, return it + const maybeSignature = this.storage.getAuthSignature(storageKey); + if (maybeSignature) { + // TODO: Consider design that includes freshness validation (see SIWE) + return maybeSignature; + } + + // If at this point we didn't return, we need to create a new message + const authMessage = await this.createAuthMessage(ephemeralPublicKeyOrAddress); + this.storage.setAuthSignature(storageKey, authMessage); + return authMessage; + } + + private async createAuthMessage( + ephemeralPublicKeyOrAddress: string + ): Promise { + // TODO: Consider adding domain, uri, version, chainId (see SIWE signature) + const address = await this.signer.getAddress(); + const scheme = ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD; + const message = ephemeralPublicKeyOrAddress; + const signature = await this.signer.signMessage(message); + return { signature, address, scheme, typedData: message }; + } +} diff --git a/packages/taco-auth/src/providers/index.ts b/packages/taco-auth/src/providers/index.ts index 82912650d..2e30944f3 100644 --- a/packages/taco-auth/src/providers/index.ts +++ b/packages/taco-auth/src/providers/index.ts @@ -1,2 +1,3 @@ export * from './eip4361/eip4361'; export * from './eip4361/external-eip4361'; +export * from './encryptor/self-delegate'; diff --git a/packages/taco-auth/test/auth-provider.test.ts b/packages/taco-auth/test/auth-provider.test.ts index 611cbb712..2329f3731 100644 --- a/packages/taco-auth/test/auth-provider.test.ts +++ b/packages/taco-auth/test/auth-provider.test.ts @@ -1,4 +1,5 @@ import { + aliceSecretKeyBytes, bobSecretKeyBytes, fakeProvider, TEST_SIWE_PARAMS, @@ -8,11 +9,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { EIP4361AuthProvider, + SelfDelegateProvider, SingleSignOnEIP4361AuthProvider, } from '../src/providers'; import { EIP4361TypedDataSchema } from '../src/providers/eip4361/common'; -describe('auth provider', () => { +describe('siwe auth provider', () => { const provider = fakeProvider(bobSecretKeyBytes); const signer = provider.getSigner(); const eip4361Provider = new EIP4361AuthProvider( @@ -55,7 +57,7 @@ describe('auth provider', () => { }); }); -describe('auth provider caching', () => { +describe('siwe auth provider caching', () => { beforeEach(() => { // tell vitest we use mocked time vi.useFakeTimers(); @@ -103,7 +105,7 @@ describe('auth provider caching', () => { }); }); -describe('single sign-on auth provider', async () => { +describe('single sign-on siwe auth provider', async () => { const provider = fakeProvider(bobSecretKeyBytes); const signer = provider.getSigner(); @@ -133,3 +135,61 @@ describe('single sign-on auth provider', async () => { expect(typedSignature.scheme).toEqual('EIP4361'); }); }); + +describe('encryptor self-delegate provider authorization', () => { + const provider = fakeProvider(bobSecretKeyBytes); + const signer = provider.getSigner(); + const selfDelegateProvider = new SelfDelegateProvider(signer); + + it('creates a new self-delegated app signer', async () => { + const appSideSignerAddress = await applicationSideSigner.getAddress(); + const [newSigner, newAuthSignature] = await selfDelegateProvider.createSelfDelegatedAppSideSigner(aliceSecretKeyBytes); + expect(await newSigner.getAddress()).toEqual(appSideSignerAddress); + expect(newAuthSignature.typedData).toEqual(appSideSignerAddress); + }); + + const applicationSideProvider = fakeProvider(aliceSecretKeyBytes); + const applicationSideSigner = applicationSideProvider.getSigner(); + + it('creates a new auth signature', async () => { + const appSideSignerAddress = await applicationSideSigner.getAddress(); + const typedSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + expect(typedSignature.signature).toBeDefined(); + expect(typedSignature.address).toEqual(await signer.getAddress()); + expect(typedSignature.scheme).toEqual('EncryptorSelfDelegate'); + expect(typedSignature.typedData).toEqual(appSideSignerAddress); + + }); + +}); + +describe('encryptor self-delegate provider caching', () => { + const provider = fakeProvider(bobSecretKeyBytes); + const signer = provider.getSigner(); + const selfDelegateProvider = new SelfDelegateProvider(signer); + + const applicationSideProvider = fakeProvider(aliceSecretKeyBytes); + const applicationSideSigner = applicationSideProvider.getSigner(); + + it('caches signature', async () => { + const appSideSignerAddress = await applicationSideSigner.getAddress(); + + const createSignatureSpy = vi.spyOn( + selfDelegateProvider, + // @ts-expect-error -- spying on private function + 'createAuthMessage', + ); + + expect(createSignatureSpy).toHaveBeenCalledTimes(0); + + const typedSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + expect(createSignatureSpy).toHaveBeenCalledTimes(1); + + const typedSignatureSecondCall = + await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + // auth signature is cached, so spy is not called a 2nd time + expect(createSignatureSpy).toHaveBeenCalledTimes(1); + expect(typedSignatureSecondCall).toEqual(typedSignature); + }); + +}); diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index da1fc9ce9..15314b77b 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -49,18 +49,6 @@ export const encrypt = async ( ritualId: number, authSigner: ethers.Signer, ): Promise => { - // TODO(#264): Enable ritual initialization - // if (ritualId === undefined) { - // ritualId = await DkgClient.initializeRitual( - // provider, - // this.cohort.ursulaAddresses, - // true - // ); - // } - // if (ritualId === undefined) { - // // Given that we just initialized the ritual, this should never happen - // throw new Error('Ritual ID is undefined'); - // } const dkgRitual = await DkgClient.getActiveRitual(provider, domain, ritualId); return await encryptWithPublicKey(