Skip to content

Commit e608b19

Browse files
committed
fix: use Connect encryption key to encrypt app identities
1 parent 22dba44 commit e608b19

File tree

9 files changed

+85
-212
lines changed

9 files changed

+85
-212
lines changed

apps/connect/src/routes/authenticate.tsx

Lines changed: 6 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -134,37 +134,13 @@ function AuthenticateComponent() {
134134
const accountAddress = useSelector(StoreConnect.store, (state) => state.context.accountAddress);
135135
const keys = useSelector(StoreConnect.store, (state) => state.context.keys);
136136

137-
const { signMessage } = usePrivy();
138137
const { wallets } = useWallets();
139138
const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy') || wallets[0];
140139

141140
const state = useSelector(componentStore, (state) => state.context);
142141

143142
const { isPending: privateSpacesPending, error: privateSpacesError, data: privateSpacesData } = usePrivateSpaces();
144-
const {
145-
isPending: publicSpacesPending,
146-
error: publicSpacesError,
147-
data: publicSpacesData,
148-
} = usePublicSpaces(import.meta.env.VITE_HYPERGRAPH_API_URL);
149-
150-
const selectedPrivateSpaces = new Set<string>();
151-
const selectedPublicSpaces = new Set<string>();
152-
153-
const handlePrivateSpaceToggle = (spaceId: string, checked: boolean) => {
154-
if (checked) {
155-
selectedPrivateSpaces.add(spaceId);
156-
} else {
157-
selectedPrivateSpaces.delete(spaceId);
158-
}
159-
};
160-
161-
const handlePublicSpaceToggle = (spaceId: string, checked: boolean) => {
162-
if (checked) {
163-
selectedPublicSpaces.add(spaceId);
164-
} else {
165-
selectedPublicSpaces.delete(spaceId);
166-
}
167-
};
143+
const { data: publicSpacesData } = usePublicSpaces(import.meta.env.VITE_HYPERGRAPH_API_URL);
168144

169145
useEffect(() => {
170146
const run = async () => {
@@ -351,21 +327,6 @@ function AuthenticateComponent() {
351327
transport: custom(privyProvider),
352328
});
353329

354-
const signer: Identity.Signer = {
355-
getAddress: async () => {
356-
const [address] = await walletClient.getAddresses();
357-
return address;
358-
},
359-
signMessage: async (message: string) => {
360-
if (embeddedWallet.walletClientType === 'privy') {
361-
const { signature } = await signMessage({ message });
362-
return signature;
363-
}
364-
const [address] = await walletClient.getAddresses();
365-
return await walletClient.signMessage({ account: address, message });
366-
},
367-
};
368-
369330
const newAppIdentity = Connect.createAppIdentity();
370331

371332
console.log('creating smart session');
@@ -405,25 +366,13 @@ function AuthenticateComponent() {
405366
rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL,
406367
});
407368

408-
const appIdentityKeys = {
409-
encryptionPrivateKey: newAppIdentity.encryptionPrivateKey,
410-
encryptionPublicKey: newAppIdentity.encryptionPublicKey,
411-
signaturePrivateKey: newAppIdentity.signaturePrivateKey,
412-
signaturePublicKey: newAppIdentity.signaturePublicKey,
413-
};
414369
console.log('encrypting app identity');
415-
const { ciphertext, nonce } = await Connect.encryptAppIdentity(
416-
signer,
417-
newAppIdentity.address,
418-
newAppIdentity.addressPrivateKey,
419-
permissionId,
420-
appIdentityKeys,
421-
);
370+
const { ciphertext } = await Connect.encryptAppIdentity({ ...newAppIdentity, permissionId }, keys);
422371
console.log('proving ownership');
423372
const { accountProof, keyProof } = await Identity.proveIdentityOwnership(
424373
smartAccountClient,
425374
accountAddress,
426-
appIdentityKeys,
375+
newAppIdentity,
427376
);
428377

429378
const message: Messages.RequestConnectCreateAppIdentity = {
@@ -433,7 +382,6 @@ function AuthenticateComponent() {
433382
signaturePublicKey: newAppIdentity.signaturePublicKey,
434383
encryptionPublicKey: newAppIdentity.encryptionPublicKey,
435384
ciphertext,
436-
nonce,
437385
accountProof,
438386
keyProof,
439387
};
@@ -476,48 +424,16 @@ function AuthenticateComponent() {
476424
};
477425

478426
const decryptAppIdentityAndRedirect = async () => {
479-
if (!state.appIdentityResponse) {
427+
if (!state.appIdentityResponse || !keys) {
480428
return;
481429
}
482430

483-
const privyProvider = await embeddedWallet.getEthereumProvider();
484-
const walletClient = createWalletClient({
485-
account: embeddedWallet.address as `0x${string}`,
486-
chain: CHAIN,
487-
transport: custom(privyProvider),
488-
});
489-
490-
const signer: Identity.Signer = {
491-
getAddress: async () => {
492-
const [address] = await walletClient.getAddresses();
493-
return address;
494-
},
495-
signMessage: async (message: string) => {
496-
if (embeddedWallet.walletClientType === 'privy') {
497-
const { signature } = await signMessage({ message });
498-
return signature;
499-
}
500-
const [address] = await walletClient.getAddresses();
501-
return await walletClient.signMessage({ account: address, message });
502-
},
503-
};
504-
505-
const decryptedIdentity = await Connect.decryptAppIdentity(
506-
signer,
507-
state.appIdentityResponse.ciphertext,
508-
state.appIdentityResponse.nonce,
509-
);
431+
const decryptedIdentity = await Connect.decryptAppIdentity(state.appIdentityResponse.ciphertext, keys);
510432
await encryptSpacesAndRedirect({
511433
accountAddress: state.appIdentityResponse.accountAddress,
512434
appIdentity: {
513-
address: decryptedIdentity.address,
514-
addressPrivateKey: decryptedIdentity.addressPrivateKey,
435+
...decryptedIdentity,
515436
accountAddress: state.appIdentityResponse.accountAddress,
516-
permissionId: decryptedIdentity.permissionId,
517-
encryptionPrivateKey: decryptedIdentity.encryptionPrivateKey,
518-
signaturePrivateKey: decryptedIdentity.signaturePrivateKey,
519-
encryptionPublicKey: decryptedIdentity.encryptionPublicKey,
520-
signaturePublicKey: decryptedIdentity.signaturePublicKey,
521437
sessionToken: state.appIdentityResponse.sessionToken,
522438
sessionTokenExpires: new Date(state.appIdentityResponse.sessionTokenExpires),
523439
},
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `nonce` on the `AppIdentity` table. All the data in the column will be lost.
5+
6+
*/
7+
-- RedefineTables
8+
PRAGMA defer_foreign_keys=ON;
9+
PRAGMA foreign_keys=OFF;
10+
CREATE TABLE "new_AppIdentity" (
11+
"address" TEXT NOT NULL PRIMARY KEY,
12+
"ciphertext" TEXT NOT NULL,
13+
"signaturePublicKey" TEXT NOT NULL,
14+
"encryptionPublicKey" TEXT NOT NULL,
15+
"accountProof" TEXT NOT NULL,
16+
"keyProof" TEXT NOT NULL,
17+
"accountAddress" TEXT NOT NULL,
18+
"appId" TEXT NOT NULL,
19+
"sessionToken" TEXT NOT NULL,
20+
"sessionTokenExpires" DATETIME NOT NULL,
21+
CONSTRAINT "AppIdentity_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "Account" ("address") ON DELETE RESTRICT ON UPDATE CASCADE
22+
);
23+
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";
24+
DROP TABLE "AppIdentity";
25+
ALTER TABLE "new_AppIdentity" RENAME TO "AppIdentity";
26+
CREATE INDEX "AppIdentity_sessionToken_idx" ON "AppIdentity"("sessionToken");
27+
CREATE UNIQUE INDEX "AppIdentity_accountAddress_appId_key" ON "AppIdentity"("accountAddress", "appId");
28+
PRAGMA foreign_keys=ON;
29+
PRAGMA defer_foreign_keys=OFF;

apps/server/prisma/schema.prisma

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ model Account {
115115
model AppIdentity {
116116
address String @id
117117
ciphertext String
118-
nonce String
119118
signaturePublicKey String
120119
encryptionPublicKey String
121120
accountProof String
@@ -129,7 +128,6 @@ model AppIdentity {
129128
sessionTokenExpires DateTime
130129
131130
@@unique([accountAddress, appId])
132-
@@unique([accountAddress, nonce])
133131
@@index([sessionToken])
134132
}
135133

apps/server/src/handlers/create-app-identity.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export const createAppIdentity = async ({
1919
address,
2020
appId,
2121
ciphertext,
22-
nonce,
2322
signaturePublicKey,
2423
encryptionPublicKey,
2524
accountProof,
@@ -43,7 +42,6 @@ export const createAppIdentity = async ({
4342
accountAddress,
4443
appId,
4544
ciphertext,
46-
nonce,
4745
signaturePublicKey,
4846
encryptionPublicKey,
4947
accountProof,

apps/server/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,6 @@ app.post('/connect/app-identity', async (req, res) => {
340340
appId: message.appId,
341341
address: message.address,
342342
ciphertext: message.ciphertext,
343-
nonce: message.nonce,
344343
signaturePublicKey: message.signaturePublicKey,
345344
encryptionPublicKey: message.encryptionPublicKey,
346345
accountProof: message.accountProof,

packages/hypergraph/src/connect/identity-encryption.ts

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import { sha256 } from '@noble/hashes/sha256';
55
import type { Hex } from 'viem';
66
import { verifyMessage } from 'viem';
77

8+
import { cryptoBoxSeal, cryptoBoxSealOpen } from '@serenity-kit/noble-sodium';
89
import { bytesToHex, canonicalize, hexToBytes } from '../utils/index.js';
910
import type { IdentityKeys, PrivateAppIdentity, Signer } from './types.js';
1011

12+
export type AppIdentityForEncryption = Omit<
13+
PrivateAppIdentity,
14+
'sessionToken' | 'sessionTokenExpires' | 'accountAddress'
15+
>;
16+
1117
// Adapted from the XMTP approach to encrypt keys
1218
// See: https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L79-L116
1319
// (We reimplement their encrypt/decrypt functions using noble).
@@ -134,95 +140,54 @@ export const decryptIdentity = async (signer: Signer, ciphertext: string, nonce:
134140
};
135141

136142
export const encryptAppIdentity = async (
137-
signer: Signer,
138-
appIdentityAddress: string,
139-
appIdentityAddressPrivateKey: string,
140-
permissionId: string,
143+
appIdentity: AppIdentityForEncryption,
141144
keys: IdentityKeys,
142-
): Promise<{ ciphertext: string; nonce: string }> => {
143-
const nonce = randomBytes(32);
144-
const message = signatureMessage(nonce);
145-
const signature = (await signer.signMessage(message)) as Hex;
146-
147-
// Check that the signature is valid
148-
const valid = await verifyMessage({
149-
address: (await signer.getAddress()) as Hex,
150-
message,
151-
signature,
152-
});
153-
if (!valid) {
154-
throw new Error('Invalid signature');
155-
}
156-
const secretKey = hexToBytes(signature);
145+
): Promise<{ ciphertext: string }> => {
157146
// We use a simple plaintext encoding:
158147
// Hex keys separated by newlines
159148
const keysTxt = [
160-
keys.encryptionPublicKey,
161-
keys.encryptionPrivateKey,
162-
keys.signaturePublicKey,
163-
keys.signaturePrivateKey,
164-
appIdentityAddress,
165-
appIdentityAddressPrivateKey,
166-
permissionId,
149+
appIdentity.encryptionPublicKey,
150+
appIdentity.encryptionPrivateKey,
151+
appIdentity.signaturePublicKey,
152+
appIdentity.signaturePrivateKey,
153+
appIdentity.address,
154+
appIdentity.addressPrivateKey,
155+
appIdentity.permissionId,
167156
].join('\n');
168157
const keysMsg = new TextEncoder().encode(keysTxt);
169-
const ciphertext = encrypt(keysMsg, secretKey);
170-
return { ciphertext, nonce: bytesToHex(nonce) };
158+
const ciphertext = bytesToHex(
159+
cryptoBoxSeal({
160+
message: keysMsg,
161+
publicKey: hexToBytes(keys.encryptionPublicKey),
162+
}),
163+
);
164+
return { ciphertext };
171165
};
172166

173-
export const decryptAppIdentity = async (
174-
signer: Signer,
175-
ciphertext: string,
176-
nonce: string,
177-
): Promise<Omit<PrivateAppIdentity, 'sessionToken' | 'sessionTokenExpires' | 'accountAddress'>> => {
178-
const message = signatureMessage(hexToBytes(nonce));
179-
const signature = (await signer.signMessage(message)) as Hex;
180-
181-
// Check that the signature is valid
182-
const valid = await verifyMessage({
183-
address: (await signer.getAddress()) as Hex,
184-
message,
185-
signature,
167+
export const decryptAppIdentity = async (ciphertext: string, keys: IdentityKeys): Promise<AppIdentityForEncryption> => {
168+
const ciphertextBytes = hexToBytes(ciphertext);
169+
const keysMsg = cryptoBoxSealOpen({
170+
ciphertext: ciphertextBytes,
171+
privateKey: hexToBytes(keys.encryptionPrivateKey),
172+
publicKey: hexToBytes(keys.encryptionPublicKey),
186173
});
187-
if (!valid) {
188-
throw new Error('Invalid signature');
189-
}
190-
const secretKey = hexToBytes(signature);
191-
let keysMsg: Uint8Array;
192-
try {
193-
keysMsg = await decrypt(ciphertext, secretKey);
194-
} catch (e) {
195-
// See https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L142-L146
196-
if (secretKey.length !== 65) {
197-
throw new Error('Expected 65 bytes before trying a different recovery byte');
198-
}
199-
// Try the other version of recovery byte, either +27 or -27
200-
const lastByte = secretKey[secretKey.length - 1];
201-
let newSecret = secretKey.slice(0, secretKey.length - 1);
202-
if (lastByte < 27) {
203-
newSecret = new Uint8Array([...newSecret, lastByte + 27]);
204-
} else {
205-
newSecret = new Uint8Array([...newSecret, lastByte - 27]);
206-
}
207-
keysMsg = await decrypt(ciphertext, newSecret);
208-
}
209174
const keysTxt = new TextDecoder().decode(keysMsg);
210175
const [
211176
encryptionPublicKey,
212177
encryptionPrivateKey,
213178
signaturePublicKey,
214179
signaturePrivateKey,
215-
appIdentityAddress,
216-
appIdentityAddressPrivateKey,
180+
address,
181+
addressPrivateKey,
217182
permissionId,
218183
] = keysTxt.split('\n');
219184
return {
220185
encryptionPublicKey,
221186
encryptionPrivateKey,
222187
signaturePublicKey,
223188
signaturePrivateKey,
224-
address: appIdentityAddress,
225-
addressPrivateKey: appIdentityAddressPrivateKey,
189+
address,
190+
addressPrivateKey,
226191
permissionId,
227192
};
228193
};

packages/hypergraph/src/connect/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export const AppIdentityResponse = Schema.Struct({
3636
accountProof: Schema.String,
3737
keyProof: Schema.String,
3838
ciphertext: Schema.String,
39-
nonce: Schema.String,
4039
sessionToken: Schema.String,
4140
address: Schema.String,
4241
appId: Schema.String,

packages/hypergraph/src/messages/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,6 @@ export const RequestConnectCreateAppIdentity = Schema.Struct({
245245
address: Schema.String,
246246
accountAddress: Schema.String,
247247
ciphertext: Schema.String,
248-
nonce: Schema.String,
249248
signaturePublicKey: Schema.String,
250249
encryptionPublicKey: Schema.String,
251250
accountProof: Schema.String,

0 commit comments

Comments
 (0)