Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decrypt API Rework #560

Merged
merged 21 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38eca23
Simplify calls to retrieveAndDecrypt and retrieve. We don't need to g…
derekpierre Jul 17, 2024
bb99d4a
Allow creation of Context from a message kit.
derekpierre Jul 18, 2024
ee262c4
Don't have ConditionExpr build a context, the context can be created …
derekpierre Jul 18, 2024
9e443e2
Allow ConditionContext to be an optional parameter for decryption; it…
derekpierre Jul 18, 2024
f2c8358
Allow address property to be public for SingleSignOnExternalEIP4361Au…
derekpierre Jul 18, 2024
68788f6
Update tests to accomodate new architecture for ConditionContext.
derekpierre Jul 18, 2024
53a83d2
Reconfigure taco-auth providers folder to have an eip4361 folder and …
derekpierre Jul 18, 2024
9c5548c
Move RESERVED_CONTEXT_VARIABLES constant to context module since it i…
derekpierre Jul 18, 2024
dce202a
Fix message to be use prefix constant.
derekpierre Jul 18, 2024
4f7c2ac
Update nodejs example used for CI job to pass ConditionContext instea…
derekpierre Jul 19, 2024
e613d3a
Appease linter.
derekpierre Jul 19, 2024
e9e332f
Fix webpack-5 example to use ConditionContext instead of directly pro…
derekpierre Jul 19, 2024
3c7cefb
Fix other taco examples to use ConditionContext instead of directly p…
derekpierre Jul 19, 2024
cfc669d
Code cleanup based on RFC for #554.
derekpierre Jul 22, 2024
8a751c6
Add tests for processing addition of authProviders to ConditionContext.
derekpierre Jul 22, 2024
83585f4
Remove unnecessary import.
derekpierre Jul 30, 2024
8c30bc4
Fix linting errors post-rebase.
derekpierre Aug 15, 2024
81d9006
Construct a PorterClient earlier in the call stack for decrypt(), and…
derekpierre Aug 16, 2024
e872fff
Update variable naming for ConditionContext to clarify that they are …
derekpierre Aug 16, 2024
99732a9
Fix failing porter test now that the dictionary of URIs from github i…
derekpierre Aug 16, 2024
1e528aa
Add illustrative example of using requestedContextParameters property…
derekpierre Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions examples/taco/nextjs/src/hooks/useTaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
encrypt,
initialize,
ThresholdMessageKit,
USER_ADDRESS_PARAM_DEFAULT,
} from '@nucypher/taco';
import { ethers } from 'ethers';
import { useCallback, useEffect, useState } from 'react';
Expand All @@ -32,12 +33,13 @@ export default function useTaco({
}
const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes);
const authProvider = new EIP4361AuthProvider(provider, signer);
return decrypt(
provider,
domain,
messageKit,
const conditionContext =
conditions.context.ConditionContext.fromMessageKit(messageKit);
conditionContext.addAuthProvider(
USER_ADDRESS_PARAM_DEFAULT,
authProvider,
);
return decrypt(provider, domain, messageKit, conditionContext);
},
[isInit, provider, domain],
);
Expand Down
32 changes: 19 additions & 13 deletions examples/taco/nodejs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { format } from 'node:util';

import {
EIP4361AuthProvider,
ThresholdMessageKit,
USER_ADDRESS_PARAM_DEFAULT,
conditions,
decrypt,
domains,
EIP4361AuthProvider,
encrypt,
fromBytes,
initialize,
isAuthorized,
ThresholdMessageKit,
toBytes,
toHexString,
} from '@nucypher/taco';
Expand Down Expand Up @@ -108,17 +109,22 @@ const decryptFromBytes = async (encryptedBytes: Uint8Array) => {
domain: 'localhost',
uri: 'http://localhost:3000',
};
const authProvider = new EIP4361AuthProvider(
provider,
consumerSigner,
siweParams,
);
return decrypt(
provider,
domain,
messageKit,
authProvider,
);
const conditionContext =
conditions.context.ConditionContext.fromMessageKit(messageKit);

// illustrative optional example of checking what context parameters are required
// unnecessary if you already know what the condition contains
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beautiful! 🚀

if (
conditionContext.requestedContextParameters.has(USER_ADDRESS_PARAM_DEFAULT)
) {
const authProvider = new EIP4361AuthProvider(
provider,
consumerSigner,
siweParams,
);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);
}
return decrypt(provider, domain, messageKit, conditionContext);
Comment on lines +118 to +127
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this avoids having to hardcode context parameters – i.e. the app doesn't need to know the conditions in advance in order to match them to the right authentication method? That's pretty amazing!

One thing I'm wondering is – what resource is being queried here? Do we need to create a public resource that maps conditions to context parameters? Or is this a list that developers will create for their own domain?

Copy link
Member Author

@derekpierre derekpierre Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this avoids having to hardcode context parameters – i.e. the app doesn't need to know the conditions in advance in order to match them to the right authentication method? That's pretty amazing!

Indeed. This is the case where an application doesn't just use the same condition every time, but perhaps some permutations of a set of conditions, each with varying context parameters. In this way the dev can use logic to get the set of expected parameters and populate them based on their own logic.

They could have dynamic logic like the following:

if (requestedContextParmeters.has(":tokenId")) {
   // user subjected to condition 1 which uses ":tokenId" custom context parameter
   const value = ...
   conditionContext.addCustomContextParameters({":tokenId": value});
}
if (requestedContextParmeters.has(":otherParam")) {
   // user subjected to condition 2 which uses ":otherParam" custom context parameter
   const otherValue = ...
   conditionContext.addCustomContextParameters({":otherParam": otherValue});
}
if (requestedContextParmeters.has(":userAddress")) {
   // user subjected to condition with taco SIWE
   const authProvider = new EIP4361AuthProvider(...);
   conditionContext.addAuthProvider(":userAddress", auth provider);
}
if (requestedContextParmeters.has(":userAddressEIP4361")) {
   // user subjected to condition with single-sign on SIWE
   const authProvider = SingleSignOnEIP4361AuthProvider.fromExistingSiweInfo(...)
   conditionContext.addAuthProvider(":userAddressEIP4361", authProvider);
}
...

One thing I'm wondering is – what resource is being queried here?

Assuming I correctly understand the question, the ConditionContext looks at the underlying condition in the ThresholdMessageKit (encrypted data), and then returns all the context parameters in the underlying condition (requestedContextParameters property). It is then up to the app to populate the values appropriately. How they do that is up to them - we are not being prescriptive.

Do we need to create a public resource that maps conditions to context parameters? Or is this a list that developers will create for their own domain?

Interesting. To me, it's the latter. Devs can decide how they want to get/generate the values to use for conditions.
IMHO, on the decryption side, we should endeavour for apps not to have to deal with the underlying condition object(string) themselves. They can if they want to, of course, but they shouldn't really have to. It's part of the reason I made the ConditionContext object take a ThresholdMessageKit and not the condition object contained within the ThresholdMessageKit.

With something like a public mapping resource (condition string/type -> context parameters...?), it may be easy for simple conditions eg. BalanceOf, or OwnsNFT, but it's tough cover all cases with a mapping because our conditions are extensible, which is intentional for breadth of functionality. Devs can use whatever custom context parameters they want, and they can also use :userAddressExternalEIP4361 (single sign on SIWE) OR :userAddress (taco SIWE) if they like...it would be tough to cover all cases. Even with the mapping, they'll still need to know/determine what value to use for the context parameters - so they are kind of back to something like the 'if' statements above.

};

const runExample = async () => {
Expand Down
10 changes: 6 additions & 4 deletions examples/taco/react/src/hooks/useTaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
encrypt,
initialize,
ThresholdMessageKit,
USER_ADDRESS_PARAM_DEFAULT,
} from '@nucypher/taco';
import { ethers } from 'ethers';
import { useCallback, useEffect, useState } from 'react';
Expand All @@ -32,12 +33,13 @@ export default function useTaco({
}
const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes);
const authProvider = new EIP4361AuthProvider(provider, signer);
return decrypt(
provider,
domain,
messageKit,
const conditionContext =
conditions.context.ConditionContext.fromMessageKit(messageKit);
conditionContext.addAuthProvider(
USER_ADDRESS_PARAM_DEFAULT,
authProvider,
);
return decrypt(provider, domain, messageKit, conditionContext);
},
[isInit, provider, domain],
);
Expand Down
6 changes: 5 additions & 1 deletion examples/taco/webpack-5/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
fromBytes,
initialize,
toBytes,
USER_ADDRESS_PARAM_DEFAULT,
} from '@nucypher/taco';
import { ethers } from 'ethers';
import { hexlify } from 'ethers/lib/utils';
Expand Down Expand Up @@ -61,11 +62,14 @@ const runExample = async () => {

console.log('Decrypting message...');
const authProvider = new EIP4361AuthProvider(provider, signer);
const conditionContext =
conditions.context.ConditionContext.fromMessageKit(messageKit);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);
const decryptedBytes = await decrypt(
provider,
domain,
messageKit,
authProvider,
conditionContext,
);
const decryptedMessage = fromBytes(decryptedBytes);
console.log('Decrypted message:', decryptedMessage);
Expand Down
4 changes: 1 addition & 3 deletions packages/shared/src/porter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ export const getPorterUri = async (domain: Domain): Promise<string> => {
return (await getPorterUris(domain))[0];
};

export const getPorterUris = async (
domain: Domain,
): Promise<string[]> => {
export const getPorterUris = async (domain: Domain): Promise<string[]> => {
const fullList = [];
const uri = defaultPorterUri[domain];
if (!uri) {
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/test/porter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('getPorterUris', () => {
it('Get URIs from source', async () => {
for (const domain of Object.values(domains)) {
const uris = await getPorterUrisFromSource(domain);
expect(uris.length).toBeGreaterThan(0);
expect(uris.length).toBeGreaterThanOrEqual(0);
const fullList = await getPorterUris(domain);
expect(fullList).toEqual(expect.arrayContaining(uris));
}
Expand Down
15 changes: 0 additions & 15 deletions packages/taco-auth/src/auth-provider.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
import { AuthSignature } from './auth-sig';
import { EIP4361AuthProvider } from './providers';

export const EIP4361_AUTH_METHOD = 'EIP4361';

export interface AuthProvider {
getOrCreateAuthSignature(): Promise<AuthSignature>;
}

export type AuthProviders = {
[EIP4361_AUTH_METHOD]?: EIP4361AuthProvider;
// Fallback to satisfy type checking
[key: string]: AuthProvider | undefined;
};

export const USER_ADDRESS_PARAM_DEFAULT = ':userAddress';

export const AUTH_METHOD_FOR_PARAM: Record<string, string> = {
[USER_ADDRESS_PARAM_DEFAULT]: EIP4361_AUTH_METHOD,
};
6 changes: 4 additions & 2 deletions packages/taco-auth/src/auth-sig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { EthAddressSchema } from '@nucypher/shared';
import { z } from 'zod';

import { EIP4361_AUTH_METHOD } from './auth-provider';
import { EIP4361TypedDataSchema } from './providers';
import {
EIP4361_AUTH_METHOD,
EIP4361TypedDataSchema,
} from './providers/eip4361/common';

export const authSignatureSchema = z.object({
signature: z.string(),
Expand Down
17 changes: 17 additions & 0 deletions packages/taco-auth/src/providers/eip4361/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SiweMessage } from 'siwe';
import { z } from 'zod';

export const EIP4361_AUTH_METHOD = 'EIP4361';

const isSiweMessage = (message: string): boolean => {
try {
new SiweMessage(message);
return true;
} catch {
return false;
}
};

export const EIP4361TypedDataSchema = z
.string()
.refine(isSiweMessage, { message: 'Invalid SIWE message' });
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { ethers } from 'ethers';
import { generateNonce, SiweMessage } from 'siwe';
import { z } from 'zod';

import { EIP4361_AUTH_METHOD } from '../auth-provider';
import { AuthSignature } from '../auth-sig';
import { LocalStorage } from '../storage';
import { AuthSignature } from '../../auth-sig';
import { LocalStorage } from '../../storage';

const isSiweMessage = (message: string): boolean => {
try {
new SiweMessage(message);
return true;
} catch {
return false;
}
};
import { EIP4361_AUTH_METHOD } from './common';

export const EIP4361TypedDataSchema = z
.string()
.refine(isSiweMessage, { message: 'Invalid SIWE message' });
export const USER_ADDRESS_PARAM_DEFAULT = ':userAddress';

export type EIP4361AuthProviderParams = {
domain: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { SiweMessage } from 'siwe';

import { EIP4361_AUTH_METHOD } from '../auth-provider';
import { AuthSignature } from '../auth-sig';
import { AuthSignature } from '../../auth-sig';

import { EIP4361_AUTH_METHOD } from './common';

export const USER_ADDRESS_PARAM_EXTERNAL_EIP4361 =
':userAddressExternalEIP4361';

export class SingleSignOnEIP4361AuthProvider {
public static async fromExistingSiweInfo(
Expand All @@ -22,7 +26,7 @@ export class SingleSignOnEIP4361AuthProvider {

private constructor(
private readonly existingSiweMessage: string,
private readonly address: string,
public readonly address: string,
private readonly signature: string,
) {}

Expand Down
4 changes: 2 additions & 2 deletions packages/taco-auth/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './eip4361';
export * from './external-eip4361';
export * from './eip4361/eip4361';
export * from './eip4361/external-eip4361';
3 changes: 2 additions & 1 deletion packages/taco-auth/test/auth-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
import { SiweMessage } from 'siwe';
import { describe, expect, it } from 'vitest';

import { EIP4361AuthProvider, EIP4361TypedDataSchema } from '../src';
import { EIP4361AuthProvider } from '../src/providers';
import { EIP4361TypedDataSchema } from '../src/providers/eip4361/common';

describe('auth provider', () => {
const provider = fakeProvider(bobSecretKeyBytes);
Expand Down
6 changes: 5 additions & 1 deletion packages/taco/examples/encrypt-decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
initialize,
ThresholdMessageKit,
toBytes,
USER_ADDRESS_PARAM_DEFAULT,
} from '../src';

const ritualId = 1;
Expand Down Expand Up @@ -49,11 +50,14 @@ const run = async () => {
web3Provider,
web3Provider.getSigner(),
);
const conditionContext =
conditions.context.ConditionContext.fromMessageKit(messageKit);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);
const decryptedMessage = await decrypt(
web3Provider,
domains.TESTNET,
messageKit,
authProvider,
conditionContext,
);
return decryptedMessage;
};
Expand Down
13 changes: 0 additions & 13 deletions packages/taco/src/conditions/condition-expr.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Conditions as CoreConditions } from '@nucypher/nucypher-core';
import { toJSON } from '@nucypher/shared';
import { AuthProviders } from '@nucypher/taco-auth';
import { SemVer } from 'semver';

import { Condition } from './condition';
import { ConditionFactory } from './condition-factory';
import { ConditionContext, CustomContextParam } from './context';

const ERR_VERSION = (provided: string, current: string) =>
`Version provided, ${provided}, is incompatible with current version, ${current}`;
Expand Down Expand Up @@ -64,17 +62,6 @@ export class ConditionExpression {
return ConditionExpression.fromJSON(conditions.toString());
}

public buildContext(
customParameters: Record<string, CustomContextParam> = {},
authProviders: AuthProviders = {},
): ConditionContext {
return new ConditionContext(
this.condition,
customParameters,
authProviders,
);
}

public equals(other: ConditionExpression): boolean {
return [
this.version === other.version,
Expand Down
13 changes: 4 additions & 9 deletions packages/taco/src/conditions/const.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChainId } from '@nucypher/shared';
import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';

export const USER_ADDRESS_PARAM_EXTERNAL_EIP4361 =
':userAddressExternalEIP4361';
import {
USER_ADDRESS_PARAM_DEFAULT,
USER_ADDRESS_PARAM_EXTERNAL_EIP4361,
} from '@nucypher/taco-auth';

// Only allow alphanumeric characters and underscores
export const CONTEXT_PARAM_REGEXP = new RegExp('^:[a-zA-Z_][a-zA-Z0-9_]*$');
Expand All @@ -21,8 +21,3 @@ export const USER_ADDRESS_PARAMS = [
// Ordering matters, this should always be last
USER_ADDRESS_PARAM_DEFAULT,
];

export const RESERVED_CONTEXT_PARAMS = [
USER_ADDRESS_PARAM_DEFAULT,
// USER_ADDRESS_PARAM_EXTERNAL_EIP4361 is not reserved and can be used as a custom context parameter
];
Loading