Skip to content

Commit 6e24559

Browse files
tsmblAlexey Tsymbal
andauthored
Support e2e encryption using curve25519 keypair (#115)
* Change keypair type in ecdh encryption util * Use Ed25519Key in ecdh utils to represent other party * Add encryption props to text serde * Add encryption props to api * Fix tests after switching to dh keypair Co-authored-by: Alexey Tsymbal <[email protected]>
1 parent 8f5352e commit 6e24559

File tree

5 files changed

+360
-231
lines changed

5 files changed

+360
-231
lines changed

src/api/index.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { sleep, waitForFinality, Wallet_ } from '../utils';
77
import { ENCRYPTION_OVERHEAD_BYTES } from '../utils/ecdh-encryption';
88
import { CyclicByteBuffer } from '../utils/cyclic-bytebuffer';
99
import ByteBuffer from 'bytebuffer';
10-
import { TextSerdeFactory } from './text-serde';
10+
import { EncryptionProps, TextSerdeFactory } from './text-serde';
1111

1212
// TODO: Switch from types to classes
1313

@@ -254,9 +254,9 @@ export async function getDialectProgramAddress(
254254

255255
function parseMessages(
256256
{ messages: rawMessagesBuffer, members, encrypted }: RawDialect,
257-
user?: anchor.web3.Keypair,
257+
encryptionProps?: EncryptionProps,
258258
) {
259-
if (encrypted && !user) {
259+
if (encrypted && !encryptionProps) {
260260
return [];
261261
}
262262
const messagesBuffer = new CyclicByteBuffer(
@@ -270,7 +270,7 @@ function parseMessages(
270270
encrypted,
271271
members,
272272
},
273-
user,
273+
encryptionProps,
274274
);
275275
const allMessages: Message[] = messagesBuffer.items().map(({ buffer }) => {
276276
const byteBuffer = new ByteBuffer(buffer.length).append(buffer).flip();
@@ -288,29 +288,29 @@ function parseMessages(
288288
return allMessages.reverse();
289289
}
290290

291-
function parseRawDialect(rawDialect: RawDialect, user?: anchor.web3.Keypair) {
291+
function parseRawDialect(
292+
rawDialect: RawDialect,
293+
encryptionProps?: EncryptionProps,
294+
) {
292295
return {
293296
encrypted: rawDialect.encrypted,
294297
members: rawDialect.members,
295298
nextMessageIdx: rawDialect.messages.writeOffset,
296299
lastMessageTimestamp: rawDialect.lastMessageTimestamp * 1000,
297-
messages: parseMessages(rawDialect, user),
300+
messages: parseMessages(rawDialect, encryptionProps),
298301
};
299302
}
300303

301304
export async function getDialect(
302305
program: anchor.Program,
303306
publicKey: PublicKey,
304-
user?: anchor.web3.Keypair | Wallet,
307+
encryptionProps?: EncryptionProps,
305308
): Promise<DialectAccount> {
306309
const rawDialect = (await program.account.dialectAccount.fetch(
307310
publicKey,
308311
)) as RawDialect;
309312
const account = await program.provider.connection.getAccountInfo(publicKey);
310-
const dialect = parseRawDialect(
311-
rawDialect,
312-
user && 'secretKey' in user ? user : undefined,
313-
);
313+
const dialect = parseRawDialect(rawDialect, encryptionProps);
314314
return {
315315
...account,
316316
publicKey: publicKey,
@@ -321,14 +321,15 @@ export async function getDialect(
321321
export async function getDialects(
322322
program: anchor.Program,
323323
user: anchor.web3.Keypair | Wallet,
324+
encryptionProps?: EncryptionProps,
324325
): Promise<DialectAccount[]> {
325326
const metadata = await getMetadata(program, user.publicKey);
326327
const enabledSubscriptions = metadata.subscriptions.filter(
327328
(it) => it.enabled,
328329
);
329330
return Promise.all(
330331
enabledSubscriptions.map(async ({ pubkey }) =>
331-
getDialect(program, pubkey, user),
332+
getDialect(program, pubkey, encryptionProps),
332333
),
333334
).then((dialects) =>
334335
dialects.sort(
@@ -341,13 +342,13 @@ export async function getDialects(
341342
export async function getDialectForMembers(
342343
program: anchor.Program,
343344
members: Member[],
344-
user?: anchor.web3.Keypair,
345+
encryptionProps?: EncryptionProps,
345346
): Promise<DialectAccount> {
346347
const sortedMembers = members.sort((a, b) =>
347348
a.publicKey.toBuffer().compare(b.publicKey.toBuffer()),
348349
);
349350
const [publicKey] = await getDialectProgramAddress(program, sortedMembers);
350-
return await getDialect(program, publicKey, user);
351+
return await getDialect(program, publicKey, encryptionProps);
351352
}
352353

353354
export async function findDialects(
@@ -395,7 +396,8 @@ export async function createDialect(
395396
program: anchor.Program,
396397
owner: anchor.web3.Keypair | Wallet,
397398
members: Member[],
398-
encrypted = true,
399+
encrypted = false,
400+
encryptionProps?: EncryptionProps,
399401
): Promise<DialectAccount> {
400402
const sortedMembers = members.sort((a, b) =>
401403
a.publicKey.toBuffer().compare(b.publicKey.toBuffer()),
@@ -425,11 +427,7 @@ export async function createDialect(
425427
},
426428
);
427429
await waitForFinality(program, tx);
428-
return await getDialectForMembers(
429-
program,
430-
members,
431-
'secretKey' in owner ? owner : undefined,
432-
);
430+
return await getDialectForMembers(program, members, encryptionProps);
433431
}
434432

435433
export async function deleteDialect(
@@ -470,6 +468,7 @@ export async function sendMessage(
470468
{ dialect, publicKey }: DialectAccount,
471469
sender: anchor.web3.Keypair | Wallet,
472470
text: string,
471+
encryptionProps?: EncryptionProps,
473472
): Promise<Message> {
474473
const [dialectPublicKey, nonce] = await getDialectProgramAddress(
475474
program,
@@ -480,7 +479,7 @@ export async function sendMessage(
480479
encrypted: dialect.encrypted,
481480
members: dialect.members,
482481
},
483-
sender && 'secretKey' in sender ? sender : undefined,
482+
encryptionProps,
484483
);
485484
const serializedText = textSerde.serialize(text);
486485
await program.rpc.sendMessage(
@@ -498,7 +497,7 @@ export async function sendMessage(
498497
signers: sender && 'secretKey' in sender ? [sender] : [],
499498
},
500499
);
501-
const d = await getDialect(program, publicKey, sender);
500+
const d = await getDialect(program, publicKey, encryptionProps);
502501
return d.dialect.messages[d.dialect.nextMessageIdx - 1]; // TODO: Support ring
503502
}
504503

src/api/text-serde.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ import {
33
generateRandomNonceWithPrefix,
44
NONCE_SIZE_BYTES,
55
} from '../utils/nonce-generator';
6-
import { ecdhDecrypt, ecdhEncrypt } from '../utils/ecdh-encryption';
6+
import {
7+
Curve25519KeyPair,
8+
ecdhDecrypt,
9+
ecdhEncrypt,
10+
Ed25519Key,
11+
} from '../utils/ecdh-encryption';
712
import * as anchor from '@project-serum/anchor';
13+
import { PublicKey } from '@solana/web3.js';
814

915
export interface TextSerde {
1016
serialize(text: string): Uint8Array;
@@ -17,37 +23,35 @@ export class EncryptedTextSerde implements TextSerde {
1723
new UnencryptedTextSerde();
1824

1925
constructor(
20-
private readonly user: anchor.web3.Keypair,
26+
private readonly encryptionProps: EncryptionProps,
2127
private readonly members: Member[],
2228
) {}
2329

2430
deserialize(bytes: Uint8Array): string {
2531
const encryptionNonce = bytes.slice(0, NONCE_SIZE_BYTES);
2632
const encryptedText = bytes.slice(NONCE_SIZE_BYTES, bytes.length);
27-
const otherMember = this.findOtherMember(this.user.publicKey);
33+
const otherMember = this.findOtherMember(
34+
new PublicKey(this.encryptionProps.ed25519PublicKey),
35+
);
2836
const encodedText = ecdhDecrypt(
2937
encryptedText,
30-
{
31-
secretKey: this.user.secretKey,
32-
publicKey: this.user.publicKey.toBytes(),
33-
},
34-
otherMember.publicKey.toBuffer(),
38+
this.encryptionProps.diffieHellmanKeyPair,
39+
otherMember.publicKey.toBytes(),
3540
encryptionNonce,
3641
);
3742
return this.unencryptedTextSerde.deserialize(encodedText);
3843
}
3944

4045
serialize(text: string): Uint8Array {
41-
const senderMemberIdx = this.findMemberIdx(this.user.publicKey);
46+
const publicKey = new PublicKey(this.encryptionProps.ed25519PublicKey);
47+
const senderMemberIdx = this.findMemberIdx(publicKey);
4248
const textBytes = this.unencryptedTextSerde.serialize(text);
43-
const otherMember = this.findOtherMember(this.user.publicKey);
49+
const otherMember = this.findOtherMember(publicKey);
4450
const encryptionNonce = generateRandomNonceWithPrefix(senderMemberIdx);
4551
const encryptedText = ecdhEncrypt(
4652
textBytes,
47-
{
48-
secretKey: this.user.secretKey,
49-
publicKey: this.user.publicKey.toBytes(),
50-
},
53+
this.encryptionProps.diffieHellmanKeyPair,
54+
5155
otherMember.publicKey.toBytes(),
5256
encryptionNonce,
5357
);
@@ -88,17 +92,22 @@ export type DialectAttributes = {
8892
members: Member[];
8993
};
9094

95+
export interface EncryptionProps {
96+
diffieHellmanKeyPair: Curve25519KeyPair;
97+
ed25519PublicKey: Ed25519Key;
98+
}
99+
91100
export class TextSerdeFactory {
92101
static create(
93102
{ encrypted, members }: DialectAttributes,
94-
user?: anchor.web3.Keypair,
103+
encryptionProps?: EncryptionProps,
95104
): TextSerde {
96105
if (!encrypted) {
97106
return new UnencryptedTextSerde();
98107
}
99-
if (encrypted && user) {
100-
return new EncryptedTextSerde(user, members);
108+
if (encrypted && encryptionProps) {
109+
return new EncryptedTextSerde(encryptionProps, members);
101110
}
102-
throw new Error('Cannot proceed with encrypted dialect w/o user identity');
111+
throw new Error('Cannot proceed without encryptionProps');
103112
}
104113
}

src/utils/ecdh-encryption.spec.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import { expect } from 'chai';
22
import {
3+
Curve25519KeyPair,
34
ecdhDecrypt,
45
ecdhEncrypt,
56
ENCRYPTION_OVERHEAD_BYTES,
6-
generateEd25519KeyPair,
77
} from './ecdh-encryption';
88
import { randomBytes } from 'tweetnacl';
99
import { NONCE_SIZE_BYTES } from './nonce-generator';
10+
import { Keypair } from '@solana/web3.js';
11+
import ed2curve from 'ed2curve';
12+
13+
function generateKeypair() {
14+
const { publicKey, secretKey } = new Keypair();
15+
const curve25519: Curve25519KeyPair = ed2curve.convertKeyPair({
16+
publicKey: publicKey.toBytes(),
17+
secretKey,
18+
})!;
19+
return {
20+
ed25519: {
21+
publicKey: publicKey.toBytes(),
22+
secretKey,
23+
},
24+
curve25519,
25+
};
26+
}
1027

1128
describe('ECDH encryptor/decryptor test', async () => {
1229
/*
@@ -25,10 +42,13 @@ describe('ECDH encryptor/decryptor test', async () => {
2542
const sizesComparison = messageSizes.map((size) => {
2643
const unencrypted = randomBytes(size);
2744
const nonce = randomBytes(NONCE_SIZE_BYTES);
45+
const keyPair1 = generateKeypair();
46+
const keyPair2 = generateKeypair();
47+
2848
const encrypted = ecdhEncrypt(
2949
unencrypted,
30-
generateEd25519KeyPair(),
31-
generateEd25519KeyPair().publicKey,
50+
keyPair1.curve25519,
51+
keyPair2.ed25519.publicKey,
3252
nonce,
3353
);
3454
return {
@@ -47,19 +67,19 @@ describe('ECDH encryptor/decryptor test', async () => {
4767
// given
4868
const unencrypted = randomBytes(10);
4969
const nonce = randomBytes(NONCE_SIZE_BYTES);
50-
const party1KeyPair = generateEd25519KeyPair();
51-
const party2KeyPair = generateEd25519KeyPair();
70+
const party1KeyPair = generateKeypair();
71+
const party2KeyPair = generateKeypair();
5272
const encrypted = ecdhEncrypt(
5373
unencrypted,
54-
party1KeyPair,
55-
party2KeyPair.publicKey,
74+
party1KeyPair.curve25519,
75+
party2KeyPair.ed25519.publicKey,
5676
nonce,
5777
);
5878
// when
5979
const decrypted = ecdhDecrypt(
6080
encrypted,
61-
party1KeyPair,
62-
party2KeyPair.publicKey,
81+
party1KeyPair.curve25519,
82+
party2KeyPair.ed25519.publicKey,
6383
nonce,
6484
);
6585
// then
@@ -71,19 +91,19 @@ describe('ECDH encryptor/decryptor test', async () => {
7191
// given
7292
const unencrypted = randomBytes(10);
7393
const nonce = randomBytes(NONCE_SIZE_BYTES);
74-
const party1KeyPair = generateEd25519KeyPair();
75-
const party2KeyPair = generateEd25519KeyPair();
94+
const party1KeyPair = generateKeypair();
95+
const party2KeyPair = generateKeypair();
7696
const encrypted = ecdhEncrypt(
7797
unencrypted,
78-
party1KeyPair,
79-
party2KeyPair.publicKey,
98+
party1KeyPair.curve25519,
99+
party2KeyPair.ed25519.publicKey,
80100
nonce,
81101
);
82102
// when
83103
const decrypted = ecdhDecrypt(
84104
encrypted,
85-
party2KeyPair,
86-
party1KeyPair.publicKey,
105+
party2KeyPair.curve25519,
106+
party1KeyPair.ed25519.publicKey,
87107
nonce,
88108
);
89109
// then

0 commit comments

Comments
 (0)