mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
refactor: migrate to @blah-im/core
This commit is contained in:
parent
d361ac2db3
commit
2074687692
12 changed files with 15 additions and 226 deletions
|
@ -36,6 +36,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@blah-im/core": "^0.2.1",
|
||||||
"@zeabur/svelte-adapter": "^1.0.0",
|
"@zeabur/svelte-adapter": "^1.0.0",
|
||||||
"bits-ui": "^0.21.13",
|
"bits-ui": "^0.21.13",
|
||||||
"canonicalize": "^2.0.0",
|
"canonicalize": "^2.0.0",
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './keypair';
|
|
||||||
export * from './publicIdentity';
|
|
||||||
export * from './signedPayload';
|
|
||||||
export * from './utils';
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>;
|
|
||||||
};
|
|
|
@ -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)));
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { readable, writable, type Readable, type Writable } from 'svelte/store';
|
import { readable, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import { chatFromBlah, type Chat } from './types';
|
import { chatFromBlah, type Chat } from './types';
|
||||||
import type { BlahMessage, BlahRoomInfo } from './blah/structures';
|
import type { BlahMessage, BlahRoomInfo } from './blah/structures';
|
||||||
import type { BlahSignedPayload } from './blah/crypto';
|
import type { BlahSignedPayload } from '@blah-im/core/crypto';
|
||||||
|
|
||||||
export class ChatListManager {
|
export class ChatListManager {
|
||||||
chatList: Writable<Chat[]>;
|
chatList: Writable<Chat[]>;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { persisted } from 'svelte-persisted-store';
|
import { persisted } from 'svelte-persisted-store';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { BlahChatServerConnection } from './blah/connection/chatServer';
|
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 { currentKeyPair } from './keystore';
|
||||||
import { ChatListManager } from './chatList';
|
import { ChatListManager } from './chatList';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { persisted } from 'svelte-persisted-store';
|
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';
|
import { derived } from 'svelte/store';
|
||||||
|
|
||||||
export const keyStore = persisted<EncodedBlahKeyPair[]>('weblah-keypairs', []);
|
export const keyStore = persisted<EncodedBlahKeyPair[]>('weblah-keypairs', []);
|
||||||
|
|
|
@ -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 { BlahMessage } from '$lib/blah/structures';
|
||||||
import type { BlahRichText } from '$lib/richText';
|
import type { BlahRichText } from '$lib/richText';
|
||||||
|
|
||||||
|
@ -12,7 +12,10 @@ export type Message = {
|
||||||
export function messageFromBlah(payload: BlahSignedPayload<BlahMessage>): Message {
|
export function messageFromBlah(payload: BlahSignedPayload<BlahMessage>): Message {
|
||||||
return {
|
return {
|
||||||
id: payload.sig,
|
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,
|
content: payload.signee.payload.rich_text,
|
||||||
date: new Date(payload.signee.timestamp * 1000)
|
date: new Date(payload.signee.timestamp * 1000)
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import * as DropdownMenu from '$lib/components/DropdownMenu';
|
import * as DropdownMenu from '$lib/components/DropdownMenu';
|
||||||
import { AvatarBeam } from 'svelte-boring-avatars';
|
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||||
import { keyStore, currentKeyIndex, currentKeyPair } from '$lib/keystore';
|
import { keyStore, currentKeyIndex, currentKeyPair } from '$lib/keystore';
|
||||||
import { BlahKeyPair, generateName } from '$lib/blah/crypto';
|
import { BlahKeyPair } from '@blah-im/core/crypto';
|
||||||
|
|
||||||
let className: string = '';
|
let className: string = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
@ -11,7 +11,9 @@
|
||||||
let currentKeyName: string | null;
|
let currentKeyName: string | null;
|
||||||
$: {
|
$: {
|
||||||
currentKeyId = $currentKeyPair?.id;
|
currentKeyId = $currentKeyPair?.id;
|
||||||
currentKeyName = currentKeyId ? generateName(currentKeyId) : null;
|
currentKeyName = currentKeyId
|
||||||
|
? currentKeyId.slice(0, 4) + '...' + currentKeyId.slice(-4)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createKeyPair() {
|
async function createKeyPair() {
|
||||||
|
@ -48,7 +50,7 @@
|
||||||
onValueChange={setCurrentKeyIndex}
|
onValueChange={setCurrentKeyIndex}
|
||||||
>
|
>
|
||||||
{#each $keyStore as { id }, idx}
|
{#each $keyStore as { id }, idx}
|
||||||
{@const name = generateName(id)}
|
{@const name = id.slice(0, 4) + '...' + id.slice(-4)}
|
||||||
<DropdownMenu.RadioItem value={idx.toString()}>
|
<DropdownMenu.RadioItem value={idx.toString()}>
|
||||||
<div class="flex items-center gap-2 py-0.5">
|
<div class="flex items-center gap-2 py-0.5">
|
||||||
<AvatarBeam size={24} name={id} />
|
<AvatarBeam size={24} name={id} />
|
||||||
|
@ -58,7 +60,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item>Manage identities</DropdownMenu.Item>
|
<DropdownMenu.Item>Settings...</DropdownMenu.Item>
|
||||||
{:else}
|
{:else}
|
||||||
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
|
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
Loading…
Add table
Reference in a new issue