chatly-sdk 0.0.1

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.
@@ -0,0 +1,103 @@
1
+
2
+ import { KeyManager, KeyPair } from './crypto/keyManager';
3
+ import { encrypt } from './crypto/encrypt';
4
+ import { decrypt } from './crypto/decrypt';
5
+ import { ChatClient } from './transport/websocketClient';
6
+ import { Message } from './models/Message';
7
+
8
+ export class ChatManager {
9
+ private keyManager: KeyManager;
10
+ private chatClient: ChatClient;
11
+ private contacts: { [id: string]: Uint8Array } = {}; // Maps user ID to public key
12
+
13
+ constructor(websocketUrl: string) {
14
+ this.keyManager = new KeyManager();
15
+ this.chatClient = new ChatClient(websocketUrl);
16
+ }
17
+
18
+ async initialize(): Promise<void> {
19
+ await this.keyManager.generateKeys();
20
+ this.chatClient.connect();
21
+ this.chatClient.onMessage(this.handleIncomingMessage.bind(this));
22
+ }
23
+
24
+ addContact(userId: string, publicKey: Uint8Array): void {
25
+ this.contacts[userId] = publicKey;
26
+ }
27
+
28
+ async sendMessage(recipientId: string, content: string): Promise<void> {
29
+ const recipientPublicKey = this.contacts[recipientId];
30
+ if (!recipientPublicKey) {
31
+ throw new Error(`Contact not found: ${recipientId}`);
32
+ }
33
+
34
+ const senderPrivateKey = this.keyManager.getPrivateKey();
35
+ if (!senderPrivateKey) {
36
+ throw new Error('Sender keys not generated.');
37
+ }
38
+
39
+ const message: Message = {
40
+ id: Date.now().toString(),
41
+ senderId: this.getOwnUserId(),
42
+ recipientId,
43
+ content,
44
+ timestamp: Date.now(),
45
+ };
46
+
47
+ const encryptedMessage = await encrypt(
48
+ JSON.stringify(message),
49
+ recipientPublicKey,
50
+ senderPrivateKey
51
+ );
52
+
53
+ this.chatClient.sendMessage(Buffer.from(encryptedMessage));
54
+ }
55
+
56
+ private async handleIncomingMessage(encryptedMessage: Buffer): Promise<void> {
57
+ // In a real application, you would need a way to identify the sender
58
+ // and get their public key. For this example, we'll assume the sender
59
+ // is the only other contact.
60
+ const senderId = Object.keys(this.contacts)[0];
61
+ if (!senderId) {
62
+ console.error('Received message but no contacts are known.');
63
+ return;
64
+ }
65
+ const senderPublicKey = this.contacts[senderId];
66
+
67
+ if (!senderPublicKey) {
68
+ console.error('Received message from unknown sender.');
69
+ return;
70
+ }
71
+
72
+ const recipientPrivateKey = this.keyManager.getPrivateKey();
73
+ if (!recipientPrivateKey) {
74
+ throw new Error('Recipient keys not generated.');
75
+ }
76
+
77
+ try {
78
+ const decryptedMessageJson = await decrypt(
79
+ encryptedMessage,
80
+ senderPublicKey,
81
+ recipientPrivateKey
82
+ );
83
+ const message: Message = JSON.parse(decryptedMessageJson);
84
+ console.log('Decrypted message:', message);
85
+ } catch (error) {
86
+ console.error('Failed to decrypt message:', error);
87
+ }
88
+ }
89
+
90
+ getOwnUserId(): string {
91
+ // In a real app, this would be a unique user ID.
92
+ // For this example, we'll use a hash of the public key.
93
+ const publicKey = this.keyManager.getPublicKey();
94
+ if (!publicKey) {
95
+ throw new Error('Keys not generated.');
96
+ }
97
+ return Buffer.from(publicKey).toString('hex');
98
+ }
99
+
100
+ getOwnPublicKey(): Uint8Array | null {
101
+ return this.keyManager.getPublicKey();
102
+ }
103
+ }
@@ -0,0 +1,88 @@
1
+ import type { User } from "../models/user.js";
2
+ import type { Message } from "../models/message.js";
3
+ import { deriveSharedSecret, encryptMessage, decryptMessage } from "../crypto/e2e.js";
4
+ import type { KeyPair } from "../crypto/keys.js";
5
+ import { generateUUID } from "../crypto/uuid.js";
6
+
7
+ export class ChatSession {
8
+ private sharedSecret: Buffer | null = null;
9
+ private ephemeralKeyPair: KeyPair | null = null;
10
+
11
+ constructor(
12
+ public readonly id: string,
13
+ public readonly userA: User,
14
+ public readonly userB: User
15
+ ) {}
16
+
17
+ /**
18
+ * Initialize the session by deriving the shared secret
19
+ * ECDH is commutative, so we can use either user's keys
20
+ */
21
+ async initialize(): Promise<void> {
22
+ // For 1:1 chat, derive shared secret from identity keys
23
+ // Use userA's private key and userB's public key
24
+ // (ECDH is commutative, so userB could also use their private key and userA's public key)
25
+ const localKeyPair: KeyPair = {
26
+ publicKey: this.userA.publicKey,
27
+ privateKey: this.userA.privateKey,
28
+ };
29
+
30
+ this.sharedSecret = deriveSharedSecret(localKeyPair, this.userB.publicKey);
31
+ }
32
+
33
+ /**
34
+ * Initialize from a specific user's perspective (useful when decrypting)
35
+ */
36
+ async initializeForUser(user: User): Promise<void> {
37
+ const otherUser = user.id === this.userA.id ? this.userB : this.userA;
38
+ const localKeyPair: KeyPair = {
39
+ publicKey: user.publicKey,
40
+ privateKey: user.privateKey,
41
+ };
42
+
43
+ this.sharedSecret = deriveSharedSecret(localKeyPair, otherUser.publicKey);
44
+ }
45
+
46
+ /**
47
+ * Encrypt a message for this session
48
+ */
49
+ async encrypt(plaintext: string, senderId: string): Promise<Message> {
50
+ if (!this.sharedSecret) {
51
+ await this.initialize();
52
+ }
53
+
54
+ if (!this.sharedSecret) {
55
+ throw new Error("Failed to initialize session");
56
+ }
57
+
58
+ const { ciphertext, iv } = encryptMessage(plaintext, this.sharedSecret);
59
+
60
+ return {
61
+ id: generateUUID(),
62
+ senderId,
63
+ receiverId: senderId === this.userA.id ? this.userB.id : this.userA.id,
64
+ ciphertext,
65
+ iv,
66
+ timestamp: Date.now(),
67
+ type: "text",
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Decrypt a message in this session
73
+ */
74
+ async decrypt(message: Message, user: User): Promise<string> {
75
+ // Re-initialize from the decrypting user's perspective if needed
76
+ // (though ECDH is commutative, this ensures we're using the right keys)
77
+ if (!this.sharedSecret ||
78
+ (user.id !== this.userA.id && user.id !== this.userB.id)) {
79
+ await this.initializeForUser(user);
80
+ }
81
+
82
+ if (!this.sharedSecret) {
83
+ throw new Error("Failed to initialize session");
84
+ }
85
+
86
+ return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
87
+ }
88
+ }
@@ -0,0 +1,60 @@
1
+ import type { Group } from "../models/group.js";
2
+ import type { Message } from "../models/message.js";
3
+ import { deriveGroupKey } from "../crypto/group.js";
4
+ import { encryptMessage, decryptMessage } from "../crypto/e2e.js";
5
+ import { base64ToBuffer } from "../crypto/utils.js";
6
+ import { generateUUID } from "../crypto/uuid.js";
7
+
8
+ export class GroupSession {
9
+ private groupKey: Buffer | null = null;
10
+
11
+ constructor(public readonly group: Group) {}
12
+
13
+ /**
14
+ * Initialize the session by deriving the group key
15
+ */
16
+ async initialize(): Promise<void> {
17
+ const groupKeyData = deriveGroupKey(this.group.id);
18
+ this.groupKey = base64ToBuffer(groupKeyData.key);
19
+ }
20
+
21
+ /**
22
+ * Encrypt a message for this group
23
+ */
24
+ async encrypt(plaintext: string, senderId: string): Promise<Message> {
25
+ if (!this.groupKey) {
26
+ await this.initialize();
27
+ }
28
+
29
+ if (!this.groupKey) {
30
+ throw new Error("Failed to initialize group session");
31
+ }
32
+
33
+ const { ciphertext, iv } = encryptMessage(plaintext, this.groupKey);
34
+
35
+ return {
36
+ id: generateUUID(),
37
+ senderId,
38
+ groupId: this.group.id,
39
+ ciphertext,
40
+ iv,
41
+ timestamp: Date.now(),
42
+ type: "text",
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Decrypt a message in this group
48
+ */
49
+ async decrypt(message: Message): Promise<string> {
50
+ if (!this.groupKey) {
51
+ await this.initialize();
52
+ }
53
+
54
+ if (!this.groupKey) {
55
+ throw new Error("Failed to initialize group session");
56
+ }
57
+
58
+ return decryptMessage(message.ciphertext, message.iv, this.groupKey);
59
+ }
60
+ }
@@ -0,0 +1,95 @@
1
+ import { createECDH, createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
2
+ import type { KeyPair } from "./keys.js";
3
+ import { base64ToBuffer, bufferToBase64 } from "./utils.js";
4
+ import { SUPPORTED_CURVE } from "./keys.js";
5
+ import { createHash } from "crypto";
6
+
7
+ const ALGORITHM = "aes-256-gcm";
8
+ const IV_LENGTH = 12; // 96 bits for GCM
9
+ const SALT_LENGTH = 16;
10
+ const KEY_LENGTH = 32; // 256 bits
11
+ const TAG_LENGTH = 16;
12
+ const PBKDF2_ITERATIONS = 100000;
13
+
14
+ /**
15
+ * Derive a shared secret using ECDH key exchange
16
+ */
17
+ // export function deriveSharedSecret(local: KeyPair, remotePublicKey: string): Buffer {
18
+ // const ecdh = createECDH(SUPPORTED_CURVE);
19
+ // ecdh.setPrivateKey(base64ToBuffer(local.privateKey));
20
+
21
+ // const remotePublicKeyBuffer = base64ToBuffer(remotePublicKey);
22
+ // const sharedSecret = ecdh.computeSecret(remotePublicKeyBuffer);
23
+
24
+ // // Derive a symmetric key from the shared secret using PBKDF2
25
+ // // Use a deterministic salt based on both public keys for consistency
26
+ // const salt = Buffer.concat([
27
+ // base64ToBuffer(local.publicKey),
28
+ // base64ToBuffer(remotePublicKey)
29
+ // ]).slice(0, SALT_LENGTH);
30
+ // const derivedKey = pbkdf2Sync(sharedSecret, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
31
+
32
+ // return derivedKey;
33
+ // }
34
+
35
+ export function deriveSharedSecret(local: KeyPair, remotePublicKey: string): Buffer {
36
+ const ecdh = createECDH(SUPPORTED_CURVE);
37
+ ecdh.setPrivateKey(base64ToBuffer(local.privateKey));
38
+
39
+ const remotePublicKeyBuffer = base64ToBuffer(remotePublicKey);
40
+ const sharedSecret = ecdh.computeSecret(remotePublicKeyBuffer);
41
+
42
+ // Deterministic salt: sort the two public-key buffers lexically,
43
+ // then hash and take first SALT_LENGTH bytes.
44
+ const a = base64ToBuffer(local.publicKey);
45
+ const b = base64ToBuffer(remotePublicKey);
46
+ const [first, second] = Buffer.compare(a, b) <= 0 ? [a, b] : [b, a];
47
+
48
+ const hash = createHash("sha256").update(first).update(second).digest();
49
+ const salt = hash.slice(0, SALT_LENGTH);
50
+
51
+ const derivedKey = pbkdf2Sync(sharedSecret, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
52
+
53
+ return derivedKey;
54
+ }
55
+
56
+ /**
57
+ * Encrypt a message using AES-GCM
58
+ */
59
+ export function encryptMessage(plaintext: string, secret: Buffer): { ciphertext: string; iv: string } {
60
+ const iv = randomBytes(IV_LENGTH);
61
+ const cipher = createCipheriv(ALGORITHM, secret, iv);
62
+
63
+ let ciphertext = cipher.update(plaintext, "utf8");
64
+ ciphertext = Buffer.concat([ciphertext, cipher.final()]);
65
+
66
+ const tag = cipher.getAuthTag();
67
+
68
+ // Combine ciphertext + tag
69
+ const encrypted = Buffer.concat([ciphertext, tag]);
70
+
71
+ return {
72
+ ciphertext: bufferToBase64(encrypted),
73
+ iv: bufferToBase64(iv),
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Decrypt a message using AES-GCM
79
+ */
80
+ export function decryptMessage(ciphertext: string, iv: string, secret: Buffer): string {
81
+ const encryptedBuffer = base64ToBuffer(ciphertext);
82
+ const ivBuffer = base64ToBuffer(iv);
83
+
84
+ // Extract tag from the end
85
+ const tag = encryptedBuffer.slice(-TAG_LENGTH);
86
+ const actualCiphertext = encryptedBuffer.slice(0, -TAG_LENGTH);
87
+
88
+ const decipher = createDecipheriv(ALGORITHM, secret, ivBuffer);
89
+ decipher.setAuthTag(tag);
90
+
91
+ let decrypted = decipher.update(actualCiphertext);
92
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
93
+
94
+ return decrypted.toString("utf8");
95
+ }
@@ -0,0 +1,24 @@
1
+ import { randomBytes, pbkdf2Sync } from "crypto";
2
+ import { bufferToBase64 } from "./utils.js";
3
+
4
+ export interface GroupKey {
5
+ groupId: string;
6
+ key: string;
7
+ }
8
+
9
+ const KEY_LENGTH = 32; // 256 bits
10
+ const PBKDF2_ITERATIONS = 100000;
11
+
12
+ /**
13
+ * Derive a group shared key from the group ID
14
+ */
15
+ export function deriveGroupKey(groupId: string): GroupKey {
16
+ // Use PBKDF2 to derive a deterministic key from the group ID
17
+ const salt = Buffer.from(groupId, "utf8");
18
+ const key = pbkdf2Sync(groupId, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
19
+
20
+ return {
21
+ groupId,
22
+ key: bufferToBase64(key),
23
+ };
24
+ }
File without changes
@@ -0,0 +1,28 @@
1
+
2
+ import sodium from 'libsodium-wrappers';
3
+
4
+ export interface KeyPair {
5
+ publicKey: Uint8Array;
6
+ privateKey: Uint8Array;
7
+ }
8
+
9
+ export class KeyManager {
10
+ private keyPair: KeyPair | null = null;
11
+
12
+ async generateKeys(): Promise<void> {
13
+ await sodium.ready;
14
+ const keys = sodium.crypto_box_keypair();
15
+ this.keyPair = {
16
+ publicKey: keys.publicKey,
17
+ privateKey: keys.privateKey,
18
+ };
19
+ }
20
+
21
+ getPublicKey(): Uint8Array | null {
22
+ return this.keyPair ? this.keyPair.publicKey : null;
23
+ }
24
+
25
+ getPrivateKey(): Uint8Array | null {
26
+ return this.keyPair ? this.keyPair.privateKey : null;
27
+ }
28
+ }
@@ -0,0 +1,34 @@
1
+ import { createECDH, randomBytes } from "crypto";
2
+ import { bufferToBase64, base64ToBuffer } from "./utils.js";
3
+
4
+ export interface KeyPair {
5
+ publicKey: string;
6
+ privateKey: string;
7
+ }
8
+
9
+ export interface StoredKeyPair extends KeyPair {
10
+ createdAt: number;
11
+ }
12
+
13
+ export const SUPPORTED_CURVE = "prime256v1";
14
+
15
+ /**
16
+ * Generate a long-term identity key pair for a user
17
+ */
18
+ export function generateIdentityKeyPair(): KeyPair {
19
+ const ecdh = createECDH(SUPPORTED_CURVE);
20
+ ecdh.generateKeys();
21
+
22
+ return {
23
+ publicKey: bufferToBase64(ecdh.getPublicKey()),
24
+ privateKey: bufferToBase64(ecdh.getPrivateKey()),
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Generate an ephemeral key pair for a session
30
+ */
31
+ export function generateEphemeralKeyPair(): KeyPair {
32
+ // Ephemeral keys use the same curve, but are meant to be temporary
33
+ return generateIdentityKeyPair();
34
+ }
@@ -0,0 +1,7 @@
1
+ export function bufferToBase64(buffer: Buffer): string {
2
+ return buffer.toString("base64");
3
+ }
4
+
5
+ export function base64ToBuffer(data: string): Buffer {
6
+ return Buffer.from(data, "base64");
7
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cross-platform UUID generator
3
+ * Works in both Node.js and browser environments
4
+ */
5
+ export function generateUUID(): string {
6
+ // Try Node.js crypto.randomUUID first
7
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
8
+ return crypto.randomUUID();
9
+ }
10
+
11
+ // Fallback for browsers: use crypto.getRandomValues
12
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
13
+ // Generate UUID v4 using crypto.getRandomValues
14
+ const bytes = new Uint8Array(16);
15
+ crypto.getRandomValues(bytes);
16
+
17
+ // Set version (4) and variant bits
18
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
19
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10
20
+
21
+ // Convert to UUID string format
22
+ const hex = Array.from(bytes)
23
+ .map((b) => b.toString(16).padStart(2, "0"))
24
+ .join("");
25
+
26
+ return [
27
+ hex.slice(0, 8),
28
+ hex.slice(8, 12),
29
+ hex.slice(12, 16),
30
+ hex.slice(16, 20),
31
+ hex.slice(20, 32),
32
+ ].join("-");
33
+ }
34
+
35
+ // Last resort fallback (shouldn't happen in modern environments)
36
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
37
+ const r = (Math.random() * 16) | 0;
38
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
39
+ return v.toString(16);
40
+ });
41
+ }
42
+
File without changes
File without changes
File without changes