chatly-sdk 0.0.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  import type { User } from "../models/user.js";
2
2
  import type { Message } from "../models/message.js";
3
3
  import type { MediaAttachment } from "../models/mediaTypes.js";
4
- import { deriveSharedSecret, encryptMessage, decryptMessage } from "../crypto/e2e.js";
4
+ import { deriveSharedSecret, deriveLegacySharedSecret, encryptMessage, decryptMessage } from "../crypto/e2e.js";
5
5
  import type { KeyPair } from "../crypto/keys.js";
6
6
  import { generateUUID } from "../crypto/uuid.js";
7
+ import type { StorageProvider } from "../storage/adapters.js";
8
+ import { logger } from "../utils/logger.js";
7
9
 
8
10
  export class ChatSession {
9
11
  private sharedSecret: Buffer | null = null;
@@ -12,7 +14,8 @@ export class ChatSession {
12
14
  constructor(
13
15
  public readonly id: string,
14
16
  public readonly userA: User,
15
- public readonly userB: User
17
+ public readonly userB: User,
18
+ private storageProvider?: StorageProvider
16
19
  ) {}
17
20
 
18
21
  /**
@@ -41,6 +44,13 @@ export class ChatSession {
41
44
  privateKey: user.privateKey,
42
45
  };
43
46
 
47
+ logger.debug(`[ChatSession] Initializing for user ${user.id}`, {
48
+ hasLocalPriv: !!user.privateKey,
49
+ privType: typeof user.privateKey,
50
+ hasRemotePub: !!otherUser.publicKey,
51
+ pubType: typeof otherUser.publicKey
52
+ });
53
+
44
54
  this.sharedSecret = deriveSharedSecret(localKeyPair, otherUser.publicKey);
45
55
  }
46
56
 
@@ -88,9 +98,9 @@ export class ChatSession {
88
98
  // Encrypt the message text (could be caption)
89
99
  const { ciphertext, iv } = encryptMessage(plaintext, this.sharedSecret);
90
100
 
91
- // Encrypt the media data
92
- const { ciphertext: encryptedMediaData } = encryptMessage(
93
- media.data,
101
+ // Encrypt the media data with its own IV
102
+ const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
103
+ media.data || "",
94
104
  this.sharedSecret
95
105
  );
96
106
 
@@ -98,8 +108,24 @@ export class ChatSession {
98
108
  const encryptedMedia: MediaAttachment = {
99
109
  ...media,
100
110
  data: encryptedMediaData,
111
+ iv: mediaIv,
101
112
  };
102
113
 
114
+ // If storage provider is available, upload the encrypted data
115
+ if (this.storageProvider) {
116
+ const filename = `${this.id}/${generateUUID()}-${media.metadata.filename}`;
117
+ const uploadResult = await this.storageProvider.upload(
118
+ encryptedMediaData,
119
+ filename,
120
+ media.metadata.mimeType
121
+ );
122
+
123
+ encryptedMedia.storage = this.storageProvider.name as 'local' | 's3';
124
+ encryptedMedia.storageKey = uploadResult.storageKey;
125
+ encryptedMedia.url = uploadResult.url;
126
+ encryptedMedia.data = undefined; // Remove data from attachment if stored remotely
127
+ }
128
+
103
129
  return {
104
130
  id: generateUUID(),
105
131
  senderId,
@@ -127,7 +153,31 @@ export class ChatSession {
127
153
  throw new Error("Failed to initialize session");
128
154
  }
129
155
 
130
- return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
156
+ try {
157
+ return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
158
+ } catch (error) {
159
+ // Fallback for legacy messages (before salt logic)
160
+ const legacySecret = this.deriveLegacySecret(user);
161
+ try {
162
+ return decryptMessage(message.ciphertext, message.iv, legacySecret);
163
+ } catch (innerError) {
164
+ throw error; // Throw original error if fallback also fails
165
+ }
166
+ }
167
+ }
168
+
169
+ private deriveLegacySecret(user: User): Buffer {
170
+ const otherUser = user.id === this.userA.id ? this.userB : this.userA;
171
+ logger.debug(`[ChatSession] Deriving legacy secret for user ${user.id}`, {
172
+ hasPriv: !!user.privateKey,
173
+ privType: typeof user.privateKey,
174
+ remotePubType: typeof otherUser.publicKey
175
+ });
176
+ const localKeyPair: KeyPair = {
177
+ publicKey: user.publicKey,
178
+ privateKey: user.privateKey,
179
+ };
180
+ return deriveLegacySharedSecret(localKeyPair, otherUser.publicKey);
131
181
  }
132
182
 
133
183
  /**
@@ -151,12 +201,38 @@ export class ChatSession {
151
201
  // Decrypt the message text
152
202
  const text = decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
153
203
 
154
- // Decrypt the media data
155
- const decryptedMediaData = decryptMessage(
156
- message.media.data,
157
- message.iv,
158
- this.sharedSecret
159
- );
204
+ let encryptedMediaData = message.media.data;
205
+
206
+ // If data is missing but storageKey is present, download it
207
+ if (!encryptedMediaData && message.media.storageKey && this.storageProvider) {
208
+ encryptedMediaData = await this.storageProvider.download(message.media.storageKey);
209
+ }
210
+
211
+ // Decrypt the media data using its own IV
212
+ if (!message.media.iv && !encryptedMediaData) {
213
+ throw new Error("Media data or IV missing");
214
+ }
215
+
216
+ let decryptedMediaData: string;
217
+ try {
218
+ decryptedMediaData = decryptMessage(
219
+ encryptedMediaData || "",
220
+ message.media.iv || message.iv,
221
+ this.sharedSecret
222
+ );
223
+ } catch (error) {
224
+ // Fallback for legacy media
225
+ const legacySecret = this.deriveLegacySecret(user);
226
+ try {
227
+ decryptedMediaData = decryptMessage(
228
+ encryptedMediaData || "",
229
+ message.media.iv || message.iv,
230
+ legacySecret
231
+ );
232
+ } catch (innerError) {
233
+ throw error;
234
+ }
235
+ }
160
236
 
161
237
  // Create decrypted media attachment
162
238
  const decryptedMedia: MediaAttachment = {
@@ -5,11 +5,12 @@ import { deriveGroupKey } from "../crypto/group.js";
5
5
  import { encryptMessage, decryptMessage } from "../crypto/e2e.js";
6
6
  import { base64ToBuffer } from "../crypto/utils.js";
7
7
  import { generateUUID } from "../crypto/uuid.js";
8
+ import type { StorageProvider } from "../storage/adapters.js";
8
9
 
9
10
  export class GroupSession {
10
11
  private groupKey: Buffer | null = null;
11
12
 
12
- constructor(public readonly group: Group) {}
13
+ constructor(public readonly group: Group, private storageProvider?: StorageProvider) {}
13
14
 
14
15
  /**
15
16
  * Initialize the session by deriving the group key
@@ -63,9 +64,9 @@ export class GroupSession {
63
64
  // Encrypt the message text (could be caption)
64
65
  const { ciphertext, iv } = encryptMessage(plaintext, this.groupKey);
65
66
 
66
- // Encrypt the media data
67
- const { ciphertext: encryptedMediaData } = encryptMessage(
68
- media.data,
67
+ // Encrypt the media data with its own IV
68
+ const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
69
+ media.data || "",
69
70
  this.groupKey
70
71
  );
71
72
 
@@ -73,8 +74,24 @@ export class GroupSession {
73
74
  const encryptedMedia: MediaAttachment = {
74
75
  ...media,
75
76
  data: encryptedMediaData,
77
+ iv: mediaIv,
76
78
  };
77
79
 
80
+ // If storage provider is available, upload the encrypted data
81
+ if (this.storageProvider) {
82
+ const filename = `groups/${this.group.id}/${generateUUID()}-${media.metadata.filename}`;
83
+ const uploadResult = await this.storageProvider.upload(
84
+ encryptedMediaData,
85
+ filename,
86
+ media.metadata.mimeType
87
+ );
88
+
89
+ encryptedMedia.storage = this.storageProvider.name as 'local' | 's3';
90
+ encryptedMedia.storageKey = uploadResult.storageKey;
91
+ encryptedMedia.url = uploadResult.url;
92
+ encryptedMedia.data = undefined; // Remove data from attachment if stored remotely
93
+ }
94
+
78
95
  return {
79
96
  id: generateUUID(),
80
97
  senderId,
@@ -121,10 +138,21 @@ export class GroupSession {
121
138
  // Decrypt the message text
122
139
  const text = decryptMessage(message.ciphertext, message.iv, this.groupKey);
123
140
 
124
- // Decrypt the media data
141
+ let encryptedMediaData = message.media.data;
142
+
143
+ // If data is missing but storageKey is present, download it
144
+ if (!encryptedMediaData && message.media.storageKey && this.storageProvider) {
145
+ encryptedMediaData = await this.storageProvider.download(message.media.storageKey);
146
+ }
147
+
148
+ // Decrypt the media data using its own IV
149
+ if (!message.media.iv && !encryptedMediaData) {
150
+ throw new Error("Media data or IV missing");
151
+ }
152
+
125
153
  const decryptedMediaData = decryptMessage(
126
- message.media.data,
127
- message.iv,
154
+ encryptedMediaData || "",
155
+ message.media.iv || message.iv, // Fallback to message IV for backward compatibility
128
156
  this.groupKey
129
157
  );
130
158
 
package/src/crypto/e2e.ts CHANGED
@@ -33,6 +33,15 @@
33
33
  return derivedKey;
34
34
  }
35
35
 
36
+ /**
37
+ * Legacy secret derivation without salt (for backward compatibility)
38
+ */
39
+ export function deriveLegacySharedSecret(local: KeyPair, remotePublicKey: string): Buffer {
40
+ const ecdh = createECDH(SUPPORTED_CURVE);
41
+ ecdh.setPrivateKey(base64ToBuffer(local.privateKey));
42
+ return ecdh.computeSecret(base64ToBuffer(remotePublicKey));
43
+ }
44
+
36
45
  /**
37
46
  * Encrypt a message using AES-GCM
38
47
  */
@@ -2,6 +2,8 @@ export function bufferToBase64(buffer: Buffer): string {
2
2
  return buffer.toString("base64");
3
3
  }
4
4
 
5
- export function base64ToBuffer(data: string): Buffer {
5
+ export function base64ToBuffer(data: string | Buffer): Buffer {
6
+ if (Buffer.isBuffer(data)) return data;
7
+ if (!data) return Buffer.alloc(0);
6
8
  return Buffer.from(data, "base64");
7
9
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  GroupStoreAdapter,
10
10
  } from "./stores/adapters.js";
11
11
  import type { TransportAdapter } from "./transport/adapters.js";
12
+ import type { StorageProvider } from "./storage/adapters.js";
12
13
  import { ChatSession } from "./chat/ChatSession.js";
13
14
  import { GroupSession } from "./chat/GroupSession.js";
14
15
  import { generateIdentityKeyPair } from "./crypto/keys.js";
@@ -23,6 +24,7 @@ export interface ChatSDKConfig {
23
24
  userStore: UserStoreAdapter;
24
25
  messageStore: MessageStoreAdapter;
25
26
  groupStore: GroupStoreAdapter;
27
+ storageProvider?: StorageProvider;
26
28
  transport?: TransportAdapter;
27
29
  logLevel?: LogLevel;
28
30
  }
@@ -213,7 +215,7 @@ export class ChatSDK extends EventEmitter {
213
215
  const sessionId = `${ids[0]}-${ids[1]}`;
214
216
 
215
217
  try {
216
- const session = new ChatSession(sessionId, userA, userB);
218
+ const session = new ChatSession(sessionId, userA, userB, this.config.storageProvider);
217
219
  await session.initialize();
218
220
  logger.info('Chat session created', { sessionId, users: [userA.id, userB.id] });
219
221
  this.emit(EVENTS.SESSION_CREATED, session);
@@ -245,7 +247,7 @@ export class ChatSDK extends EventEmitter {
245
247
 
246
248
  try {
247
249
  await this.config.groupStore.create(group);
248
- const session = new GroupSession(group);
250
+ const session = new GroupSession(group, this.config.storageProvider);
249
251
  await session.initialize();
250
252
  logger.info('Group created', { groupId: group.id, name: group.name, memberCount: members.length });
251
253
  this.emit(EVENTS.GROUP_CREATED, session);
@@ -272,7 +274,7 @@ export class ChatSDK extends EventEmitter {
272
274
  throw new SessionError(`Group not found: ${id}`, { groupId: id });
273
275
  }
274
276
 
275
- const session = new GroupSession(group);
277
+ const session = new GroupSession(group, this.config.storageProvider);
276
278
  await session.initialize();
277
279
  logger.debug('Group loaded', { groupId: id });
278
280
  return session;
@@ -437,7 +439,7 @@ export class ChatSDK extends EventEmitter {
437
439
  // Create consistent session ID
438
440
  const ids = [user.id, otherUser.id].sort();
439
441
  const sessionId = `${ids[0]}-${ids[1]}`;
440
- const session = new ChatSession(sessionId, user, otherUser);
442
+ const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
441
443
  await session.initializeForUser(user);
442
444
  return await session.decrypt(message, user);
443
445
  }
@@ -467,7 +469,7 @@ export class ChatSDK extends EventEmitter {
467
469
  if (!group) {
468
470
  throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
469
471
  }
470
- const session = new GroupSession(group);
472
+ const session = new GroupSession(group, this.config.storageProvider);
471
473
  await session.initialize();
472
474
  return await session.decryptMedia(message);
473
475
  } else {
@@ -486,7 +488,7 @@ export class ChatSDK extends EventEmitter {
486
488
  // Create consistent session ID
487
489
  const ids = [user.id, otherUser.id].sort();
488
490
  const sessionId = `${ids[0]}-${ids[1]}`;
489
- const session = new ChatSession(sessionId, user, otherUser);
491
+ const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
490
492
  await session.initializeForUser(user);
491
493
  return await session.decryptMedia(message, user);
492
494
  }
@@ -671,4 +673,7 @@ export * from "./utils/errors.js";
671
673
  export * from "./utils/logger.js";
672
674
  export * from "./utils/validation.js";
673
675
  export * from "./utils/mediaUtils.js";
676
+ export * from "./storage/adapters.js";
677
+ export * from "./storage/localStorage.js";
678
+ export * from "./storage/s3Storage.js";
674
679
  export * from "./constants.js";
@@ -26,8 +26,12 @@ export interface MediaMetadata {
26
26
  */
27
27
  export interface MediaAttachment {
28
28
  type: MediaType;
29
- data: string; // Base64 encoded file data
29
+ data?: string | undefined; // Base64 encoded file data (optional if stored remotely)
30
+ iv?: string | undefined; // Separate IV for media data encryption
30
31
  metadata: MediaMetadata;
32
+ storage?: 'local' | 's3' | undefined; // Storage provider used
33
+ storageKey?: string | undefined; // Key/path in the storage provider
34
+ url?: string | undefined; // Public URL if available
31
35
  }
32
36
 
33
37
  /**
@@ -0,0 +1,36 @@
1
+ export interface StorageUploadResult {
2
+ storageKey: string;
3
+ url?: string;
4
+ }
5
+
6
+ export interface StorageProvider {
7
+ /**
8
+ * Name of the storage provider (e.g., 'local', 's3')
9
+ */
10
+ readonly name: string;
11
+
12
+ /**
13
+ * Upload data to storage
14
+ * @param data Base64 encoded data or Buffer
15
+ * @param filename Desired filename or path
16
+ * @param mimeType MIME type of the file
17
+ */
18
+ upload(
19
+ data: string | Buffer,
20
+ filename: string,
21
+ mimeType: string
22
+ ): Promise<StorageUploadResult>;
23
+
24
+ /**
25
+ * Download data from storage
26
+ * @param storageKey Key/path of the file in storage
27
+ * @returns Base64 encoded data
28
+ */
29
+ download(storageKey: string): Promise<string>;
30
+
31
+ /**
32
+ * Delete data from storage
33
+ * @param storageKey Key/path of the file in storage
34
+ */
35
+ delete(storageKey: string): Promise<void>;
36
+ }
@@ -0,0 +1,49 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { StorageProvider, StorageUploadResult } from './adapters.js';
4
+
5
+ export class LocalStorageProvider implements StorageProvider {
6
+ public readonly name = 'local';
7
+ private storageDir: string;
8
+
9
+ constructor(storageDir: string = './storage') {
10
+ this.storageDir = path.resolve(storageDir);
11
+ // Ensure storage directory exists
12
+ fs.mkdir(this.storageDir, { recursive: true }).catch(() => {});
13
+ }
14
+
15
+ async upload(
16
+ data: string | Buffer,
17
+ filename: string,
18
+ mimeType: string
19
+ ): Promise<StorageUploadResult> {
20
+ const buffer = typeof data === 'string' ? Buffer.from(data, 'base64') : data;
21
+ const filePath = path.join(this.storageDir, filename);
22
+
23
+ // Ensure parent directories exist
24
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
25
+ await fs.writeFile(filePath, buffer);
26
+
27
+ return {
28
+ storageKey: filename,
29
+ url: `file://${filePath}`,
30
+ };
31
+ }
32
+
33
+ async download(storageKey: string): Promise<string> {
34
+ const filePath = path.join(this.storageDir, storageKey);
35
+ const buffer = await fs.readFile(filePath);
36
+ return buffer.toString('base64');
37
+ }
38
+
39
+ async delete(storageKey: string): Promise<void> {
40
+ const filePath = path.join(this.storageDir, storageKey);
41
+ try {
42
+ await fs.unlink(filePath);
43
+ } catch (error: any) {
44
+ if (error.code !== 'ENOENT') {
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,84 @@
1
+ import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
2
+ import { StorageProvider, StorageUploadResult } from './adapters.js';
3
+
4
+ export interface S3Config {
5
+ region: string;
6
+ bucket: string;
7
+ credentials?: {
8
+ accessKeyId: string;
9
+ secretAccessKey: string;
10
+ };
11
+ endpoint?: string;
12
+ forcePathStyle?: boolean;
13
+ }
14
+
15
+ export class S3StorageProvider implements StorageProvider {
16
+ public readonly name = 's3';
17
+ private client: S3Client;
18
+ private bucket: string;
19
+
20
+ constructor(config: S3Config) {
21
+ const s3Config: any = {
22
+ region: config.region,
23
+ forcePathStyle: config.forcePathStyle,
24
+ };
25
+
26
+ if (config.credentials) {
27
+ s3Config.credentials = config.credentials;
28
+ }
29
+
30
+ if (config.endpoint) {
31
+ s3Config.endpoint = config.endpoint;
32
+ }
33
+
34
+ this.client = new S3Client(s3Config);
35
+ this.bucket = config.bucket;
36
+ }
37
+
38
+ async upload(
39
+ data: string | Buffer,
40
+ filename: string,
41
+ mimeType: string
42
+ ): Promise<StorageUploadResult> {
43
+ const body = typeof data === 'string' ? Buffer.from(data, 'base64') : data;
44
+
45
+ await this.client.send(
46
+ new PutObjectCommand({
47
+ Bucket: this.bucket,
48
+ Key: filename,
49
+ Body: body,
50
+ ContentType: mimeType,
51
+ })
52
+ );
53
+
54
+ return {
55
+ storageKey: filename,
56
+ url: `https://${this.bucket}.s3.amazonaws.com/${filename}`,
57
+ };
58
+ }
59
+
60
+ async download(storageKey: string): Promise<string> {
61
+ const response = await this.client.send(
62
+ new GetObjectCommand({
63
+ Bucket: this.bucket,
64
+ Key: storageKey,
65
+ })
66
+ );
67
+
68
+ if (!response.Body) {
69
+ throw new Error('S3 download failed: empty body');
70
+ }
71
+
72
+ const bytes = await response.Body.transformToByteArray();
73
+ return Buffer.from(bytes).toString('base64');
74
+ }
75
+
76
+ async delete(storageKey: string): Promise<void> {
77
+ await this.client.send(
78
+ new DeleteObjectCommand({
79
+ Bucket: this.bucket,
80
+ Key: storageKey,
81
+ })
82
+ );
83
+ }
84
+ }
@@ -11,8 +11,10 @@ export interface UserStoreAdapter {
11
11
 
12
12
  export interface MessageStoreAdapter {
13
13
  create(message: Message): Promise<Message>;
14
+ findById(id: string): Promise<Message | undefined>;
14
15
  listByUser(userId: string): Promise<Message[]>;
15
16
  listByGroup(groupId: string): Promise<Message[]>;
17
+ delete(id: string): Promise<void>;
16
18
  }
17
19
 
18
20
  export interface GroupStoreAdapter {
@@ -9,6 +9,10 @@ export class InMemoryMessageStore implements MessageStoreAdapter {
9
9
  return message;
10
10
  }
11
11
 
12
+ async findById(id: string): Promise<Message | undefined> {
13
+ return this.messages.find((msg) => msg.id === id);
14
+ }
15
+
12
16
  async listByUser(userId: string): Promise<Message[]> {
13
17
  return this.messages.filter(
14
18
  (msg) => msg.senderId === userId || msg.receiverId === userId
@@ -18,4 +22,8 @@ export class InMemoryMessageStore implements MessageStoreAdapter {
18
22
  async listByGroup(groupId: string): Promise<Message[]> {
19
23
  return this.messages.filter((msg) => msg.groupId === groupId);
20
24
  }
25
+
26
+ async delete(id: string): Promise<void> {
27
+ this.messages = this.messages.filter((msg) => msg.id !== id);
28
+ }
21
29
  }
@@ -1,5 +1,5 @@
1
- import { encryptMessage, decryptMessage, deriveSharedSecret } from '../src/crypto/e2e';
2
- import { generateIdentityKeyPair } from '../src/crypto/keys';
1
+ import { encryptMessage, decryptMessage, deriveSharedSecret } from '../src/crypto/e2e.js';
2
+ import { generateIdentityKeyPair } from '../src/crypto/keys.js';
3
3
 
4
4
  describe('End-to-End Encryption', () => {
5
5
  describe('Key Generation', () => {
@@ -28,10 +28,10 @@ describe('End-to-End Encryption', () => {
28
28
  const aliceKeys = generateIdentityKeyPair();
29
29
  const bobKeys = generateIdentityKeyPair();
30
30
 
31
- const aliceShared = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
32
- const bobShared = await deriveSharedSecret(bobKeys.privateKey, aliceKeys.publicKey);
31
+ const aliceShared = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
32
+ const bobShared = await deriveSharedSecret(bobKeys, aliceKeys.publicKey);
33
33
 
34
- expect(aliceShared).toBe(bobShared);
34
+ expect(aliceShared).toEqual(bobShared);
35
35
  });
36
36
 
37
37
  it('should derive different secrets for different key pairs', async () => {
@@ -39,10 +39,10 @@ describe('End-to-End Encryption', () => {
39
39
  const bobKeys = generateIdentityKeyPair();
40
40
  const charlieKeys = generateIdentityKeyPair();
41
41
 
42
- const aliceBobSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
43
- const aliceCharlieSecret = await deriveSharedSecret(aliceKeys.privateKey, charlieKeys.publicKey);
42
+ const aliceBobSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
43
+ const aliceCharlieSecret = await deriveSharedSecret(aliceKeys, charlieKeys.publicKey);
44
44
 
45
- expect(aliceBobSecret).not.toBe(aliceCharlieSecret);
45
+ expect(aliceBobSecret).not.toEqual(aliceCharlieSecret);
46
46
  });
47
47
  });
48
48
 
@@ -50,7 +50,7 @@ describe('End-to-End Encryption', () => {
50
50
  it('should encrypt and decrypt a message correctly', async () => {
51
51
  const aliceKeys = generateIdentityKeyPair();
52
52
  const bobKeys = generateIdentityKeyPair();
53
- const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
53
+ const sharedSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
54
54
 
55
55
  const plaintext = 'Hello, Bob!';
56
56
  const encrypted = await encryptMessage(plaintext, sharedSecret);
@@ -67,7 +67,7 @@ describe('End-to-End Encryption', () => {
67
67
  it('should produce different ciphertexts for the same plaintext', async () => {
68
68
  const aliceKeys = generateIdentityKeyPair();
69
69
  const bobKeys = generateIdentityKeyPair();
70
- const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
70
+ const sharedSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
71
71
 
72
72
  const plaintext = 'Hello, Bob!';
73
73
  const encrypted1 = await encryptMessage(plaintext, sharedSecret);
@@ -82,21 +82,21 @@ describe('End-to-End Encryption', () => {
82
82
  const bobKeys = generateIdentityKeyPair();
83
83
  const charlieKeys = generateIdentityKeyPair();
84
84
 
85
- const aliceBobSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
86
- const aliceCharlieSecret = await deriveSharedSecret(aliceKeys.privateKey, charlieKeys.publicKey);
85
+ const aliceBobSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
86
+ const aliceCharlieSecret = await deriveSharedSecret(aliceKeys, charlieKeys.publicKey);
87
87
 
88
88
  const plaintext = 'Secret message';
89
89
  const encrypted = await encryptMessage(plaintext, aliceBobSecret);
90
90
 
91
- await expect(
91
+ expect(() =>
92
92
  decryptMessage(encrypted.ciphertext, encrypted.iv, aliceCharlieSecret)
93
- ).rejects.toThrow();
93
+ ).toThrow(/Unsupported state or unable to authenticate data/);
94
94
  });
95
95
 
96
96
  it('should handle empty messages', async () => {
97
97
  const aliceKeys = generateIdentityKeyPair();
98
98
  const bobKeys = generateIdentityKeyPair();
99
- const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
99
+ const sharedSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
100
100
 
101
101
  const plaintext = '';
102
102
  const encrypted = await encryptMessage(plaintext, sharedSecret);
@@ -108,7 +108,7 @@ describe('End-to-End Encryption', () => {
108
108
  it('should handle long messages', async () => {
109
109
  const aliceKeys = generateIdentityKeyPair();
110
110
  const bobKeys = generateIdentityKeyPair();
111
- const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
111
+ const sharedSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
112
112
 
113
113
  const plaintext = 'A'.repeat(10000);
114
114
  const encrypted = await encryptMessage(plaintext, sharedSecret);
@@ -120,7 +120,7 @@ describe('End-to-End Encryption', () => {
120
120
  it('should handle special characters and emojis', async () => {
121
121
  const aliceKeys = generateIdentityKeyPair();
122
122
  const bobKeys = generateIdentityKeyPair();
123
- const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
123
+ const sharedSecret = await deriveSharedSecret(aliceKeys, bobKeys.publicKey);
124
124
 
125
125
  const plaintext = 'Hello 👋 World! 🌍 Special chars: @#$%^&*()';
126
126
  const encrypted = await encryptMessage(plaintext, sharedSecret);