chatly-sdk 1.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.
- package/README.md +312 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.js +443 -0
- package/examples/groupChat.ts +71 -0
- package/examples/oneToOne.ts +72 -0
- package/examples/saveLoadUser.ts +61 -0
- package/package.json +37 -0
- package/src/ChatManager.ts +103 -0
- package/src/chat/ChatSession.ts +88 -0
- package/src/chat/GroupSession.ts +60 -0
- package/src/crypto/e2e.ts +95 -0
- package/src/crypto/group.ts +24 -0
- package/src/crypto/groupKeys.ts +0 -0
- package/src/crypto/keyManager.ts +28 -0
- package/src/crypto/keys.ts +34 -0
- package/src/crypto/utils.ts +7 -0
- package/src/crypto/uuid.ts +42 -0
- package/src/features/groups.ts +0 -0
- package/src/features/imageSharing.ts +0 -0
- package/src/features/readReceipts.ts +0 -0
- package/src/index.ts +212 -0
- package/src/models/ImageMessage.ts +6 -0
- package/src/models/ReadReceipt.ts +6 -0
- package/src/models/group.ts +8 -0
- package/src/models/message.ts +13 -0
- package/src/models/user.ts +11 -0
- package/src/stores/adapters.ts +22 -0
- package/src/stores/memory/groupStore.ts +19 -0
- package/src/stores/memory/messageStore.ts +21 -0
- package/src/stores/memory/userStore.ts +24 -0
- package/src/transport/adapters.ts +7 -0
- package/src/transport/memoryTransport.ts +24 -0
- package/src/transport/websocketClient.ts +37 -0
- package/src/transport/websocketServer.ts +33 -0
- package/test/crypto.test.ts +45 -0
- package/tsconfig.json +44 -0
|
@@ -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,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
|