refactor: migrate to @blah-im/core

This commit is contained in:
Shibo Lyu 2024-10-01 03:09:51 +08:00
parent d361ac2db3
commit 2074687692
12 changed files with 15 additions and 226 deletions

View file

@ -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",

View file

@ -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);
});

View file

@ -1,4 +0,0 @@
export * from './keypair';
export * from './publicIdentity';
export * from './signedPayload';
export * from './utils';

View file

@ -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<BlahKeyPair> {
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<BlahKeyPair> {
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<EncodedBlahKeyPair> {
return {
v: '0',
id: this.publicIdentity.id,
privateKey: await crypto.subtle.exportKey('jwk', this.privateKey)
};
}
async signPayload<P>(payload: P, date: Date = new Date()): Promise<BlahSignedPayload<P>> {
const nonceBuf = new Uint32Array(1);
crypto.getRandomValues(nonceBuf);
const timestamp = Math.floor(date.getTime() / 1000);
const signee: BlahPayloadSignee<P> = {
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
};
}
}

View file

@ -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<BlahPublicIdentity> {
const rawKey = await crypto.subtle.exportKey('raw', publicKey);
const id = bufToHex(rawKey);
return new BlahPublicIdentity(publicKey, id);
}
static async fromID(id: string): Promise<BlahPublicIdentity> {
const rawKey = hexToBuf(id);
const publicKey = await crypto.subtle.importKey('raw', rawKey, { name: 'Ed25519' }, true, [
'verify'
]);
return new BlahPublicIdentity(publicKey, id);
}
static async verifyPayload<P>(
signedPayload: BlahSignedPayload<P>
): Promise<{ payload: P; identity: BlahPublicIdentity }> {
const { signee } = signedPayload;
const identity = await BlahPublicIdentity.fromID(signee.user);
return { payload: await identity.verifyPayload(signedPayload), identity };
}
async verifyPayload<P>(signedPayload: BlahSignedPayload<P>): Promise<P> {
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;
}
}

View file

@ -1,12 +0,0 @@
export type BlahPayloadSignee<P> = {
nonce: number;
payload: P;
timestamp: number;
user: string;
act_key?: string;
};
export type BlahSignedPayload<P> = {
sig: string;
signee: BlahPayloadSignee<P>;
};

View file

@ -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)));
}

View file

@ -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<Chat[]>;

View file

@ -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';

View file

@ -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<EncodedBlahKeyPair[]>('weblah-keypairs', []);

View file

@ -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<BlahMessage>): 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)
};

View file

@ -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)}
<DropdownMenu.RadioItem value={idx.toString()}>
<div class="flex items-center gap-2 py-0.5">
<AvatarBeam size={24} name={id} />
@ -58,7 +60,7 @@
{/each}
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator />
<DropdownMenu.Item>Manage identities</DropdownMenu.Item>
<DropdownMenu.Item>Settings...</DropdownMenu.Item>
{:else}
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
{/if}