From 2d6d92eea06ccdcc87496cb9ff9b0785b421c52a Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Mon, 14 Aug 2023 11:16:22 +0200 Subject: [PATCH] draft taco api --- src/characters/cbd-recipient.ts | 17 ++-- src/conditions/condition-expr.ts | 5 ++ src/dkg.ts | 13 +++ src/sdk/strategy/cbd-strategy.ts | 6 +- src/taco.ts | 61 ++++++++++++++ test/unit/cbd-strategy.test.ts | 2 +- test/unit/conditions/condition-expr.test.ts | 33 +++++--- test/unit/taco.test.ts | 88 +++++++++++++++++++++ test/utils.ts | 5 +- 9 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 src/taco.ts create mode 100644 test/unit/taco.test.ts diff --git a/src/characters/cbd-recipient.ts b/src/characters/cbd-recipient.ts index c37629fdc..cb5a2696a 100644 --- a/src/characters/cbd-recipient.ts +++ b/src/characters/cbd-recipient.ts @@ -15,7 +15,6 @@ import { ethers } from 'ethers'; import { DkgCoordinatorAgent, DkgParticipant } from '../agents/coordinator'; import { ConditionExpression } from '../conditions'; -import { DkgRitual } from '../dkg'; import { PorterClient } from '../porter'; import { fromJSON, toJSON } from '../utils'; @@ -34,22 +33,22 @@ export class ThresholdDecrypter { private readonly threshold: number ) {} - public static create(porterUri: string, dkgRitual: DkgRitual) { + public static create(porterUri: string, ritualId: number, threshold: number) { return new ThresholdDecrypter( new PorterClient(porterUri), - dkgRitual.id, - dkgRitual.dkgParams.threshold + ritualId, + threshold ); } // Retrieve and decrypt ciphertext using provider and condition expression public async retrieveAndDecrypt( - provider: ethers.providers.Web3Provider, + web3Provider: ethers.providers.Web3Provider, conditionExpr: ConditionExpression, ciphertext: Ciphertext ): Promise { const decryptionShares = await this.retrieve( - provider, + web3Provider, conditionExpr, ciphertext ); @@ -64,15 +63,15 @@ export class ThresholdDecrypter { // Retrieve decryption shares public async retrieve( - provider: ethers.providers.Web3Provider, + web3Provider: ethers.providers.Web3Provider, conditionExpr: ConditionExpression, ciphertext: Ciphertext ): Promise { const dkgParticipants = await DkgCoordinatorAgent.getParticipants( - provider, + web3Provider, this.ritualId ); - const contextStr = await conditionExpr.buildContext(provider).toJson(); + const contextStr = await conditionExpr.buildContext(web3Provider).toJson(); const { sharedSecrets, encryptedRequests } = this.makeDecryptionRequests( this.ritualId, ciphertext, diff --git a/src/conditions/condition-expr.ts b/src/conditions/condition-expr.ts index 61c446e53..6af48fb36 100644 --- a/src/conditions/condition-expr.ts +++ b/src/conditions/condition-expr.ts @@ -2,6 +2,7 @@ import { Conditions as WASMConditions } from '@nucypher/nucypher-core'; import { ethers } from 'ethers'; import { SemVer } from 'semver'; +import { fromBytes } from '../../test/utils'; import { objectEquals, toBytes, toJSON } from '../utils'; import { @@ -94,6 +95,10 @@ export class ConditionExpression { return toBytes(this.toJson()); } + public static fromAad(aad: Uint8Array): ConditionExpression { + return ConditionExpression.fromJSON(fromBytes(aad)); + } + public equals(other: ConditionExpression): boolean { return [ this.version === other.version, diff --git a/src/dkg.ts b/src/dkg.ts index e65095838..e1e0d59e6 100644 --- a/src/dkg.ts +++ b/src/dkg.ts @@ -133,6 +133,19 @@ export class DkgClient { ); } + public static async getFinalizedRitual( + web3Provider: ethers.providers.Web3Provider, + ritualId: number + ): Promise { + const ritual = await DkgClient.getExistingRitual(web3Provider, ritualId); + if (ritual.state !== DkgRitualState.FINALIZED) { + throw new Error( + `Ritual ${ritualId} is not finalized. State: ${ritual.state}` + ); + } + return ritual; + } + // TODO: Without Validator public key in Coordinator, we cannot verify the // transcript. We need to add it to the Coordinator (nucypher-contracts #77). // public async verifyRitual(ritualId: number): Promise { diff --git a/src/sdk/strategy/cbd-strategy.ts b/src/sdk/strategy/cbd-strategy.ts index 83f61365d..32bef0523 100644 --- a/src/sdk/strategy/cbd-strategy.ts +++ b/src/sdk/strategy/cbd-strategy.ts @@ -77,7 +77,11 @@ export class DeployedCbdStrategy { ) {} public static create(dkgRitual: DkgRitual, porterUri: string) { - const decrypter = ThresholdDecrypter.create(porterUri, dkgRitual); + const decrypter = ThresholdDecrypter.create( + porterUri, + dkgRitual.id, + dkgRitual.dkgParams.threshold + ); return new DeployedCbdStrategy(decrypter, dkgRitual.dkgPublicKey); } diff --git a/src/taco.ts b/src/taco.ts new file mode 100644 index 000000000..3d4228c24 --- /dev/null +++ b/src/taco.ts @@ -0,0 +1,61 @@ +import { Ciphertext, ferveoEncrypt } from '@nucypher/nucypher-core'; +import { ethers } from 'ethers'; + +import { ThresholdDecrypter } from './characters/cbd-recipient'; +import { ConditionExpression } from './conditions'; +import { DkgClient } from './dkg'; +import { toBytes } from './utils'; + +export interface TacoMessageKit { + ciphertext: Ciphertext; + aad: Uint8Array; + ritualId: number; + threshold: number; +} + +export const encrypt = async ( + web3Provider: ethers.providers.Web3Provider, + message: string, + conditions: ConditionExpression, + ritualId: number +): Promise => { + const dkgRitual = await DkgClient.getFinalizedRitual(web3Provider, ritualId); + const aad = conditions.asAad(); + const ciphertext = ferveoEncrypt( + toBytes(message), + aad, + dkgRitual.dkgPublicKey + ); + return { + ciphertext, + aad, + ritualId, + threshold: dkgRitual.dkgParams.threshold, + }; +}; + +export const decrypt = async ( + web3Provider: ethers.providers.Web3Provider, + messageKit: TacoMessageKit, + porterUri: string +): Promise => { + const decrypter = ThresholdDecrypter.create( + porterUri, + messageKit.ritualId, + messageKit.threshold + ); + const condExpr = ConditionExpression.fromAad(messageKit.aad); + // TODO: Need web3Provider to fetch participants from Coordinator to make decryption requests. + // Should we put them into the message kit instead? + // Consider case where participants are changing over time. Is that an issue we should consider now? + return decrypter.retrieveAndDecrypt( + web3Provider, + condExpr, + messageKit.ciphertext + ); +}; + +export const taco = { + encrypt, + decrypt, +}; diff --git a/test/unit/cbd-strategy.test.ts b/test/unit/cbd-strategy.test.ts index f054bd15d..94c9db141 100644 --- a/test/unit/cbd-strategy.test.ts +++ b/test/unit/cbd-strategy.test.ts @@ -50,13 +50,13 @@ const makeCbdStrategy = async () => { async function makeDeployedCbdStrategy() { const strategy = await makeCbdStrategy(); - const mockedDkg = fakeDkgFlow(variant, 0, 4, 4); const mockedDkgRitual = fakeDkgRitual(mockedDkg); const web3Provider = fakeWeb3Provider(aliceSecretKey.toBEBytes()); const getUrsulasSpy = mockGetUrsulas(ursulas); const initializeRitualSpy = mockInitializeRitual(ritualId); const getExistingRitualSpy = mockGetExistingRitual(mockedDkgRitual); + const deployedStrategy = await strategy.deploy(web3Provider); expect(getUrsulasSpy).toHaveBeenCalled(); diff --git a/test/unit/conditions/condition-expr.test.ts b/test/unit/conditions/condition-expr.test.ts index 806269a7f..4d961026f 100644 --- a/test/unit/conditions/condition-expr.test.ts +++ b/test/unit/conditions/condition-expr.test.ts @@ -77,7 +77,7 @@ describe('condition set', () => { ).toBeTruthy(); }); - it('different minor/patch version but same condition', async () => { + it('different minor/patch version but same condition', () => { const conditionExprOlderMinorVersion = new ConditionExpression( rpcCondition, '0.1.0' @@ -97,7 +97,7 @@ describe('condition set', () => { ).not.toBeTruthy(); }); - it('minor/patch number greater than major; still older', async () => { + it('minor/patch number greater than major; still older', () => { const conditionExprOlderMinorVersion = new ConditionExpression( rpcCondition, '0.9.0' @@ -140,7 +140,7 @@ describe('condition set', () => { contractConditionWithAbi, timeCondition, compoundCondition, - ])('same version but different condition', async (condition) => { + ])('same version but different condition', (condition) => { const conditionExprSameVersionDifferentCondition = new ConditionExpression(condition); expect( @@ -150,7 +150,7 @@ describe('condition set', () => { ).not.toBeTruthy(); }); - it('same contract condition although using erc721 helper', async () => { + it('same contract condition although using erc721 helper', () => { const erc721ConditionExpr = new ConditionExpression( erc721BalanceCondition ); @@ -171,7 +171,7 @@ describe('condition set', () => { rpcCondition, timeCondition, compoundCondition, - ])('serializes to and from json', async (condition) => { + ])('serializes to and from json', (condition) => { const conditionExpr = new ConditionExpression(condition); const conditionExprJson = conditionExpr.toJson(); expect(conditionExprJson).toBeDefined(); @@ -186,7 +186,16 @@ describe('condition set', () => { expect(conditionExprFromJson.equals(conditionExprFromJson)).toBeTruthy(); }); - it('incompatible version', async () => { + it('serializes to and from aad', () => { + const conditionExpr = new ConditionExpression(rpcCondition); + const conditionExprAad = conditionExpr.asAad(); + const conditionExprFromAad = + ConditionExpression.fromAad(conditionExprAad); + expect(conditionExprFromAad).toBeDefined(); + expect(conditionExprFromAad.equals(conditionExpr)).toBeTruthy(); + }); + + it('incompatible version', () => { const currentVersion = new SemVer(ConditionExpression.VERSION); const invalidVersion = currentVersion.inc('major'); expect(() => { @@ -249,13 +258,13 @@ describe('condition set', () => { method: 'isPolicyActive', }, }, - ])("can't determine condition type", async (invalidCondition) => { + ])("can't determine condition type", (invalidCondition) => { expect(() => { ConditionExpression.fromObj(invalidCondition); }).toThrow('unrecognized condition data'); }); - it('erc721 condition serialization', async () => { + it('erc721 condition serialization', () => { const conditionExpr = new ConditionExpression(erc721BalanceCondition); const erc721BalanceConditionObj = erc721BalanceCondition.toObj(); @@ -283,7 +292,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition); }); - it('contract condition no abi serialization', async () => { + it('contract condition no abi serialization', () => { const conditionExpr = new ConditionExpression(contractConditionNoAbi); const conditionExprJson = conditionExpr.toJson(); @@ -315,7 +324,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition); }); - it('contract condition with abi serialization', async () => { + it('contract condition with abi serialization', () => { const conditionExpr = new ConditionExpression(contractConditionWithAbi); const conditionExprJson = conditionExpr.toJson(); @@ -348,7 +357,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition); }); - it('time condition serialization', async () => { + it('time condition serialization', () => { const conditionExpr = new ConditionExpression(timeCondition); const conditionExprJson = conditionExpr.toJson(); @@ -371,7 +380,7 @@ describe('condition set', () => { expect(conditionExprFromJson.condition).toBeInstanceOf(TimeCondition); }); - it('rpc condition serialization', async () => { + it('rpc condition serialization', () => { const conditionExpr = new ConditionExpression(rpcCondition); const conditionExprJson = conditionExpr.toJson(); diff --git a/test/unit/taco.test.ts b/test/unit/taco.test.ts new file mode 100644 index 000000000..fe74043dd --- /dev/null +++ b/test/unit/taco.test.ts @@ -0,0 +1,88 @@ +import { + FerveoVariant, + SecretKey, + SessionStaticSecret, +} from '@nucypher/nucypher-core'; + +import { conditions } from '../../src'; +import { taco } from '../../src/taco'; +import { toBytes } from '../../src/utils'; +import { + fakeDkgFlow, + fakeDkgParticipants, + fakeDkgRitual, + fakePorterUri, + fakeTDecFlow, + fakeWeb3Provider, + mockCbdDecrypt, + mockGetExistingRitual, + mockGetParticipants, + mockRandomSessionStaticSecret, +} from '../utils'; + +import { aliceSecretKeyBytes } from './testVariables'; + +const { + predefined: { ERC721Ownership }, + ConditionExpression, +} = conditions; + +// Shared test variables +const aliceSecretKey = SecretKey.fromBEBytes(aliceSecretKeyBytes); +const ownsNFT = new ERC721Ownership({ + contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', + parameters: [3591], + chain: 5, +}); +const conditionExpr = new ConditionExpression(ownsNFT); +const variant = FerveoVariant.precomputed; +// const ritualId = 0; +const message = 'this is a secret'; + +describe('taco', () => { + it('encrypts and decrypts', async () => { + const mockedDkg = fakeDkgFlow(variant, 0, 4, 4); + const mockedDkgRitual = fakeDkgRitual(mockedDkg); + const web3Provider = fakeWeb3Provider(aliceSecretKey.toBEBytes()); + const getExistingRitualSpy = mockGetExistingRitual(mockedDkgRitual); + + const tacoMk = await taco.encrypt( + web3Provider, + message, + conditionExpr, + mockedDkg.ritualId + ); + + expect(getExistingRitualSpy).toHaveBeenCalled(); + + const { decryptionShares } = fakeTDecFlow({ + ...mockedDkg, + message: toBytes(message), + aad: tacoMk.aad, + ciphertext: tacoMk.ciphertext, + }); + const { participantSecrets, participants } = fakeDkgParticipants( + mockedDkg.ritualId, + variant + ); + const requesterSessionKey = SessionStaticSecret.random(); + const decryptSpy = mockCbdDecrypt( + mockedDkg.ritualId, + decryptionShares, + participantSecrets, + requesterSessionKey.publicKey() + ); + const getParticipantsSpy = mockGetParticipants(participants); + const sessionKeySpy = mockRandomSessionStaticSecret(requesterSessionKey); + + const decryptedMessage = await taco.decrypt( + web3Provider, + tacoMk, + fakePorterUri + ); + expect(getParticipantsSpy).toHaveBeenCalled(); + expect(sessionKeySpy).toHaveBeenCalled(); + expect(decryptSpy).toHaveBeenCalled(); + expect(decryptedMessage).toEqual(toBytes(message)); + }); +}); diff --git a/test/utils.ts b/test/utils.ts index 69fd8186e..7f4ff2eec 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -304,10 +304,7 @@ export const fakeTDecFlow = ({ message, }: FakeDkgRitualFlow) => { // Having aggregated the transcripts, the validators can now create decryption shares - const decryptionShares: ( - | DecryptionSharePrecomputed - | DecryptionShareSimple - )[] = []; + const decryptionShares: DecryptionShareSimple[] = []; zip(validators, validatorKeypairs).forEach(([validator, keypair]) => { const dkg = new Dkg(ritualId, sharesNum, threshold, validators, validator); const aggregate = dkg.aggregateTranscript(receivedMessages);