~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-webrtc

WebRTC transport for atlibs: com.fedfork.exec.webrtc.connect in the browser, and webrtc-peer, its guest side

0
0
main
67 lines · 3.0 KB Raw History
// SSH over an open WebRTC datachannel (a tag bridged to the guest's sshd on 22), with
// @microsoft/dev-tunnels-ssh (pure TS, WebCrypto) injected as `ssh` ({...namespace, Buffer}).
// The library speaks ecdsa/rsa host keys, not ed25519, so the host key is reported, not
// pinned: the guest is authenticated by the DTLS fingerprint the bidder signed (vm.onNetwork).

// ponytail: one exec channel per command (no pty); add a shell channel + xterm if a real terminal is wanted.
// received: what the channel delivered before ssh took it over (sshd speaks first: its banner).
export async function connectSsh(ssh, dc, keyPair, user, received = []) {
	class DataChannelStream extends ssh.BaseStream {
		constructor() {
			super();
			for (const data of received) this.onData(ssh.Buffer.from(data));
			dc.onmessage = ({ data }) => this.onData(ssh.Buffer.from(data));
			dc.onclose = () => this.onEnd();
		}
		async write(data) {
			dc.send(data);
		}
		async close() {
			dc.close();
		}
	}
	const session = new ssh.SshClientSession(new ssh.SshSessionConfiguration());
	let hostKey;
	session.onAuthenticating((e) => {
		hostKey = e.publicKey;
		e.authenticationPromise = Promise.resolve({});
	});
	await session.connect(new DataChannelStream());
	if (!(await session.authenticateServer())) throw new Error("ssh: server authentication failed");
	const digest = await crypto.subtle.digest("SHA-256", await hostKey.getPublicKeyBytes());
	const fingerprint = `SHA256:${new Uint8Array(digest).toBase64({ omitPadding: true })}`;
	if (!(await session.authenticateClient({ username: user, publicKeys: [keyPair] }))) throw new Error(`ssh: ${user} rejected the key`);
	return {
		hostKey: { type: hostKey.keyAlgorithmName, fingerprint },
		// stdin (optional): written to the command, then EOF; secrets go here, never in command
		// (the guest's shell runs the command as an argument, so ps shows it).
		async exec(command, stdin) {
			const channel = await session.openChannel();
			let output = "";
			const decoder = new TextDecoder();
			channel.onDataReceived((data) => {
				output += decoder.decode(data, { stream: true });
				channel.adjustWindow(data.length);
			});
			const closed = new Promise((resolve) => channel.onClosed(resolve));
			const request = new ssh.CommandRequestMessage();
			request.requestType = "exec";
			request.command = command;
			request.wantReply = true;
			if (!(await channel.request(request))) throw new Error("ssh: exec refused");
			if (stdin !== undefined) {
				if (stdin) await channel.send(ssh.Buffer.from(stdin));
				await channel.send(ssh.Buffer.alloc(0)); // an empty send is EOF
			}
			await closed;
			return output;
		},
		close: () => session.close(),
	};
}

// An ecdsa-sha2-nistp256 key pair and its authorized_keys line.
export async function sshKeyPair(ssh) {
	const keyPair = await ssh.SshAlgorithms.publicKey.ecdsaSha2Nistp256.generateKeyPair();
	return { keyPair, authorizedKey: `ecdsa-sha2-nistp256 ${(await keyPair.getPublicKeyBytes()).toString("base64")} webrtc-page` };
}