From 2b6e81119be0a45ff62bda4ce1bdcffe03a09cdf Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 30 Mar 2026 00:28:12 +0800 Subject: [PATCH] feat: add TS3 identity generation with Ed25519 keypairs Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 7 ++++++ package.json | 1 + src/ts-protocol/identity.test.ts | 28 ++++++++++++++++++++++++ src/ts-protocol/identity.ts | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 src/ts-protocol/identity.test.ts create mode 100644 src/ts-protocol/identity.ts diff --git a/package-lock.json b/package-lock.json index 200f1f9..336c877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "better-sqlite3": "^12.8.0", "express": "^5.2.1", "pino": "^10.3.1", + "tweetnacl": "^1.0.3", "ws": "^8.20.0" }, "devDependencies": { @@ -3080,6 +3081,12 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index 5fd3a34..62ff2bc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "better-sqlite3": "^12.8.0", "express": "^5.2.1", "pino": "^10.3.1", + "tweetnacl": "^1.0.3", "ws": "^8.20.0" }, "devDependencies": { diff --git a/src/ts-protocol/identity.test.ts b/src/ts-protocol/identity.test.ts new file mode 100644 index 0000000..2f0aabd --- /dev/null +++ b/src/ts-protocol/identity.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { generateIdentity, exportIdentity, importIdentity } from "./identity.js"; + +describe("TS3 Identity", () => { + it("generates a valid identity with keypair", () => { + const identity = generateIdentity(); + expect(identity.publicKey).toBeInstanceOf(Uint8Array); + expect(identity.privateKey).toBeInstanceOf(Uint8Array); + expect(identity.publicKey.length).toBe(32); + expect(identity.privateKey.length).toBe(64); + expect(identity.uid).toBeTruthy(); + expect(typeof identity.uid).toBe("string"); + }); + + it("exports and imports identity consistently", () => { + const identity = generateIdentity(); + const exported = exportIdentity(identity); + const imported = importIdentity(exported); + expect(imported.uid).toBe(identity.uid); + expect(imported.publicKey).toEqual(identity.publicKey); + }); + + it("generates unique identities each time", () => { + const id1 = generateIdentity(); + const id2 = generateIdentity(); + expect(id1.uid).not.toBe(id2.uid); + }); +}); diff --git a/src/ts-protocol/identity.ts b/src/ts-protocol/identity.ts new file mode 100644 index 0000000..eea032e --- /dev/null +++ b/src/ts-protocol/identity.ts @@ -0,0 +1,37 @@ +import nacl from "tweetnacl"; +import crypto from "node:crypto"; + +export interface TS3Identity { + publicKey: Uint8Array; + privateKey: Uint8Array; + uid: string; // base64-encoded SHA1 of public key +} + +export function generateIdentity(): TS3Identity { + const keypair = nacl.sign.keyPair(); + const uid = computeUid(keypair.publicKey); + return { + publicKey: keypair.publicKey, + privateKey: keypair.secretKey, + uid, + }; +} + +export function computeUid(publicKey: Uint8Array): string { + return crypto.createHash("sha1").update(publicKey).digest("base64"); +} + +export function exportIdentity(identity: TS3Identity): string { + return JSON.stringify({ + publicKey: Buffer.from(identity.publicKey).toString("base64"), + privateKey: Buffer.from(identity.privateKey).toString("base64"), + }); +} + +export function importIdentity(data: string): TS3Identity { + const parsed = JSON.parse(data) as { publicKey: string; privateKey: string }; + const publicKey = new Uint8Array(Buffer.from(parsed.publicKey, "base64")); + const privateKey = new Uint8Array(Buffer.from(parsed.privateKey, "base64")); + const uid = computeUid(publicKey); + return { publicKey, privateKey, uid }; +}