diff --git a/package.json b/package.json index f5908a5..7121219 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "type": "module", "dependencies": { + "@blah-im/core": "^0.2.1", "@zeabur/svelte-adapter": "^1.0.0", "bits-ui": "^0.21.13", "canonicalize": "^2.0.0", diff --git a/src/lib/blah/crypto/crypto.test.ts b/src/lib/blah/crypto/crypto.test.ts deleted file mode 100644 index a08b82d..0000000 --- a/src/lib/blah/crypto/crypto.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { test, expect } from 'vitest'; -import { BlahKeyPair } from '.'; - -let keypair: BlahKeyPair; - -test('generate keypair', async () => { - keypair = await BlahKeyPair.generate(); -}); - -test('encode & decode keypair', async () => { - const encoded = await keypair.encode(); - const decoded = await BlahKeyPair.fromEncoded(encoded); - - expect(decoded.id).toBe(keypair.id); -}); - -test('sign & verify payload', async () => { - const payload = { foo: 'bar', baz: 123 }; - const signedPayload = await keypair.signPayload(payload); - const verifiedPayload = await keypair.publicIdentity.verifyPayload(signedPayload); - - expect(verifiedPayload).toEqual(payload); -}); - -test('sign & verify payload with wrong keypair', async () => { - const keypair2 = await BlahKeyPair.generate(); - const payload = { foo: 'bar', baz: 123 }; - const signedPayload = await keypair.signPayload(payload); - expect(async () => { - await keypair2.publicIdentity.verifyPayload(signedPayload); - }).rejects.toThrowError(); -}); - -test('sign & verify payload with wrong key order but should still work', async () => { - const payload = { foo: 'bar', baz: 123 }; - const signedPayload = await keypair.signPayload(payload); - const signedPayload2 = { - sig: signedPayload.sig, - signee: { - payload: { baz: 123, foo: 'bar' }, - user: signedPayload.signee.user, - nonce: signedPayload.signee.nonce, - timestamp: signedPayload.signee.timestamp - } - }; - const verifiedPayload = await keypair.publicIdentity.verifyPayload(signedPayload2); - expect(verifiedPayload).toEqual(payload); -}); diff --git a/src/lib/blah/crypto/index.ts b/src/lib/blah/crypto/index.ts deleted file mode 100644 index 011211d..0000000 --- a/src/lib/blah/crypto/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './keypair'; -export * from './publicIdentity'; -export * from './signedPayload'; -export * from './utils'; diff --git a/src/lib/blah/crypto/keypair.ts b/src/lib/blah/crypto/keypair.ts deleted file mode 100644 index 81df7e4..0000000 --- a/src/lib/blah/crypto/keypair.ts +++ /dev/null @@ -1,81 +0,0 @@ -import canonicalize from 'canonicalize'; -import { BlahPublicIdentity } from './publicIdentity'; -import type { BlahPayloadSignee, BlahSignedPayload } from './signedPayload'; -import { bufToHex } from './utils'; - -export type EncodedBlahKeyPair = { - v: '0'; - id: string; - privateKey: JsonWebKey; -}; - -export class BlahKeyPair { - publicIdentity: BlahPublicIdentity; - private privateKey: CryptoKey; - - get id() { - return this.publicIdentity.id; - } - get name() { - return this.publicIdentity.name; - } - - private constructor(publicIdentity: BlahPublicIdentity, privateKey: CryptoKey) { - this.publicIdentity = publicIdentity; - this.privateKey = privateKey; - } - - static async generate(): Promise { - const { publicKey, privateKey } = await crypto.subtle.generateKey('Ed25519', true, [ - 'sign', - 'verify' - ]); - const publicIdentity = await BlahPublicIdentity.fromPublicKey(publicKey); - return new BlahKeyPair(publicIdentity, privateKey); - } - - static async fromEncoded(encoded: EncodedBlahKeyPair): Promise { - if (encoded.v !== '0') { - throw new Error('Unsupported version'); - } - const publicIdentity = await BlahPublicIdentity.fromID(encoded.id); - const privateKey = await crypto.subtle.importKey( - 'jwk', - encoded.privateKey, - { name: 'Ed25519' }, - true, - ['sign'] - ); - - return new BlahKeyPair(publicIdentity, privateKey); - } - - async encode(): Promise { - return { - v: '0', - id: this.publicIdentity.id, - privateKey: await crypto.subtle.exportKey('jwk', this.privateKey) - }; - } - - async signPayload

(payload: P, date: Date = new Date()): Promise> { - const nonceBuf = new Uint32Array(1); - crypto.getRandomValues(nonceBuf); - - const timestamp = Math.floor(date.getTime() / 1000); - - const signee: BlahPayloadSignee

= { - nonce: nonceBuf[0], - payload, - timestamp, - user: this.id - }; - const signeeBytes = new TextEncoder().encode(canonicalize(signee)); - - const rawSig = await crypto.subtle.sign('Ed25519', this.privateKey, signeeBytes); - return { - sig: bufToHex(rawSig), - signee - }; - } -} diff --git a/src/lib/blah/crypto/publicIdentity.ts b/src/lib/blah/crypto/publicIdentity.ts deleted file mode 100644 index 166104d..0000000 --- a/src/lib/blah/crypto/publicIdentity.ts +++ /dev/null @@ -1,65 +0,0 @@ -import canonicalize from 'canonicalize'; -import type { BlahSignedPayload } from './signedPayload'; -import { bufToHex, hexToBuf } from './utils'; -import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator'; - -export function generateName(id: string) { - return uniqueNamesGenerator({ - seed: id, - style: 'capital', - separator: ' ', - dictionaries: [adjectives, animals] - }); -} - -export class BlahPublicIdentity { - private publicKey: CryptoKey; - id: string; - name: string; - - private constructor(publicKey: CryptoKey, id: string) { - this.publicKey = publicKey; - this.id = id; - this.name = generateName(id); - } - - static async fromPublicKey(publicKey: CryptoKey): Promise { - const rawKey = await crypto.subtle.exportKey('raw', publicKey); - const id = bufToHex(rawKey); - return new BlahPublicIdentity(publicKey, id); - } - - static async fromID(id: string): Promise { - const rawKey = hexToBuf(id); - const publicKey = await crypto.subtle.importKey('raw', rawKey, { name: 'Ed25519' }, true, [ - 'verify' - ]); - return new BlahPublicIdentity(publicKey, id); - } - - static async verifyPayload

( - signedPayload: BlahSignedPayload

- ): Promise<{ payload: P; identity: BlahPublicIdentity }> { - const { signee } = signedPayload; - const identity = await BlahPublicIdentity.fromID(signee.user); - return { payload: await identity.verifyPayload(signedPayload), identity }; - } - - async verifyPayload

(signedPayload: BlahSignedPayload

): Promise

{ - const { sig, signee } = signedPayload; - if (signee.user !== this.id) { - throw new Error(`Payload is not signed by this identity. Was signed by ${signee.user}.`); - } - const signeeBytes = new TextEncoder().encode(canonicalize(signee)); - const result = await crypto.subtle.verify( - 'Ed25519', - this.publicKey, - hexToBuf(sig), - signeeBytes - ); - if (!result) { - throw new Error('Invalid signature'); - } - return signee.payload; - } -} diff --git a/src/lib/blah/crypto/signedPayload.ts b/src/lib/blah/crypto/signedPayload.ts deleted file mode 100644 index 808ac66..0000000 --- a/src/lib/blah/crypto/signedPayload.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type BlahPayloadSignee

= { - nonce: number; - payload: P; - timestamp: number; - user: string; - act_key?: string; -}; - -export type BlahSignedPayload

= { - sig: string; - signee: BlahPayloadSignee

; -}; diff --git a/src/lib/blah/crypto/utils.ts b/src/lib/blah/crypto/utils.ts deleted file mode 100644 index d1af9fd..0000000 --- a/src/lib/blah/crypto/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function bufToHex(buf: ArrayBufferLike): string { - return [...new Uint8Array(buf)].map((x) => x.toString(16).padStart(2, '0')).join(''); -} - -export function hexToBuf(hex: string): Uint8Array { - return new Uint8Array((hex.match(/[\da-f]{2}/gi) ?? []).map((m) => parseInt(m, 16))); -} diff --git a/src/lib/chatList.ts b/src/lib/chatList.ts index f565d1b..a162dff 100644 --- a/src/lib/chatList.ts +++ b/src/lib/chatList.ts @@ -1,7 +1,7 @@ import { readable, writable, type Readable, type Writable } from 'svelte/store'; import { chatFromBlah, type Chat } from './types'; import type { BlahMessage, BlahRoomInfo } from './blah/structures'; -import type { BlahSignedPayload } from './blah/crypto'; +import type { BlahSignedPayload } from '@blah-im/core/crypto'; export class ChatListManager { chatList: Writable; diff --git a/src/lib/chatServers.ts b/src/lib/chatServers.ts index 1d56479..8bcb4bd 100644 --- a/src/lib/chatServers.ts +++ b/src/lib/chatServers.ts @@ -1,7 +1,7 @@ import { persisted } from 'svelte-persisted-store'; import { get } from 'svelte/store'; import { BlahChatServerConnection } from './blah/connection/chatServer'; -import { BlahKeyPair, type EncodedBlahKeyPair } from './blah/crypto'; +import { BlahKeyPair, type EncodedBlahKeyPair } from '@blah-im/core/crypto'; import { currentKeyPair } from './keystore'; import { ChatListManager } from './chatList'; import { browser } from '$app/environment'; diff --git a/src/lib/keystore.ts b/src/lib/keystore.ts index 9d81dfa..7d68322 100644 --- a/src/lib/keystore.ts +++ b/src/lib/keystore.ts @@ -1,5 +1,5 @@ import { persisted } from 'svelte-persisted-store'; -import type { EncodedBlahKeyPair } from './blah/crypto'; +import type { EncodedBlahKeyPair } from '@blah-im/core/crypto'; import { derived } from 'svelte/store'; export const keyStore = persisted('weblah-keypairs', []); diff --git a/src/lib/types/message.ts b/src/lib/types/message.ts index 1523daa..56a2fb3 100644 --- a/src/lib/types/message.ts +++ b/src/lib/types/message.ts @@ -1,4 +1,4 @@ -import { generateName, type BlahSignedPayload } from '$lib/blah/crypto'; +import type { BlahSignedPayload } from '@blah-im/core/crypto'; import type { BlahMessage } from '$lib/blah/structures'; import type { BlahRichText } from '$lib/richText'; @@ -12,7 +12,10 @@ export type Message = { export function messageFromBlah(payload: BlahSignedPayload): Message { return { id: payload.sig, - sender: { id: payload.signee.user, name: generateName(payload.signee.user) }, + sender: { + id: payload.signee.id_key, + name: payload.signee.id_key.slice(0, 4) + '...' + payload.signee.id_key.slice(-4) + }, content: payload.signee.payload.rich_text, date: new Date(payload.signee.timestamp * 1000) }; diff --git a/src/routes/(app)/IdentityMenu.svelte b/src/routes/(app)/IdentityMenu.svelte index f68eb56..fce1cd7 100644 --- a/src/routes/(app)/IdentityMenu.svelte +++ b/src/routes/(app)/IdentityMenu.svelte @@ -2,7 +2,7 @@ import * as DropdownMenu from '$lib/components/DropdownMenu'; import { AvatarBeam } from 'svelte-boring-avatars'; import { keyStore, currentKeyIndex, currentKeyPair } from '$lib/keystore'; - import { BlahKeyPair, generateName } from '$lib/blah/crypto'; + import { BlahKeyPair } from '@blah-im/core/crypto'; let className: string = ''; export { className as class }; @@ -11,7 +11,9 @@ let currentKeyName: string | null; $: { currentKeyId = $currentKeyPair?.id; - currentKeyName = currentKeyId ? generateName(currentKeyId) : null; + currentKeyName = currentKeyId + ? currentKeyId.slice(0, 4) + '...' + currentKeyId.slice(-4) + : null; } async function createKeyPair() { @@ -48,7 +50,7 @@ onValueChange={setCurrentKeyIndex} > {#each $keyStore as { id }, idx} - {@const name = generateName(id)} + {@const name = id.slice(0, 4) + '...' + id.slice(-4)}

@@ -58,7 +60,7 @@ {/each} - Manage identities + Settings... {:else} Create new identity {/if}