diff --git a/apps/connect/src/routes/authenticate.tsx b/apps/connect/src/routes/authenticate.tsx index 843196b2..94202afe 100644 --- a/apps/connect/src/routes/authenticate.tsx +++ b/apps/connect/src/routes/authenticate.tsx @@ -16,6 +16,10 @@ import { createWalletClient, custom } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET; +const API_URL = + import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' + ? `${Graph.MAINNET_API_ORIGIN}/graphql` + : `${Graph.TESTNET_API_ORIGIN}/graphql`; type AuthenticateSearch = { data: unknown; @@ -135,7 +139,6 @@ function AuthenticateComponent() { const accountAddress = useSelector(StoreConnect.store, (state) => state.context.accountAddress); const keys = useSelector(StoreConnect.store, (state) => state.context.keys); - const { signMessage } = usePrivy(); const { wallets } = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy') || wallets[0]; @@ -143,11 +146,7 @@ function AuthenticateComponent() { const [selectedPrivateSpaces, setSelectedPrivateSpaces] = useState>(new Set()); const { isPending: privateSpacesPending, error: privateSpacesError, data: privateSpacesData } = usePrivateSpaces(); - const { - isPending: publicSpacesPending, - error: publicSpacesError, - data: publicSpacesData, - } = usePublicSpaces(`${Graph.TESTNET_API_ORIGIN}/graphql`); + const { data: publicSpacesData } = usePublicSpaces(API_URL); useEffect(() => { const run = async () => { @@ -334,21 +333,6 @@ function AuthenticateComponent() { transport: custom(privyProvider), }); - const signer: Identity.Signer = { - getAddress: async () => { - const [address] = await walletClient.getAddresses(); - return address; - }, - signMessage: async (message: string) => { - if (embeddedWallet.walletClientType === 'privy') { - const { signature } = await signMessage({ message }); - return signature; - } - const [address] = await walletClient.getAddresses(); - return await walletClient.signMessage({ account: address, message }); - }, - }; - const newAppIdentity = Connect.createAppIdentity(); console.log('creating smart session'); @@ -388,25 +372,13 @@ function AuthenticateComponent() { rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL, }); - const appIdentityKeys = { - encryptionPrivateKey: newAppIdentity.encryptionPrivateKey, - encryptionPublicKey: newAppIdentity.encryptionPublicKey, - signaturePrivateKey: newAppIdentity.signaturePrivateKey, - signaturePublicKey: newAppIdentity.signaturePublicKey, - }; console.log('encrypting app identity'); - const { ciphertext, nonce } = await Connect.encryptAppIdentity( - signer, - newAppIdentity.address, - newAppIdentity.addressPrivateKey, - permissionId, - appIdentityKeys, - ); + const { ciphertext } = await Connect.encryptAppIdentity({ ...newAppIdentity, permissionId }, keys); console.log('proving ownership'); const { accountProof, keyProof } = await Identity.proveIdentityOwnership( smartAccountClient, accountAddress, - appIdentityKeys, + newAppIdentity, ); const message: Messages.RequestConnectCreateAppIdentity = { @@ -416,7 +388,6 @@ function AuthenticateComponent() { signaturePublicKey: keys.signaturePublicKey, encryptionPublicKey: keys.encryptionPublicKey, ciphertext, - nonce, accountProof, keyProof, }; @@ -459,48 +430,16 @@ function AuthenticateComponent() { }; const decryptAppIdentityAndRedirect = async () => { - if (!state.appIdentityResponse) { + if (!state.appIdentityResponse || !keys) { return; } - const privyProvider = await embeddedWallet.getEthereumProvider(); - const walletClient = createWalletClient({ - account: embeddedWallet.address as `0x${string}`, - chain: CHAIN, - transport: custom(privyProvider), - }); - - const signer: Identity.Signer = { - getAddress: async () => { - const [address] = await walletClient.getAddresses(); - return address; - }, - signMessage: async (message: string) => { - if (embeddedWallet.walletClientType === 'privy') { - const { signature } = await signMessage({ message }); - return signature; - } - const [address] = await walletClient.getAddresses(); - return await walletClient.signMessage({ account: address, message }); - }, - }; - - const decryptedIdentity = await Connect.decryptAppIdentity( - signer, - state.appIdentityResponse.ciphertext, - state.appIdentityResponse.nonce, - ); + const decryptedIdentity = await Connect.decryptAppIdentity(state.appIdentityResponse.ciphertext, keys); await encryptSpacesAndRedirect({ accountAddress: state.appIdentityResponse.accountAddress, appIdentity: { - address: decryptedIdentity.address, - addressPrivateKey: decryptedIdentity.addressPrivateKey, + ...decryptedIdentity, accountAddress: state.appIdentityResponse.accountAddress, - permissionId: decryptedIdentity.permissionId, - encryptionPrivateKey: decryptedIdentity.encryptionPrivateKey, - signaturePrivateKey: decryptedIdentity.signaturePrivateKey, - encryptionPublicKey: decryptedIdentity.encryptionPublicKey, - signaturePublicKey: decryptedIdentity.signaturePublicKey, sessionToken: state.appIdentityResponse.sessionToken, sessionTokenExpires: new Date(state.appIdentityResponse.sessionTokenExpires), }, diff --git a/apps/server/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql b/apps/server/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql new file mode 100644 index 00000000..b5aed7b7 --- /dev/null +++ b/apps/server/prisma/migrations/20250627185421_remove_app_identity_nonce/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the column `nonce` on the `AppIdentity` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_AppIdentity" ( + "address" TEXT NOT NULL PRIMARY KEY, + "ciphertext" TEXT NOT NULL, + "signaturePublicKey" TEXT NOT NULL, + "encryptionPublicKey" TEXT NOT NULL, + "accountProof" TEXT NOT NULL, + "keyProof" TEXT NOT NULL, + "accountAddress" TEXT NOT NULL, + "appId" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "sessionTokenExpires" DATETIME NOT NULL, + CONSTRAINT "AppIdentity_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_AppIdentity" ("accountAddress", "accountProof", "address", "appId", "ciphertext", "encryptionPublicKey", "keyProof", "sessionToken", "sessionTokenExpires", "signaturePublicKey") SELECT "accountAddress", "accountProof", "address", "appId", "ciphertext", "encryptionPublicKey", "keyProof", "sessionToken", "sessionTokenExpires", "signaturePublicKey" FROM "AppIdentity"; +DROP TABLE "AppIdentity"; +ALTER TABLE "new_AppIdentity" RENAME TO "AppIdentity"; +CREATE INDEX "AppIdentity_sessionToken_idx" ON "AppIdentity"("sessionToken"); +CREATE UNIQUE INDEX "AppIdentity_accountAddress_appId_key" ON "AppIdentity"("accountAddress", "appId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index 2f2fce42..d5f629e2 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -115,7 +115,6 @@ model Account { model AppIdentity { address String @id ciphertext String - nonce String signaturePublicKey String encryptionPublicKey String accountProof String @@ -129,7 +128,6 @@ model AppIdentity { sessionTokenExpires DateTime @@unique([accountAddress, appId]) - @@unique([accountAddress, nonce]) @@index([sessionToken]) } diff --git a/apps/server/src/handlers/create-app-identity.ts b/apps/server/src/handlers/create-app-identity.ts index a157b1fb..15022f09 100644 --- a/apps/server/src/handlers/create-app-identity.ts +++ b/apps/server/src/handlers/create-app-identity.ts @@ -19,7 +19,6 @@ export const createAppIdentity = async ({ address, appId, ciphertext, - nonce, signaturePublicKey, encryptionPublicKey, accountProof, @@ -43,7 +42,6 @@ export const createAppIdentity = async ({ accountAddress, appId, ciphertext, - nonce, signaturePublicKey, encryptionPublicKey, accountProof, diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 842c6850..7bacf3da 100755 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -340,7 +340,6 @@ app.post('/connect/app-identity', async (req, res) => { appId: message.appId, address: message.address, ciphertext: message.ciphertext, - nonce: message.nonce, signaturePublicKey: message.signaturePublicKey, encryptionPublicKey: message.encryptionPublicKey, accountProof: message.accountProof, diff --git a/packages/hypergraph/src/connect/identity-encryption.ts b/packages/hypergraph/src/connect/identity-encryption.ts index 17ea4017..81240ddf 100644 --- a/packages/hypergraph/src/connect/identity-encryption.ts +++ b/packages/hypergraph/src/connect/identity-encryption.ts @@ -5,9 +5,15 @@ import { sha256 } from '@noble/hashes/sha256'; import type { Hex } from 'viem'; import { verifyMessage } from 'viem'; +import { cryptoBoxSeal, cryptoBoxSealOpen } from '@serenity-kit/noble-sodium'; import { bytesToHex, canonicalize, hexToBytes } from '../utils/index.js'; import type { IdentityKeys, PrivateAppIdentity, Signer } from './types.js'; +export type AppIdentityForEncryption = Omit< + PrivateAppIdentity, + 'sessionToken' | 'sessionTokenExpires' | 'accountAddress' +>; + // Adapted from the XMTP approach to encrypt keys // See: https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L79-L116 // (We reimplement their encrypt/decrypt functions using noble). @@ -134,86 +140,45 @@ export const decryptIdentity = async (signer: Signer, ciphertext: string, nonce: }; export const encryptAppIdentity = async ( - signer: Signer, - appIdentityAddress: string, - appIdentityAddressPrivateKey: string, - permissionId: string, + appIdentity: AppIdentityForEncryption, keys: IdentityKeys, -): Promise<{ ciphertext: string; nonce: string }> => { - const nonce = randomBytes(32); - const message = signatureMessage(nonce); - const signature = (await signer.signMessage(message)) as Hex; - - // Check that the signature is valid - const valid = await verifyMessage({ - address: (await signer.getAddress()) as Hex, - message, - signature, - }); - if (!valid) { - throw new Error('Invalid signature'); - } - const secretKey = hexToBytes(signature); +): Promise<{ ciphertext: string }> => { // We use a simple plaintext encoding: // Hex keys separated by newlines const keysTxt = [ - keys.encryptionPublicKey, - keys.encryptionPrivateKey, - keys.signaturePublicKey, - keys.signaturePrivateKey, - appIdentityAddress, - appIdentityAddressPrivateKey, - permissionId, + appIdentity.encryptionPublicKey, + appIdentity.encryptionPrivateKey, + appIdentity.signaturePublicKey, + appIdentity.signaturePrivateKey, + appIdentity.address, + appIdentity.addressPrivateKey, + appIdentity.permissionId, ].join('\n'); const keysMsg = new TextEncoder().encode(keysTxt); - const ciphertext = encrypt(keysMsg, secretKey); - return { ciphertext, nonce: bytesToHex(nonce) }; + const ciphertext = bytesToHex( + cryptoBoxSeal({ + message: keysMsg, + publicKey: hexToBytes(keys.encryptionPublicKey), + }), + ); + return { ciphertext }; }; -export const decryptAppIdentity = async ( - signer: Signer, - ciphertext: string, - nonce: string, -): Promise> => { - const message = signatureMessage(hexToBytes(nonce)); - const signature = (await signer.signMessage(message)) as Hex; - - // Check that the signature is valid - const valid = await verifyMessage({ - address: (await signer.getAddress()) as Hex, - message, - signature, +export const decryptAppIdentity = async (ciphertext: string, keys: IdentityKeys): Promise => { + const ciphertextBytes = hexToBytes(ciphertext); + const keysMsg = cryptoBoxSealOpen({ + ciphertext: ciphertextBytes, + privateKey: hexToBytes(keys.encryptionPrivateKey), + publicKey: hexToBytes(keys.encryptionPublicKey), }); - if (!valid) { - throw new Error('Invalid signature'); - } - const secretKey = hexToBytes(signature); - let keysMsg: Uint8Array; - try { - keysMsg = await decrypt(ciphertext, secretKey); - } catch (e) { - // See https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L142-L146 - if (secretKey.length !== 65) { - throw new Error('Expected 65 bytes before trying a different recovery byte'); - } - // Try the other version of recovery byte, either +27 or -27 - const lastByte = secretKey[secretKey.length - 1]; - let newSecret = secretKey.slice(0, secretKey.length - 1); - if (lastByte < 27) { - newSecret = new Uint8Array([...newSecret, lastByte + 27]); - } else { - newSecret = new Uint8Array([...newSecret, lastByte - 27]); - } - keysMsg = await decrypt(ciphertext, newSecret); - } const keysTxt = new TextDecoder().decode(keysMsg); const [ encryptionPublicKey, encryptionPrivateKey, signaturePublicKey, signaturePrivateKey, - appIdentityAddress, - appIdentityAddressPrivateKey, + address, + addressPrivateKey, permissionId, ] = keysTxt.split('\n'); return { @@ -221,8 +186,8 @@ export const decryptAppIdentity = async ( encryptionPrivateKey, signaturePublicKey, signaturePrivateKey, - address: appIdentityAddress, - addressPrivateKey: appIdentityAddressPrivateKey, + address, + addressPrivateKey, permissionId, }; }; diff --git a/packages/hypergraph/src/connect/types.ts b/packages/hypergraph/src/connect/types.ts index 8d8b5dd0..9375fce2 100644 --- a/packages/hypergraph/src/connect/types.ts +++ b/packages/hypergraph/src/connect/types.ts @@ -36,7 +36,6 @@ export const AppIdentityResponse = Schema.Struct({ accountProof: Schema.String, keyProof: Schema.String, ciphertext: Schema.String, - nonce: Schema.String, sessionToken: Schema.String, address: Schema.String, appId: Schema.String, diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index 5adbf3b3..d2332e8e 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -245,7 +245,6 @@ export const RequestConnectCreateAppIdentity = Schema.Struct({ address: Schema.String, accountAddress: Schema.String, ciphertext: Schema.String, - nonce: Schema.String, signaturePublicKey: Schema.String, encryptionPublicKey: Schema.String, accountProof: Schema.String,