Skip to content

Commit 141eb28

Browse files
pcarranzavnikgraf
andauthored
fix: use Connect encryption key to encrypt app identities (#275)
Co-authored-by: Nik Graf <[email protected]>
1 parent b52c68c commit 141eb28

File tree

8 files changed

+71
-145
lines changed

8 files changed

+71
-145
lines changed

apps/connect/src/routes/authenticate.tsx

Lines changed: 10 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { createWalletClient, custom } from 'viem';
1616
import { privateKeyToAccount } from 'viem/accounts';
1717

1818
const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET;
19+
const API_URL =
20+
import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis'
21+
? `${Graph.MAINNET_API_ORIGIN}/graphql`
22+
: `${Graph.TESTNET_API_ORIGIN}/graphql`;
1923

2024
type AuthenticateSearch = {
2125
data: unknown;
@@ -135,19 +139,14 @@ function AuthenticateComponent() {
135139
const accountAddress = useSelector(StoreConnect.store, (state) => state.context.accountAddress);
136140
const keys = useSelector(StoreConnect.store, (state) => state.context.keys);
137141

138-
const { signMessage } = usePrivy();
139142
const { wallets } = useWallets();
140143
const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy') || wallets[0];
141144

142145
const state = useSelector(componentStore, (state) => state.context);
143146
const [selectedPrivateSpaces, setSelectedPrivateSpaces] = useState<Set<string>>(new Set());
144147

145148
const { isPending: privateSpacesPending, error: privateSpacesError, data: privateSpacesData } = usePrivateSpaces();
146-
const {
147-
isPending: publicSpacesPending,
148-
error: publicSpacesError,
149-
data: publicSpacesData,
150-
} = usePublicSpaces(`${Graph.TESTNET_API_ORIGIN}/graphql`);
149+
const { data: publicSpacesData } = usePublicSpaces(API_URL);
151150

152151
useEffect(() => {
153152
const run = async () => {
@@ -334,21 +333,6 @@ function AuthenticateComponent() {
334333
transport: custom(privyProvider),
335334
});
336335

337-
const signer: Identity.Signer = {
338-
getAddress: async () => {
339-
const [address] = await walletClient.getAddresses();
340-
return address;
341-
},
342-
signMessage: async (message: string) => {
343-
if (embeddedWallet.walletClientType === 'privy') {
344-
const { signature } = await signMessage({ message });
345-
return signature;
346-
}
347-
const [address] = await walletClient.getAddresses();
348-
return await walletClient.signMessage({ account: address, message });
349-
},
350-
};
351-
352336
const newAppIdentity = Connect.createAppIdentity();
353337

354338
console.log('creating smart session');
@@ -388,25 +372,13 @@ function AuthenticateComponent() {
388372
rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL,
389373
});
390374

391-
const appIdentityKeys = {
392-
encryptionPrivateKey: newAppIdentity.encryptionPrivateKey,
393-
encryptionPublicKey: newAppIdentity.encryptionPublicKey,
394-
signaturePrivateKey: newAppIdentity.signaturePrivateKey,
395-
signaturePublicKey: newAppIdentity.signaturePublicKey,
396-
};
397375
console.log('encrypting app identity');
398-
const { ciphertext, nonce } = await Connect.encryptAppIdentity(
399-
signer,
400-
newAppIdentity.address,
401-
newAppIdentity.addressPrivateKey,
402-
permissionId,
403-
appIdentityKeys,
404-
);
376+
const { ciphertext } = await Connect.encryptAppIdentity({ ...newAppIdentity, permissionId }, keys);
405377
console.log('proving ownership');
406378
const { accountProof, keyProof } = await Identity.proveIdentityOwnership(
407379
smartAccountClient,
408380
accountAddress,
409-
appIdentityKeys,
381+
newAppIdentity,
410382
);
411383

412384
const message: Messages.RequestConnectCreateAppIdentity = {
@@ -416,7 +388,6 @@ function AuthenticateComponent() {
416388
signaturePublicKey: keys.signaturePublicKey,
417389
encryptionPublicKey: keys.encryptionPublicKey,
418390
ciphertext,
419-
nonce,
420391
accountProof,
421392
keyProof,
422393
};
@@ -459,48 +430,16 @@ function AuthenticateComponent() {
459430
};
460431

461432
const decryptAppIdentityAndRedirect = async () => {
462-
if (!state.appIdentityResponse) {
433+
if (!state.appIdentityResponse || !keys) {
463434
return;
464435
}
465436

466-
const privyProvider = await embeddedWallet.getEthereumProvider();
467-
const walletClient = createWalletClient({
468-
account: embeddedWallet.address as `0x${string}`,
469-
chain: CHAIN,
470-
transport: custom(privyProvider),
471-
});
472-
473-
const signer: Identity.Signer = {
474-
getAddress: async () => {
475-
const [address] = await walletClient.getAddresses();
476-
return address;
477-
},
478-
signMessage: async (message: string) => {
479-
if (embeddedWallet.walletClientType === 'privy') {
480-
const { signature } = await signMessage({ message });
481-
return signature;
482-
}
483-
const [address] = await walletClient.getAddresses();
484-
return await walletClient.signMessage({ account: address, message });
485-
},
486-
};
487-
488-
const decryptedIdentity = await Connect.decryptAppIdentity(
489-
signer,
490-
state.appIdentityResponse.ciphertext,
491-
state.appIdentityResponse.nonce,
492-
);
437+
const decryptedIdentity = await Connect.decryptAppIdentity(state.appIdentityResponse.ciphertext, keys);
493438
await encryptSpacesAndRedirect({
494439
accountAddress: state.appIdentityResponse.accountAddress,
495440
appIdentity: {
496-
address: decryptedIdentity.address,
497-
addressPrivateKey: decryptedIdentity.addressPrivateKey,
441+
...decryptedIdentity,
498442
accountAddress: state.appIdentityResponse.accountAddress,
499-
permissionId: decryptedIdentity.permissionId,
500-
encryptionPrivateKey: decryptedIdentity.encryptionPrivateKey,
501-
signaturePrivateKey: decryptedIdentity.signaturePrivateKey,
502-
encryptionPublicKey: decryptedIdentity.encryptionPublicKey,
503-
signaturePublicKey: decryptedIdentity.signaturePublicKey,
504443
sessionToken: state.appIdentityResponse.sessionToken,
505444
sessionTokenExpires: new Date(state.appIdentityResponse.sessionTokenExpires),
506445
},
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)