feat: implement end-to-end encryption for relay connections
This commit is contained in:
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
function buildCandidateUrls(daemon: HostProfile): string[] {
|
||||
const endpoints = daemon.endpoints ?? [];
|
||||
const sessionId = daemon.relay?.sessionId ?? null;
|
||||
const relayEndpoint = daemon.relay?.endpoint ?? null;
|
||||
const lastKnownGood =
|
||||
typeof daemon.metadata?.lastKnownGoodEndpoint === "string"
|
||||
? (daemon.metadata.lastKnownGoodEndpoint as string)
|
||||
@@ -20,21 +21,24 @@ function buildCandidateUrls(daemon: HostProfile): string[] {
|
||||
if (!out.includes(url)) out.push(url);
|
||||
};
|
||||
|
||||
if (lastKnownGood) {
|
||||
const isLastKnownRelay = !!relayEndpoint && lastKnownGood === relayEndpoint;
|
||||
const directEndpoints = relayEndpoint
|
||||
? endpoints.filter((endpoint) => endpoint !== relayEndpoint)
|
||||
: endpoints;
|
||||
|
||||
if (lastKnownGood && !isLastKnownRelay) {
|
||||
push(buildDaemonWebSocketUrl(lastKnownGood));
|
||||
}
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
for (const endpoint of directEndpoints) {
|
||||
push(buildDaemonWebSocketUrl(endpoint));
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
if (lastKnownGood) {
|
||||
push(buildRelayWebSocketUrl({ endpoint: lastKnownGood, sessionId }));
|
||||
}
|
||||
for (const endpoint of endpoints) {
|
||||
push(buildRelayWebSocketUrl({ endpoint, sessionId }));
|
||||
if (sessionId && relayEndpoint) {
|
||||
if (isLastKnownRelay) {
|
||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||
}
|
||||
push(buildRelayWebSocketUrl({ endpoint: relayEndpoint, sessionId }));
|
||||
}
|
||||
|
||||
return out;
|
||||
@@ -123,7 +127,12 @@ function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) {
|
||||
]);
|
||||
|
||||
return (
|
||||
<SessionProvider key={`${daemon.id}:${activeUrl}`} serverUrl={activeUrl} serverId={daemon.id}>
|
||||
<SessionProvider
|
||||
key={`${daemon.id}:${activeUrl}`}
|
||||
serverUrl={activeUrl}
|
||||
serverId={daemon.id}
|
||||
daemonPublicKeyB64={daemon.daemonPublicKeyB64}
|
||||
>
|
||||
{null}
|
||||
</SessionProvider>
|
||||
);
|
||||
|
||||
@@ -326,6 +326,7 @@ interface SessionProviderProps {
|
||||
children: ReactNode;
|
||||
serverUrl: string;
|
||||
serverId: string;
|
||||
daemonPublicKeyB64?: string;
|
||||
}
|
||||
|
||||
// SessionProvider: Daemon client message handler that updates Zustand store
|
||||
@@ -333,8 +334,9 @@ export function SessionProvider({
|
||||
children,
|
||||
serverUrl,
|
||||
serverId,
|
||||
daemonPublicKeyB64,
|
||||
}: SessionProviderProps) {
|
||||
const client = useDaemonClient(serverUrl);
|
||||
const client = useDaemonClient(serverUrl, { daemonPublicKeyB64 });
|
||||
const [connectionSnapshot, setConnectionSnapshot] =
|
||||
useState<DaemonConnectionSnapshot>(() =>
|
||||
mapConnectionState(client.getConnectionState(), client.lastError)
|
||||
|
||||
@@ -9,19 +9,44 @@ function runDaemonRequest(label: string, promise: Promise<unknown>): void {
|
||||
});
|
||||
}
|
||||
|
||||
export function useDaemonClient(url: string): DaemonClientV2 {
|
||||
type DaemonClientOptions = {
|
||||
daemonPublicKeyB64?: string;
|
||||
};
|
||||
|
||||
function isRelayWebSocketUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.searchParams.get("role") === "client" && parsed.searchParams.has("session");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function useDaemonClient(
|
||||
url: string,
|
||||
options: DaemonClientOptions = {}
|
||||
): DaemonClientV2 {
|
||||
const client = useMemo(
|
||||
() => {
|
||||
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
||||
const relayConnection = isRelayWebSocketUrl(url);
|
||||
return new DaemonClientV2({
|
||||
url,
|
||||
suppressSendErrors: true,
|
||||
...(tauriTransportFactory
|
||||
? { transportFactory: tauriTransportFactory }
|
||||
: {}),
|
||||
...(relayConnection
|
||||
? {
|
||||
e2ee: {
|
||||
enabled: true,
|
||||
daemonPublicKeyB64: options.daemonPublicKeyB64,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
[url]
|
||||
[options.daemonPublicKeyB64, url]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,16 +7,44 @@ declare global {
|
||||
}
|
||||
|
||||
export function polyfillCrypto(): void {
|
||||
if (typeof globalThis.crypto === "undefined") {
|
||||
(globalThis as any).crypto = {};
|
||||
let webcrypto: Crypto | null = null;
|
||||
try {
|
||||
// Prefer the React Native entrypoint to avoid the browser shim.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const rnModule = require("@sphereon/isomorphic-webcrypto/src/react-native");
|
||||
webcrypto = (rnModule?.default ?? rnModule) as Crypto;
|
||||
} catch {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fallbackModule = require("@sphereon/isomorphic-webcrypto");
|
||||
webcrypto = (fallbackModule?.default ?? fallbackModule) as Crypto;
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto.randomUUID !== "function") {
|
||||
const existing = (globalThis as any).crypto as Crypto | null | undefined;
|
||||
let target = existing;
|
||||
if (!target || typeof (target as Crypto).subtle === "undefined") {
|
||||
target = (webcrypto && typeof webcrypto === "object" ? (webcrypto as Crypto) : undefined) ?? ({} as Crypto);
|
||||
(globalThis as any).crypto = target;
|
||||
}
|
||||
|
||||
const ensureSecure = (globalThis as any).crypto?.ensureSecure as
|
||||
| (() => Promise<void>)
|
||||
| undefined;
|
||||
if (typeof ensureSecure === "function") {
|
||||
void ensureSecure();
|
||||
}
|
||||
|
||||
if (typeof (globalThis as any).crypto?.randomUUID !== "function") {
|
||||
if (!globalThis.crypto) {
|
||||
(globalThis as any).crypto = {} as Crypto;
|
||||
}
|
||||
globalThis.crypto.randomUUID = () =>
|
||||
ExpoCrypto.randomUUID() as `${string}-${string}-${string}-${string}-${string}`;
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto.getRandomValues !== "function") {
|
||||
if (typeof (globalThis as any).crypto?.getRandomValues !== "function") {
|
||||
if (!globalThis.crypto) {
|
||||
(globalThis as any).crypto = {} as Crypto;
|
||||
}
|
||||
globalThis.crypto.getRandomValues = <T extends ArrayBufferView>(array: T): T => {
|
||||
return ExpoCrypto.getRandomValues(array as any) as T;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user