Initial commit: mumh5, a modern Mumble client
Electron desktop app with a Svelte 5 interface for any Mumble server. - Mumble protocol core: TLS, handshake, channels, users, text, plugin data, client-side pacing of Murmur's rate limits - Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk, always-on, devices, per-user volume and local mute - Several servers at once, voice on one; server rail with icons and ordering - Chat: channels, direct messages, side chat, file sharing through f0ckm, inline images without it, click-to-play YouTube - Profiles with rich descriptions, registration, rename, nicknames, connection information and moderation menus - Identity wizard, multiple identities, PKCS#12 import/export, desktop Mumble certificate import, certificate pinning and viewer - Tray icon with voice state - Unit, server and end-to-end tests against a real Murmur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import forge from 'node-forge';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export interface Identity {
|
||||
certPem: string;
|
||||
keyPem: string;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
// Self-signed client certificate, the same kind the desktop Mumble client generates.
|
||||
// Mumble servers identify registered users by the hash of this certificate.
|
||||
export function generateIdentity(name: string, email = ''): Identity {
|
||||
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = '01' + forge.util.bytesToHex(forge.random.getBytesSync(8));
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 20);
|
||||
const attrs: { name: string; value: string }[] = [{ name: 'commonName', value: name || 'mumh5 user' }];
|
||||
if (email) attrs.push({ name: 'emailAddress', value: email });
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.setExtensions([
|
||||
{ name: 'basicConstraints', cA: false },
|
||||
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
|
||||
{ name: 'extKeyUsage', clientAuth: true }
|
||||
]);
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
const certPem = forge.pki.certificateToPem(cert);
|
||||
return { certPem, keyPem: forge.pki.privateKeyToPem(keys.privateKey), fingerprint: certFingerprint(certPem) };
|
||||
}
|
||||
|
||||
// SHA-1 of the DER certificate, the hash Mumble shows and stores for users.
|
||||
export function certFingerprint(certPem: string): string {
|
||||
const der = forge.asn1.toDer(forge.pki.certificateToAsn1(forge.pki.certificateFromPem(certPem))).getBytes();
|
||||
return createHash('sha1').update(Buffer.from(der, 'binary')).digest('hex');
|
||||
}
|
||||
|
||||
// Read a PKCS#12 file (.p12/.pfx, as exported by the desktop Mumble client).
|
||||
export function identityFromP12(bytes: Uint8Array, password: string): Identity {
|
||||
let p12: forge.pkcs12.Pkcs12Pfx;
|
||||
try {
|
||||
const der = forge.util.binary.raw.encode(bytes);
|
||||
p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(der), password);
|
||||
} catch {
|
||||
throw new Error('Could not open the file. Wrong password, or not a PKCS#12 certificate.');
|
||||
}
|
||||
const bags = (type: string) => p12.getBags({ bagType: type })[type] ?? [];
|
||||
const cert = bags(forge.pki.oids.certBag).find(b => b.cert)?.cert;
|
||||
const key = [...bags(forge.pki.oids.pkcs8ShroudedKeyBag), ...bags(forge.pki.oids.keyBag)].find(b => b.key)?.key;
|
||||
if (!cert || !key) throw new Error('The file does not contain both a certificate and its private key.');
|
||||
const certPem = forge.pki.certificateToPem(cert);
|
||||
return { certPem, keyPem: forge.pki.privateKeyToPem(key), fingerprint: certFingerprint(certPem) };
|
||||
}
|
||||
|
||||
// PKCS#12 with 3DES, which the desktop Mumble client and other tools can import.
|
||||
export function identityToP12(id: Identity, password: string, friendlyName: string): Uint8Array {
|
||||
const asn1 = forge.pkcs12.toPkcs12Asn1(
|
||||
forge.pki.privateKeyFromPem(id.keyPem),
|
||||
[forge.pki.certificateFromPem(id.certPem)],
|
||||
password,
|
||||
{ algorithm: '3des', friendlyName }
|
||||
);
|
||||
return new Uint8Array(Buffer.from(forge.asn1.toDer(asn1).getBytes(), 'binary'));
|
||||
}
|
||||
|
||||
// Common name of a certificate, used as the default identity name
|
||||
export function certCommonName(certPem: string): string {
|
||||
const cn = forge.pki.certificateFromPem(certPem).subject.getField('CN');
|
||||
return cn ? String(cn.value) : '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user