├── .gitignore ├── src ├── mods │ ├── meek │ │ ├── index.ts │ │ └── meek.ts │ ├── tor │ │ ├── algorithms │ │ │ ├── index.test.ts │ │ │ ├── ntor │ │ │ │ ├── index.ts │ │ │ │ └── ntor.ts │ │ │ ├── index.ts │ │ │ └── kdftor.ts │ │ ├── certs │ │ │ ├── index.ts │ │ │ └── certs.ts │ │ ├── consensus │ │ │ ├── index.ts │ │ │ ├── index.test.ts │ │ │ └── consensus.test.ts │ │ ├── binary │ │ │ ├── certs │ │ │ │ ├── cross │ │ │ │ │ ├── index.ts │ │ │ │ │ └── cert.ts │ │ │ │ ├── ed25519 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── extensions │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── signer.ts │ │ │ │ │ └── cert.ts │ │ │ │ ├── rsa │ │ │ │ │ ├── index.ts │ │ │ │ │ └── cert.ts │ │ │ │ └── index.ts │ │ │ ├── cells │ │ │ │ ├── direct │ │ │ │ │ ├── certs │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── create2 │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── destroy │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── netinfo │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── padding │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── versions │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── vpadding │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── auth_challenge │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── create_fast │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── created_fast │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_early │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── padding_negociate │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── relayed │ │ │ │ │ ├── relay_begin │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_data │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_drop │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_sendme │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_truncate │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_begin_dir │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_connected │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_extended2 │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_truncated │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_end │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── reason.ts │ │ │ │ │ │ └── cell.ts │ │ │ │ │ ├── relay_extend2 │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── cell.ts │ │ │ │ │ │ └── link.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── cell.ts │ │ │ │ └── old.ts │ │ │ ├── index.ts │ │ │ └── address.ts │ │ ├── index.test.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── state.ts │ │ ├── errors.ts │ │ ├── target.ts │ │ ├── ciphers.ts │ │ ├── stream.ts │ │ └── circuit.ts │ ├── snowflake │ │ ├── turbo │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── reader.ts │ │ │ ├── writer.ts │ │ │ ├── frame.test.ts │ │ │ ├── stream.ts │ │ │ └── frame.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── snowflake.ts │ ├── index.test.ts │ ├── index.ts │ └── console │ │ └── index.ts ├── index.bench.ts ├── index.test.ts ├── libs │ ├── promises │ │ └── index.ts │ ├── typescript │ │ └── typescript.ts │ ├── dates │ │ └── dates.ts │ ├── transports │ │ └── http.ts │ └── resizer │ │ └── resizer.ts ├── index.ts └── bench │ ├── index.bench.ts │ └── bitset │ ├── write.bench.ts │ └── read.bench.ts ├── assets └── banner.psd ├── test └── website │ ├── styles │ └── globals.css │ ├── postcss.config.js │ ├── next.config.js │ ├── pages │ ├── _app.tsx │ ├── index.tsx │ ├── dir.tsx │ ├── http.tsx │ └── socket.tsx │ ├── tailwind.config.js │ ├── src │ └── libs │ │ ├── pool │ │ └── index.ts │ │ ├── tors │ │ └── index.ts │ │ ├── transport │ │ └── socket.ts │ │ ├── sockets │ │ └── index.ts │ │ └── circuits │ │ └── index.ts │ ├── .eslintrc.json │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── .gitattributes ├── .github └── FUNDING.yml ├── .vscode └── settings.json ├── tsconfig.json ├── tools ├── http_http_proxy.ts └── ws_tcp_proxy.ts ├── test.ts ├── LICENSE.md ├── rollup.config.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .DS_Store -------------------------------------------------------------------------------- /src/mods/meek/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./meek.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/algorithms/index.test.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | -------------------------------------------------------------------------------- /src/index.bench.ts: -------------------------------------------------------------------------------- 1 | export * from "./bench/index.bench.js"; 2 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | export * from "./mods/index.test.js"; 2 | 3 | -------------------------------------------------------------------------------- /src/libs/promises/index.ts: -------------------------------------------------------------------------------- 1 | export type Awaitable = T | Promise -------------------------------------------------------------------------------- /src/mods/tor/certs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./certs.js"; 2 | 3 | -------------------------------------------------------------------------------- /src/mods/tor/consensus/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./consensus.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/cross/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cert.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/ed25519/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cert.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/rsa/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cert.js"; 2 | -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/index.test.ts: -------------------------------------------------------------------------------- 1 | export * from "./frame.test.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/algorithms/ntor/index.ts: -------------------------------------------------------------------------------- 1 | export * as Ntor from "./ntor.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/certs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/relay/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/consensus/index.test.ts: -------------------------------------------------------------------------------- 1 | export * from "./consensus.test.js"; 2 | -------------------------------------------------------------------------------- /src/mods/snowflake/index.test.ts: -------------------------------------------------------------------------------- 1 | export * from "./turbo/index.test.js"; 2 | 3 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/create2/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/destroy/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/netinfo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/padding/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/versions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/vpadding/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /assets/banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hazae41/echalote/HEAD/assets/banner.psd -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/auth_challenge/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/create_fast/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/created_fast/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/relay_early/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_begin/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_drop/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_sendme/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_truncate/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/ed25519/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./signer.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/padding_negociate/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_begin_dir/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_connected/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_extended2/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_truncated/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./frame.js" 2 | export * from "./stream.js" 3 | -------------------------------------------------------------------------------- /test/website/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mods/index.js"; 2 | export * as Echalote from "./mods/index.js"; 3 | 4 | -------------------------------------------------------------------------------- /src/libs/typescript/typescript.ts: -------------------------------------------------------------------------------- 1 | export type Mutable = { 2 | -readonly [P in keyof T]: T[P] 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored=true 2 | *.ts linguist-vendored=false 3 | dist/* linguist-vendored=true -------------------------------------------------------------------------------- /src/mods/snowflake/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./snowflake.js"; 2 | export * from "./turbo/index.js"; 3 | 4 | -------------------------------------------------------------------------------- /src/mods/tor/algorithms/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kdftor.js"; 2 | export * from "./ntor/index.js"; 3 | 4 | -------------------------------------------------------------------------------- /src/mods/index.test.ts: -------------------------------------------------------------------------------- 1 | export * from "./snowflake/index.test.js"; 2 | export * from "./tor/index.test.js"; 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [hazae41] 4 | patreon: hazae41 5 | -------------------------------------------------------------------------------- /src/bench/index.bench.ts: -------------------------------------------------------------------------------- 1 | export * from "./bitset/read.bench.js" 2 | export * from "./bitset/write.bench.js" 3 | 4 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_end/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | export * from "./reason.js"; 3 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_extend2/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | export * from "./link.js"; 3 | -------------------------------------------------------------------------------- /src/mods/tor/index.test.ts: -------------------------------------------------------------------------------- 1 | export * from "./algorithms/index.test.js"; 2 | export * from "./consensus/index.test.js"; 3 | -------------------------------------------------------------------------------- /test/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.unstable": true, 4 | "deno.enablePaths": [ 5 | "./tools", 6 | ] 7 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./address.js"; 2 | export * from "./cells/index.js"; 3 | export * from "./certs/index.js"; 4 | 5 | -------------------------------------------------------------------------------- /src/mods/tor/constants.ts: -------------------------------------------------------------------------------- 1 | export type HASH_LEN = 20 2 | export const HASH_LEN = 20 3 | 4 | export type KEY_LEN = 16 5 | export const KEY_LEN = 16 -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cross/index.js"; 2 | export * from "./ed25519/index.js"; 3 | export * from "./rsa/index.js"; 4 | 5 | -------------------------------------------------------------------------------- /src/mods/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./console/index.js"; 2 | export * from "./meek/index.js"; 3 | export * from "./snowflake/index.js"; 4 | export * from "./tor/index.js"; 5 | -------------------------------------------------------------------------------- /test/website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cell.js"; 2 | export * from "./direct/index.js"; 3 | export * from "./errors.js"; 4 | export * from "./relayed/index.js"; 5 | -------------------------------------------------------------------------------- /test/website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@hazae41/symbol-dispose-polyfill" 2 | import type { AppProps } from 'next/app' 3 | import '../styles/globals.css' 4 | 5 | export default function MyApp({ Component, pageProps }: AppProps) { 6 | return 7 | } -------------------------------------------------------------------------------- /test/website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Page() { 4 | return <> 5 |
6 | http 7 |
8 |
9 | socket 10 |
11 | 12 | } -------------------------------------------------------------------------------- /src/mods/meek/meek.ts: -------------------------------------------------------------------------------- 1 | import { BatchedFetchStream } from "libs/transports/http.js" 2 | 3 | export async function createMeekStream(url: string) { 4 | const headers = { "x-session-id": crypto.randomUUID() } 5 | const request = new Request(url, { headers }) 6 | 7 | return new BatchedFetchStream(request) 8 | } -------------------------------------------------------------------------------- /test/website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | corePlugins: { 4 | preflight: false 5 | }, 6 | content: [ 7 | "./app/**/*.{js,ts,jsx,tsx}", 8 | "./pages/**/*.{js,ts,jsx,tsx}", 9 | "./src/**/*.{js,ts,jsx,tsx}", 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | } 16 | -------------------------------------------------------------------------------- /test/website/src/libs/pool/index.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from "@hazae41/piscine"; 2 | 3 | export class SizedPool { 4 | 5 | private constructor( 6 | readonly pool: Pool, 7 | readonly size: N, 8 | ) { } 9 | 10 | static start(pool: Pool, size: N) { 11 | for (let i = 0; i < size; i++) 12 | pool.start(i) 13 | return new SizedPool(pool, size) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /test/website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": "next/core-web-vitals", 4 | "parserOptions": { 5 | "requireConfigFile": false, 6 | "babelOptions": { 7 | "babelrc": false, 8 | "configFile": false, 9 | "presets": [ 10 | "next/babel" 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-explicit-resource-management" 14 | ] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/mods/tor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./algorithms/index.js"; 2 | export * from "./binary/index.js"; 3 | export * from "./certs/index.js"; 4 | export * from "./ciphers.js"; 5 | export * from "./circuit.js"; 6 | export * from "./client.js"; 7 | export * from "./consensus/index.js"; 8 | export * from "./constants.js"; 9 | export * from "./errors.js"; 10 | export * from "./state.js"; 11 | export * from "./stream.js"; 12 | export * from "./target.js"; 13 | 14 | -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/reader.ts: -------------------------------------------------------------------------------- 1 | import { Opaque } from "@hazae41/binary" 2 | import { TurboFrame } from "./frame.js" 3 | import { SecretTurboDuplex } from "./stream.js" 4 | 5 | export class SecretTurboReader { 6 | 7 | constructor( 8 | readonly parent: SecretTurboDuplex 9 | ) { } 10 | 11 | async onWrite(chunk: Opaque) { 12 | const frame = chunk.readIntoOrThrow(TurboFrame) 13 | 14 | if (frame.padding) 15 | return 16 | 17 | this.parent.input.enqueue(frame.fragment) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./relay_begin/index.js"; 2 | export * from "./relay_begin_dir/index.js"; 3 | export * from "./relay_connected/index.js"; 4 | export * from "./relay_data/index.js"; 5 | export * from "./relay_drop/index.js"; 6 | export * from "./relay_end/index.js"; 7 | export * from "./relay_extend2/index.js"; 8 | export * from "./relay_extended2/index.js"; 9 | export * from "./relay_sendme/index.js"; 10 | export * from "./relay_truncate/index.js"; 11 | export * from "./relay_truncated/index.js"; 12 | -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/ed25519/extensions/signer.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | 4 | export class SignedWithEd25519Key { 5 | readonly #class = SignedWithEd25519Key 6 | 7 | static readonly type = 4 8 | 9 | constructor( 10 | readonly key: Uint8Array<32> 11 | ) { } 12 | 13 | get type(): 4 { 14 | return this.#class.type 15 | } 16 | 17 | static readOrThrow(cursor: Cursor) { 18 | return new SignedWithEd25519Key(cursor.readAndCopyOrThrow(32)) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "baseUrl": "./src", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "strict": true, 8 | "downlevelIteration": true, 9 | "lib": [ 10 | "DOM", 11 | "ESNext" 12 | ], 13 | "paths": { 14 | "mods/*": [ 15 | "mods/*" 16 | ], 17 | "libs/*": [ 18 | "libs/*" 19 | ], 20 | "tests/*": [ 21 | "tests/*" 22 | ], 23 | } 24 | }, 25 | "include": [ 26 | "./src/**/**.ts", 27 | ], 28 | } -------------------------------------------------------------------------------- /test/website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth_challenge/index.js"; 2 | export * from "./certs/index.js"; 3 | export * from "./create2/index.js"; 4 | export * from "./created_fast/index.js"; 5 | export * from "./create_fast/index.js"; 6 | export * from "./destroy/index.js"; 7 | export * from "./netinfo/index.js"; 8 | export * from "./padding/index.js"; 9 | export * from "./padding_negociate/index.js"; 10 | export * from "./relay/index.js"; 11 | export * from "./relay_early/index.js"; 12 | export * from "./versions/index.js"; 13 | export * from "./vpadding/index.js"; 14 | 15 | -------------------------------------------------------------------------------- /src/mods/console/index.ts: -------------------------------------------------------------------------------- 1 | export namespace Console { 2 | export let debugging = false 3 | 4 | export function log(...params: any[]) { 5 | if (!debugging) 6 | return 7 | console.log(...params) 8 | } 9 | 10 | export function debug(...params: any[]) { 11 | if (!debugging) 12 | return 13 | console.debug(...params) 14 | } 15 | 16 | export function error(...params: any[]) { 17 | if (!debugging) 18 | return 19 | console.error(...params) 20 | } 21 | 22 | export function warn(...params: any[]) { 23 | if (!debugging) 24 | return 25 | console.warn(...params) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/mods/tor/state.ts: -------------------------------------------------------------------------------- 1 | import { Guard } from "./client.js" 2 | 3 | export type TorState = 4 | | TorNoneState 5 | | TorVersionedState 6 | | TorHandshakingState 7 | | TorHandshakedState 8 | 9 | export interface TorNoneState { 10 | readonly type: "none" 11 | } 12 | 13 | export interface TorVersionedState { 14 | readonly type: "versioned", 15 | readonly version: number 16 | } 17 | 18 | export interface TorHandshakingState { 19 | readonly type: "handshaking", 20 | readonly version: number 21 | readonly guard: Guard 22 | } 23 | 24 | export interface TorHandshakedState { 25 | readonly type: "handshaked", 26 | readonly version: number 27 | readonly guard: Guard 28 | } -------------------------------------------------------------------------------- /tools/http_http_proxy.ts: -------------------------------------------------------------------------------- 1 | Deno.serve({ port: 8080 }, async (request: Request) => { 2 | if (request.method === "OPTIONS") { 3 | const headers = new Headers() 4 | headers.set("Access-Control-Allow-Origin", "*") 5 | headers.set("Access-Control-Allow-Headers", "*") 6 | 7 | return new Response(undefined, { headers }) 8 | } 9 | 10 | const response = await fetch("https://meek.bamsoftware.com/", request) 11 | 12 | const { body, status } = response 13 | 14 | const headers = new Headers(response.headers) 15 | headers.set("Access-Control-Allow-Origin", "*") 16 | headers.set("Access-Control-Allow-Headers", "*") 17 | 18 | return new Response(body, { headers, status }) 19 | }); -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/vpadding/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | 3 | export class VariablePaddingCell { 4 | readonly #class = VariablePaddingCell 5 | 6 | static readonly circuit = false 7 | static readonly command = 128 8 | 9 | constructor( 10 | readonly data: Uint8Array 11 | ) { } 12 | 13 | get command() { 14 | return this.#class.command 15 | } 16 | 17 | sizeOrThrow() { 18 | return this.data.length 19 | } 20 | 21 | writeOrThrow(cursor: Cursor) { 22 | cursor.writeOrThrow(this.data) 23 | } 24 | 25 | static readOrThrow(cursor: Cursor) { 26 | return new VariablePaddingCell(cursor.readAndCopyOrThrow(cursor.remaining)) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_begin_dir/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | 3 | export class RelayBeginDirCell { 4 | readonly #class = RelayBeginDirCell 5 | 6 | static readonly early = false 7 | static readonly stream = true 8 | static readonly rcommand = 13 9 | 10 | constructor() { } 11 | 12 | get early(): false { 13 | return this.#class.early 14 | } 15 | 16 | get stream(): true { 17 | return this.#class.stream 18 | } 19 | 20 | get rcommand() { 21 | return this.#class.rcommand 22 | } 23 | 24 | sizeOrThrow() { 25 | return 0 26 | } 27 | 28 | writeOrThrow(cursor: Cursor) { 29 | cursor.fillOrThrow(0, cursor.remaining) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/padding/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | import { Unimplemented } from "mods/tor/errors.js" 3 | 4 | export class PaddingCell { 5 | readonly #class = PaddingCell 6 | 7 | static readonly circuit = false 8 | static readonly command = 0 9 | 10 | constructor( 11 | readonly data: Uint8Array 12 | ) { } 13 | 14 | get command() { 15 | return this.#class.command 16 | } 17 | 18 | sizeOrThrow(): never { 19 | throw new Unimplemented() 20 | } 21 | 22 | writeOrThrow(cursor: Cursor): never { 23 | throw new Unimplemented() 24 | } 25 | 26 | static readOrThrow(cursor: Cursor) { 27 | return new PaddingCell(cursor.readAndCopyOrThrow(cursor.remaining)) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /test/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "target": "ES2022", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmit": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "preserve", 22 | "incremental": true 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } -------------------------------------------------------------------------------- /src/mods/tor/errors.ts: -------------------------------------------------------------------------------- 1 | export class Unimplemented extends Error { 2 | readonly #class = Unimplemented 3 | readonly name = this.#class.name 4 | 5 | constructor() { 6 | super(`Unimplemented`) 7 | } 8 | } 9 | 10 | export type TorClientError = 11 | | InvalidTorStateError 12 | | InvalidTorVersionError 13 | 14 | export class InvalidTorStateError extends Error { 15 | readonly #class = InvalidTorStateError 16 | readonly name = this.#class.name 17 | 18 | constructor() { 19 | super(`Invalid Tor state`) 20 | } 21 | 22 | } 23 | 24 | export class InvalidTorVersionError extends Error { 25 | readonly #class = InvalidTorVersionError 26 | readonly name = this.#class.name 27 | 28 | constructor() { 29 | super(`Invalid Tor version`) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/writer.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { TurboFrame } from "./frame.js"; 3 | import { SecretTurboDuplex } from "./stream.js"; 4 | 5 | export class SecretTurboWriter { 6 | 7 | constructor( 8 | readonly parent: SecretTurboDuplex 9 | ) { } 10 | 11 | async onStart() { 12 | await this.parent.resolveOnStart.promise 13 | 14 | const token = this.parent.class.token 15 | this.parent.output.enqueue(new Opaque(token)) 16 | 17 | const client = this.parent.client 18 | this.parent.output.enqueue(new Opaque(client)) 19 | } 20 | 21 | async onWrite(fragment: Writable) { 22 | const frame = TurboFrame.createOrThrow({ padding: false, fragment }) 23 | this.parent.output.enqueue(frame) 24 | } 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/frame.test.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Readable, Writable } from "@hazae41/binary"; 2 | import { Bytes } from "@hazae41/bytes"; 3 | import { assert, test } from "@hazae41/phobos"; 4 | import { relative, resolve } from "path"; 5 | import { TurboFrame } from "./frame.js"; 6 | 7 | const directory = resolve("./dist/test/") 8 | const { pathname } = new URL(import.meta.url) 9 | console.log(relative(directory, pathname.replace(".mjs", ".ts"))) 10 | 11 | test("turbo frame", async ({ test }) => { 12 | const frame = TurboFrame.createOrThrow({ padding: false, fragment: new Opaque(Bytes.random(130)) }) 13 | const bytes = Writable.writeToBytesOrThrow(frame) 14 | const frame2 = Readable.readFromBytesOrThrow(TurboFrame, bytes) 15 | 16 | assert(Bytes.equals2(frame.fragment.bytes, frame2.fragment.bytes)) 17 | }) -------------------------------------------------------------------------------- /src/mods/tor/target.ts: -------------------------------------------------------------------------------- 1 | import { Aes128Ctr128BEKey } from "@hazae41/aes.wasm"; 2 | import { Uint8Array } from "@hazae41/bytes"; 3 | import type { Sha1 } from "@hazae41/sha1"; 4 | import { SecretCircuit } from "mods/tor/circuit.js"; 5 | 6 | export class Target { 7 | readonly #class = Target 8 | 9 | delivery = 1000 10 | package = 1000 11 | 12 | digests = new Array>() 13 | 14 | constructor( 15 | readonly relayid_rsa: Uint8Array, 16 | readonly circuit: SecretCircuit, 17 | readonly forward_digest: Sha1.Hasher, 18 | readonly backward_digest: Sha1.Hasher, 19 | readonly forward_key: Aes128Ctr128BEKey, 20 | readonly backward_key: Aes128Ctr128BEKey 21 | ) { } 22 | 23 | [Symbol.dispose]() { 24 | this.forward_digest[Symbol.dispose]() 25 | this.backward_digest[Symbol.dispose]() 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/libs/dates/dates.ts: -------------------------------------------------------------------------------- 1 | export namespace Dates { 2 | 3 | export function fromMillis(millis: number) { 4 | return new Date(millis) 5 | } 6 | 7 | export function toMillis(date: Date) { 8 | return date.getTime() 9 | } 10 | 11 | export function fromSeconds(seconds: number) { 12 | return fromMillis(seconds * 1000) 13 | } 14 | 15 | export function toSeconds(date: Date) { 16 | return Math.floor(toMillis(date) / 1000) 17 | } 18 | 19 | export function fromMillisDelay(millis: number) { 20 | return fromMillis(Date.now() + millis) 21 | } 22 | 23 | export function toMillisDelay(date: Date) { 24 | return toMillis(date) - Date.now() 25 | } 26 | 27 | export function fromSecondsDelay(seconds: number) { 28 | return fromMillisDelay(seconds * 1000) 29 | } 30 | 31 | export function toSecondsDelay(date: Date) { 32 | return Math.floor(toMillisDelay(date) / 1000) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_data/cell.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | 4 | export class RelayDataCell { 5 | readonly #class = RelayDataCell 6 | 7 | static readonly early = false 8 | static readonly stream = true 9 | static readonly rcommand = 2 10 | 11 | constructor( 12 | readonly fragment: T 13 | ) { } 14 | 15 | get early(): false { 16 | return this.#class.early 17 | } 18 | 19 | get stream(): true { 20 | return this.#class.stream 21 | } 22 | 23 | get rcommand(): 2 { 24 | return this.#class.rcommand 25 | } 26 | 27 | sizeOrThrow() { 28 | return this.fragment.sizeOrThrow() 29 | } 30 | 31 | writeOrThrow(cursor: Cursor) { 32 | this.fragment.writeOrThrow(cursor) 33 | } 34 | 35 | static readOrThrow(cursor: Cursor) { 36 | return new RelayDataCell(new Opaque(cursor.readAndCopyOrThrow(cursor.remaining))) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_drop/cell.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | 4 | export class RelayDropCell { 5 | readonly #class = RelayDropCell 6 | 7 | static readonly early = false 8 | static readonly stream = true 9 | static readonly rcommand = 10 10 | 11 | constructor( 12 | readonly fragment: T 13 | ) { } 14 | 15 | get early(): false { 16 | return this.#class.early 17 | } 18 | 19 | get stream(): true { 20 | return this.#class.stream 21 | } 22 | 23 | get rcommand(): 10 { 24 | return this.#class.rcommand 25 | } 26 | 27 | sizeOrThrow() { 28 | return this.fragment.sizeOrThrow() 29 | } 30 | 31 | writeOrThrow(cursor: Cursor) { 32 | this.fragment.writeOrThrow(cursor) 33 | } 34 | 35 | static readOrThrow(cursor: Cursor) { 36 | return new RelayDropCell(new Opaque(cursor.readAndCopyOrThrow(cursor.remaining))) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/bench/bitset/write.bench.ts: -------------------------------------------------------------------------------- 1 | import { Bitset } from "@hazae41/bitset" 2 | import { benchSync } from "@hazae41/deimos" 3 | import { relative, resolve } from "path" 4 | 5 | const samples = 100_000 6 | 7 | const a = true 8 | const b = true 9 | const c = 63 10 | 11 | const directory = resolve("./dist/bench/") 12 | const { pathname } = new URL(import.meta.url) 13 | console.log(relative(directory, pathname.replace(".mjs", ".ts"))) 14 | 15 | const resArith = benchSync("arithmetic", () => { 16 | const bitset = new Bitset(c, 8) 17 | bitset.setBE(0, a) 18 | bitset.setBE(1, b) 19 | bitset.unsign() 20 | 21 | console.assert(bitset.value === 255) 22 | }, { samples }) 23 | 24 | const resString = benchSync("string", () => { 25 | let bitset = c.toString(2).padStart(8, "0").split("") 26 | bitset[0] = a ? "1" : "0" 27 | bitset[1] = b ? "1" : "0" 28 | 29 | const x = parseInt(bitset.join(""), 2) 30 | 31 | console.assert(x === 255) 32 | }, { samples }) 33 | 34 | resString.tableAndSummary(resArith) -------------------------------------------------------------------------------- /src/mods/snowflake/snowflake.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary" 2 | import { KcpDuplex } from "@hazae41/kcp" 3 | import { SmuxDuplex } from "@hazae41/smux" 4 | import { TurboDuplex } from "mods/snowflake/turbo/stream.js" 5 | 6 | export function createSnowflakeStream(raw: { outer: ReadableWritablePair }): { outer: ReadableWritablePair } { 7 | const turbo = new TurboDuplex() 8 | const kcp = new KcpDuplex({ lowDelay: 100, highDelay: 1000 }) 9 | const smux = new SmuxDuplex() 10 | 11 | raw.outer.readable.pipeTo(turbo.inner.writable).catch(() => { }) 12 | turbo.inner.readable.pipeTo(raw.outer.writable).catch(() => { }) 13 | 14 | turbo.outer.readable.pipeTo(kcp.inner.writable).catch(() => { }) 15 | kcp.inner.readable.pipeTo(turbo.outer.writable).catch(() => { }) 16 | 17 | kcp.outer.readable.pipeTo(smux.inner.writable).catch(() => { }) 18 | smux.inner.readable.pipeTo(kcp.outer.writable).catch(() => { }) 19 | 20 | return smux 21 | } 22 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_truncate/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | import { DestroyCell } from "mods/tor/binary/cells/direct/destroy/cell.js"; 3 | 4 | export class RelayTruncateCell { 5 | readonly #class = RelayTruncateCell 6 | 7 | static readonly early = false 8 | static readonly stream = false 9 | static readonly rcommand = 8 10 | 11 | static readonly reasons = DestroyCell.reasons 12 | 13 | constructor( 14 | readonly reason: number 15 | ) { } 16 | 17 | get early(): false { 18 | return this.#class.early 19 | } 20 | 21 | get stream(): false { 22 | return this.#class.stream 23 | } 24 | 25 | get rcommand(): 8 { 26 | return this.#class.rcommand 27 | } 28 | 29 | sizeOrThrow() { 30 | return 1 31 | } 32 | 33 | writeOrThrow(cursor: Cursor) { 34 | cursor.writeUint8OrThrow(this.reason) 35 | } 36 | 37 | static readOrThrow(cursor: Cursor) { 38 | return new RelayTruncateCell(cursor.readUint8OrThrow()) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_truncated/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | import { DestroyCell } from "mods/tor/binary/cells/direct/destroy/cell.js"; 3 | 4 | export class RelayTruncatedCell { 5 | readonly #class = RelayTruncatedCell 6 | 7 | static readonly early = false 8 | static readonly stream = false 9 | static readonly rcommand = 9 10 | 11 | static readonly reasons = DestroyCell.reasons 12 | 13 | constructor( 14 | readonly reason: number 15 | ) { } 16 | 17 | get early(): false { 18 | return this.#class.early 19 | } 20 | 21 | get stream(): false { 22 | return this.#class.stream 23 | } 24 | 25 | get rcommand(): 9 { 26 | return this.#class.rcommand 27 | } 28 | 29 | sizeOrThrow() { 30 | return 1 31 | } 32 | 33 | writeOrThrow(cursor: Cursor) { 34 | cursor.writeUint8OrThrow(this.reason) 35 | } 36 | 37 | static readOrThrow(cursor: Cursor) { 38 | return new RelayTruncatedCell(cursor.readUint8OrThrow()) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/created_fast/cell.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | 4 | export class CreatedFastCell { 5 | readonly #class = CreatedFastCell 6 | 7 | static readonly old = false 8 | static readonly circuit = true 9 | static readonly command = 6 10 | 11 | constructor( 12 | readonly material: Uint8Array<20>, 13 | readonly derivative: Uint8Array<20> 14 | ) { } 15 | 16 | get command() { 17 | return this.#class.command 18 | } 19 | 20 | sizeOrThrow() { 21 | return this.material.length + this.derivative.length 22 | } 23 | 24 | writeOrThrow(cursor: Cursor) { 25 | cursor.writeOrThrow(this.material) 26 | cursor.writeOrThrow(this.derivative) 27 | } 28 | 29 | static readOrThrow(cursor: Cursor) { 30 | const material = cursor.readAndCopyOrThrow(20) 31 | const derivative = cursor.readAndCopyOrThrow(20) 32 | 33 | cursor.offset += cursor.remaining 34 | 35 | return new CreatedFastCell(material, derivative) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/versions/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | 3 | export class VersionsCell { 4 | readonly #class = VersionsCell 5 | 6 | static readonly old = true 7 | static readonly circuit = false 8 | static readonly command = 7 9 | 10 | constructor( 11 | readonly versions: number[] 12 | ) { } 13 | 14 | get old(): true { 15 | return this.#class.old 16 | } 17 | 18 | get circuit(): false { 19 | return this.#class.circuit 20 | } 21 | 22 | get command(): 7 { 23 | return this.#class.command 24 | } 25 | 26 | sizeOrThrow() { 27 | return 2 * this.versions.length 28 | } 29 | 30 | writeOrThrow(cursor: Cursor) { 31 | for (const version of this.versions) 32 | cursor.writeUint16OrThrow(version) 33 | 34 | return 35 | } 36 | 37 | static readOrThrow(cursor: Cursor) { 38 | const versions = new Array(cursor.remaining / 2) 39 | 40 | for (let i = 0; i < versions.length; i++) 41 | versions[i] = cursor.readUint16OrThrow() 42 | 43 | return new VersionsCell(versions) 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | const text = `aaa 2 | ffoDiEOwjSED/1TQ0RmpymllqDsVmWaw5NRwtbXKwLmE2cA8T/kKeU3fEhIvyqfK 3 | jc+kR4mg+/uzgc7YtXtR1ZsiyGyhSeMKkND9OO0MJv5ElK9/v6tcDlmbMB4KQbIo 4 | IQ5tQAZsMloUdkzqYykouDv/XZx8MdsLmBq8f2n/oMw2i0Jnle5OGvG13l6QdiqU 5 | U+tSv3d5haNAtbP8p++zANPOPYhsnxu6bFC/A8cp/lPY/GOJIFMpTWuh9JcIn+Hr 6 | Tg3+neBF0kCy6kjeq7wywKPlAD42rG+LbJR2tsj2ZK6WiL/5FMyeHOYohgM+yp+7 7 | OO1sYSyJ7aPk5Rxn+SaDtsxP2MvYfvXNGYRyyLToxmeq9Z+ruz1WcVmNVzGIjtrU 8 | TV2hIOHDG62Aqza1I7Yz4MbvhMgl4DsPzcvY9AmyD+F8o3dNSA9pGyI9280/G4+Y 9 | 4CebICzfkOnEHSUDMT/hGQrRfQ3o3AL65UG6Lk7DwoB/uISdvGNXkRKbd6TB4PLk 10 | bbb 11 | CEJ81LWFLw+PXHc2OABdwUguFQKVUoBI7HljTaJdTkVOYl85F1fc5PSD4wYLEVI5 12 | T6SML0keEd0yhDrWfZdIfv9RbLDP7yX9hoxdfUt2rc1My/z+EOYfu9mCFwYbimlr 13 | nJUnNXXvrFP3YjrM5Yx6HEYq55AE8T9bURoD+RGcxUiPTM1DRkfS9+aR1vXfshSN 14 | Xo4LFY3ejCPbkOng1wT4UAjxYU5wAvOTZYHRGw3rZnWaEEUQQHvceZ26vo9rfYhu 15 | gYRFPn7UXS/hU9tYQUEvTR6FTBZmQgdetOUz3eLab+y//R6gcXPNkyLTrwtkMM+z 16 | 1eZKMHDtlGei74DswHNXXQ==` 17 | 18 | export function parse() { 19 | const lines = text.split('\n') 20 | 21 | for (let i = 0; i < lines.length; i++) { 22 | 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/bench/bitset/read.bench.ts: -------------------------------------------------------------------------------- 1 | import { Bitset } from "@hazae41/bitset" 2 | import { benchSync } from "@hazae41/deimos" 3 | import { relative, resolve } from "path" 4 | 5 | const samples = 100_000 6 | 7 | const packed = 255 8 | 9 | const directory = resolve("./dist/bench/") 10 | const { pathname } = new URL(import.meta.url) 11 | console.log(relative(directory, pathname.replace(".mjs", ".ts"))) 12 | 13 | const resArith = benchSync("arithmetic", () => { 14 | const bitset = new Bitset(packed, 8) 15 | const a = Boolean(bitset.getBE(0)) 16 | const b = Boolean(bitset.getBE(1)) 17 | const c = bitset.last(6).value 18 | 19 | console.assert(a === true) 20 | console.assert(b === true) 21 | console.assert(c === 63) 22 | }, { samples }) 23 | 24 | const resString = benchSync("string", () => { 25 | const bitset = packed.toString(2) 26 | const a = Boolean(bitset[0]) 27 | const b = Boolean(bitset[1]) 28 | const c = parseInt(bitset.slice(2, 8), 2) 29 | 30 | console.assert(a === true) 31 | console.assert(b === true) 32 | console.assert(c === 63) 33 | }, { samples }) 34 | 35 | resString.tableAndSummary(resArith) -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_extended2/cell.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | import { Unimplemented } from "mods/tor/errors.js"; 4 | 5 | export class RelayExtended2Cell { 6 | readonly #class = RelayExtended2Cell 7 | 8 | static readonly early = false 9 | static readonly stream = false 10 | static readonly rcommand = 15 11 | 12 | constructor( 13 | readonly fragment: T 14 | ) { } 15 | 16 | get early(): false { 17 | return this.#class.early 18 | } 19 | 20 | get stream(): false { 21 | return this.#class.stream 22 | } 23 | 24 | get rcommand(): 15 { 25 | return this.#class.rcommand 26 | } 27 | 28 | sizeOrThrow(): never { 29 | throw new Unimplemented() 30 | } 31 | 32 | writeOrThrow(cursor: Cursor): never { 33 | throw new Unimplemented() 34 | } 35 | 36 | static readOrThrow(cursor: Cursor) { 37 | const length = cursor.readUint16OrThrow() 38 | const bytes = cursor.readAndCopyOrThrow(length) 39 | const data = new Opaque(bytes) 40 | 41 | return new RelayExtended2Cell(data) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/create_fast/cell.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | 4 | export interface CreateFastCellInit { 5 | readonly material: Uint8Array<20> 6 | } 7 | 8 | export class CreateFastCell { 9 | readonly #class = CreateFastCell 10 | 11 | static readonly old = false 12 | static readonly circuit = true 13 | static readonly command = 5 14 | 15 | /** 16 | * The CREATE_FAST cell 17 | * @param material Key material (X) [20] 18 | */ 19 | constructor( 20 | readonly material: Uint8Array<20> 21 | ) { } 22 | 23 | get old(): false { 24 | return this.#class.old 25 | } 26 | 27 | get circuit(): true { 28 | return this.#class.circuit 29 | } 30 | 31 | get command(): 5 { 32 | return this.#class.command 33 | } 34 | 35 | sizeOrThrow() { 36 | return this.material.length 37 | } 38 | 39 | writeOrThrow(cursor: Cursor) { 40 | cursor.writeOrThrow(this.material) 41 | } 42 | 43 | static readOrThrow(cursor: Cursor) { 44 | const material = cursor.readAndCopyOrThrow(20) 45 | 46 | cursor.offset += cursor.remaining 47 | 48 | return new CreateFastCell(material) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/auth_challenge/cell.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes" 2 | import { Cursor } from "@hazae41/cursor" 3 | import { Unimplemented } from "mods/tor/errors.js" 4 | 5 | export class AuthChallengeCell { 6 | readonly #class = AuthChallengeCell 7 | 8 | static readonly old = false 9 | static readonly circuit = false 10 | static readonly command = 130 11 | 12 | constructor( 13 | readonly challenge: Uint8Array<32>, 14 | readonly methods: number[] 15 | ) { } 16 | 17 | get circuit() { 18 | return this.#class.circuit 19 | } 20 | 21 | get command() { 22 | return this.#class.command 23 | } 24 | 25 | sizeOrThrow(): never { 26 | throw new Unimplemented() 27 | } 28 | 29 | writeOrThrow(cursor: Cursor): never { 30 | throw new Unimplemented() 31 | } 32 | 33 | static readOrThrow(cursor: Cursor) { 34 | const challenge = cursor.readAndCopyOrThrow(32) 35 | const nmethods = cursor.readUint16OrThrow() 36 | const methods = new Array(nmethods) 37 | 38 | for (let i = 0; i < nmethods; i++) 39 | methods[i] = cursor.readUint16OrThrow() 40 | 41 | return new AuthChallengeCell(challenge, methods) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/destroy/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | 3 | export namespace DestroyCell { 4 | export type Reasons = typeof DestroyCell.reasons 5 | } 6 | 7 | export class DestroyCell { 8 | readonly #class = DestroyCell 9 | 10 | static readonly old = false 11 | static readonly circuit = true 12 | static readonly command = 4 13 | 14 | static readonly reasons = { 15 | NONE: 0, 16 | PROTOCOL: 1, 17 | INTERNAL: 2, 18 | REQUESTED: 3, 19 | HIBERNATING: 4, 20 | RESOURCELIMIT: 5, 21 | CONNECTFAILED: 6, 22 | OR_IDENTITY: 7, 23 | CHANNEL_CLOSED: 8, 24 | FINISHED: 9, 25 | TIMEOUT: 10, 26 | DESTROYED: 11, 27 | NOSUCHSERVICE: 12 28 | } as const 29 | 30 | constructor( 31 | readonly reason: number 32 | ) { } 33 | 34 | get command() { 35 | return this.#class.command 36 | } 37 | 38 | sizeOrThrow() { 39 | return 1 40 | } 41 | 42 | writeOrThrow(cursor: Cursor) { 43 | cursor.writeUint8OrThrow(this.reason) 44 | } 45 | 46 | static readOrThrow(cursor: Cursor) { 47 | const code = cursor.readUint8OrThrow() 48 | 49 | cursor.offset += cursor.remaining 50 | 51 | return new DestroyCell(code) 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/create2/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | 3 | export class Create2Cell { 4 | readonly #class = Create2Cell 5 | 6 | static readonly circuit = true 7 | static readonly command = 10 8 | 9 | static readonly types = { 10 | /** 11 | * The old, slow, and insecure handshake 12 | * @deprecated 13 | */ 14 | TAP: 0, 15 | /** 16 | * The new, quick, and secure handshake 17 | */ 18 | NTOR: 2 19 | } as const 20 | 21 | constructor( 22 | readonly type: number, 23 | readonly data: Uint8Array 24 | ) { } 25 | 26 | get command() { 27 | return this.#class.command 28 | } 29 | 30 | sizeOrThrow() { 31 | return 2 + 2 + this.data.length 32 | } 33 | 34 | writeOrThrow(cursor: Cursor) { 35 | cursor.writeUint16OrThrow(this.type) 36 | cursor.writeUint16OrThrow(this.data.length) 37 | cursor.writeOrThrow(this.data) 38 | } 39 | 40 | static readOrThrow(cursor: Cursor) { 41 | const type = cursor.readUint16OrThrow() 42 | const length = cursor.readUint16OrThrow() 43 | const data = cursor.readAndCopyOrThrow(length) 44 | 45 | cursor.offset += cursor.remaining 46 | 47 | return new Create2Cell(type, data) 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/libs/transports/http.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary" 2 | import { FullDuplex } from "@hazae41/cascade" 3 | import { Cursor } from "@hazae41/cursor" 4 | import { Resizer } from "libs/resizer/resizer.js" 5 | 6 | export class BatchedFetchStream { 7 | 8 | readonly duplex: FullDuplex 9 | 10 | readonly #buffer = new Resizer() 11 | 12 | constructor( 13 | readonly request: RequestInfo 14 | ) { 15 | this.duplex = new FullDuplex({ 16 | output: { 17 | write: c => this.#buffer.writeFromOrThrow(c), 18 | } 19 | }) 20 | 21 | this.loop() 22 | } 23 | 24 | async loop() { 25 | while (!this.duplex.closed) { 26 | try { 27 | const body = this.#buffer.inner.before 28 | this.#buffer.inner.offset = 0 29 | 30 | const res = await fetch(this.request, { method: "POST", body }) 31 | const data = new Uint8Array(await res.arrayBuffer()) 32 | 33 | const chunker = new Cursor(data) 34 | 35 | for (const chunk of chunker.splitOrThrow(16384)) 36 | this.duplex.input.enqueue(new Opaque(chunk)) 37 | 38 | continue 39 | } catch (e: unknown) { 40 | this.duplex.error(e) 41 | break 42 | } 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/libs/resizer/resizer.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "@hazae41/binary"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | 4 | export class Resizer { 5 | 6 | inner: Cursor 7 | 8 | constructor( 9 | readonly minimum = 2 ** 10, 10 | readonly maximum = 2 ** 20, 11 | ) { 12 | this.inner = new Cursor(new Uint8Array(this.minimum)) 13 | } 14 | 15 | writeOrThrow(chunk: Uint8Array) { 16 | const length = this.inner.offset + chunk.length 17 | 18 | if (length > this.maximum) 19 | throw new Error(`Maximum size exceeded`) 20 | 21 | if (length > this.inner.length) { 22 | const resized = new Cursor(new Uint8Array(length)) 23 | resized.writeOrThrow(this.inner.before) 24 | this.inner = resized 25 | } 26 | 27 | this.inner.writeOrThrow(chunk) 28 | } 29 | 30 | writeFromOrThrow(writable: Writable) { 31 | const length = this.inner.offset + writable.sizeOrThrow() 32 | 33 | if (length > this.maximum) 34 | throw new Error(`Maximum size exceeded`) 35 | 36 | if (length > this.inner.length) { 37 | const resized = new Cursor(new Uint8Array(length)) 38 | resized.writeOrThrow(this.inner.before) 39 | this.inner = resized 40 | } 41 | 42 | writable.writeOrThrow(this.inner) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /test/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hazae41/berith": "^1.2.6", 13 | "@hazae41/box": "^2.0.1", 14 | "@hazae41/echalote": "file:../..", 15 | "@hazae41/ed25519.wasm": "^1.0.10", 16 | "@hazae41/fleche": "^1.4.5", 17 | "@hazae41/morax": "^1.1.5", 18 | "@hazae41/mutex": "^2.0.2", 19 | "@hazae41/option": "^1.1.4", 20 | "@hazae41/piscine": "^2.1.2", 21 | "@hazae41/plume": "^3.0.5", 22 | "@hazae41/result": "^1.3.1", 23 | "@hazae41/sha1.wasm": "^1.0.5", 24 | "@hazae41/symbol-dispose-polyfill": "^1.0.2", 25 | "@hazae41/x25519.wasm": "^1.0.8", 26 | "@noble/curves": "^1.6.0", 27 | "@noble/hashes": "^1.5.0", 28 | "next": "14.2.7", 29 | "react": "18.3.1", 30 | "react-dom": "18.3.1" 31 | }, 32 | "devDependencies": { 33 | "@hazae41/rimraf": "^1.0.1", 34 | "@types/node": "22.5.2", 35 | "@types/react": "18.3.5", 36 | "@types/react-dom": "18.3.0", 37 | "autoprefixer": "^10.4.20", 38 | "eslint": "^8.57.0", 39 | "eslint-config-next": "14.2.7", 40 | "postcss": "^8.4.43", 41 | "tailwindcss": "^3.4.10", 42 | "typescript": "5.5.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_begin/cell.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from "@hazae41/bytes" 2 | import { Cursor } from "@hazae41/cursor" 3 | 4 | export class RelayBeginCell { 5 | readonly #class = RelayBeginCell 6 | 7 | static readonly early = false 8 | static readonly stream = true 9 | static readonly rcommand = 1 10 | 11 | static readonly flags = { 12 | IPV6_OK: 0, 13 | IPV4_NOT_OK: 1, 14 | IPV6_PREFER: 2 15 | } as const 16 | 17 | private constructor( 18 | readonly address: string, 19 | readonly bytes: Uint8Array, 20 | readonly flags: number 21 | ) { } 22 | 23 | static create(address: string, flags: number) { 24 | return new RelayBeginCell(address, Bytes.fromUtf8(address), flags) 25 | } 26 | 27 | get early(): false { 28 | return this.#class.early 29 | } 30 | 31 | get stream(): true { 32 | return this.#class.stream 33 | } 34 | 35 | get rcommand() { 36 | return this.#class.rcommand 37 | } 38 | 39 | sizeOrThrow() { 40 | return (this.bytes.length + 1) + 4 41 | } 42 | 43 | writeOrThrow(cursor: Cursor) { 44 | cursor.writeNulledOrThrow(this.bytes) 45 | cursor.writeUint32OrThrow(this.flags) 46 | } 47 | 48 | static readOrThrow(cursor: Cursor) { 49 | const bytes = cursor.readNulledAndCopyOrThrow() 50 | const address = Bytes.toUtf8(bytes) 51 | const flags = cursor.readUint32OrThrow() 52 | 53 | return new RelayBeginCell(address, bytes, flags) 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_end/reason.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | import { Dates } from "libs/dates/dates.js"; 3 | import { Address4, Address6 } from "mods/tor/binary/address.js"; 4 | 5 | export type RelayEndReason = 6 | | RelayEndReasonExitPolicy 7 | | RelayEndReasonOther 8 | 9 | export class RelayEndReasonOther { 10 | 11 | constructor( 12 | readonly id: number 13 | ) { } 14 | 15 | sizeOrThrow() { 16 | return 0 17 | } 18 | 19 | writeOrThrow(cursor: Cursor) { 20 | return 21 | } 22 | 23 | } 24 | 25 | export class RelayEndReasonExitPolicy { 26 | readonly #class = RelayEndReasonExitPolicy 27 | 28 | static readonly id = 4 29 | 30 | constructor( 31 | readonly address: Address4 | Address6, 32 | readonly ttl: Date 33 | ) { } 34 | 35 | get id() { 36 | return this.#class.id 37 | } 38 | 39 | sizeOrThrow() { 40 | return this.address.sizeOrThrow() + 4 41 | } 42 | 43 | writeOrThrow(cursor: Cursor) { 44 | this.address.writeOrThrow(cursor) 45 | const ttlv = Dates.toSecondsDelay(this.ttl) 46 | cursor.writeUint32OrThrow(ttlv) 47 | } 48 | 49 | static readOrThrow(cursor: Cursor) { 50 | const address = cursor.remaining === 8 51 | ? Address4.readOrThrow(cursor) 52 | : Address6.readOrThrow(cursor) 53 | 54 | const ttlv = cursor.readUint32OrThrow() 55 | const ttl = Dates.fromSecondsDelay(ttlv) 56 | 57 | return new RelayEndReasonExitPolicy(address, ttl) 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /tools/ws_tcp_proxy.ts: -------------------------------------------------------------------------------- 1 | import { iterateReader, writeAll } from "https://deno.land/std@0.187.0/streams/mod.ts"; 2 | 3 | const server = Deno.listen({ port: 8080 }) 4 | 5 | for await (const conn of server) 6 | onconn(conn).catch(console.error) 7 | 8 | async function pipeToWsAndLog(symbol: string, reader: Deno.Reader, socket: WebSocket) { 9 | for await (const bytes of iterateReader(reader)) { 10 | Console.debug(symbol, bytes) 11 | socket.send(bytes) 12 | } 13 | } 14 | 15 | async function onconn(conn: Deno.Conn) { 16 | const http = Deno.serveHttp(conn) 17 | 18 | for await (const { request, respondWith } of http) { 19 | try { 20 | const { socket, response } = Deno.upgradeWebSocket(request); 21 | 22 | const target = await Deno.connect({ hostname: "127.0.0.1", port: 9001, transport: "tcp" }) 23 | 24 | socket.binaryType = "arraybuffer" 25 | 26 | socket.addEventListener("message", async e => { 27 | try { 28 | const bytes = new Uint8Array(e.data) 29 | Console.debug("->", bytes) 30 | await writeAll(target, bytes) 31 | } catch (_: unknown) { 32 | socket.close() 33 | target.close() 34 | return 35 | } 36 | }) 37 | 38 | pipeToWsAndLog("<-", target, socket) 39 | .catch(console.error) 40 | .finally(() => { 41 | socket.close() 42 | target.close() 43 | }) 44 | 45 | await respondWith(response) 46 | } catch (_: unknown) { 47 | await respondWith(new Response(undefined, { status: 500 })) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/padding_negociate/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | 3 | export class PaddingNegociateCell { 4 | readonly #class = PaddingNegociateCell 5 | 6 | static readonly old = false 7 | static readonly circuit = false 8 | static readonly command = 12 9 | 10 | static readonly versions = { 11 | ZERO: 0 12 | } as const 13 | 14 | static readonly commands = { 15 | STOP: 1, 16 | START: 2 17 | } as const 18 | 19 | constructor( 20 | readonly version: number, 21 | readonly pcommand: number, 22 | readonly ito_low_ms: number, 23 | readonly ito_high_ms: number 24 | ) { } 25 | 26 | get old(): false { 27 | return this.#class.old 28 | } 29 | 30 | get circuit(): false { 31 | return this.#class.circuit 32 | } 33 | 34 | get command() { 35 | return this.#class.command 36 | } 37 | 38 | sizeOrThrow() { 39 | return 1 + 1 + 2 + 2 40 | } 41 | 42 | writeOrThrow(cursor: Cursor) { 43 | cursor.writeUint8OrThrow(this.version) 44 | cursor.writeUint8OrThrow(this.pcommand) 45 | cursor.writeUint16OrThrow(this.ito_low_ms) 46 | cursor.writeUint16OrThrow(this.ito_high_ms) 47 | } 48 | 49 | static readOrThrow(cursor: Cursor) { 50 | const version = cursor.readUint8OrThrow() 51 | const pcommand = cursor.readUint8OrThrow() 52 | const ito_low_ms = cursor.readUint16OrThrow() 53 | const ito_high_ms = cursor.readUint16OrThrow() 54 | 55 | cursor.offset += cursor.remaining 56 | 57 | return new PaddingNegociateCell(version, pcommand, ito_low_ms, ito_high_ms) 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/netinfo/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | import { TypedAddress } from "mods/tor/binary/address.js"; 3 | 4 | export class NetinfoCell { 5 | readonly #class = NetinfoCell 6 | 7 | static readonly old = false 8 | static readonly circuit = false 9 | static readonly command = 8 10 | 11 | constructor( 12 | readonly time: number, 13 | readonly other: TypedAddress, 14 | readonly owneds: TypedAddress[] 15 | ) { } 16 | 17 | get old(): false { 18 | return this.#class.old 19 | } 20 | 21 | get circuit(): false { 22 | return this.#class.circuit 23 | } 24 | 25 | get command() { 26 | return this.#class.command 27 | } 28 | 29 | sizeOrThrow() { 30 | return 0 31 | + 4 32 | + this.other.sizeOrThrow() 33 | + 1 34 | + this.owneds.reduce((p, c) => p + c.sizeOrThrow(), 0) 35 | } 36 | 37 | writeOrThrow(cursor: Cursor) { 38 | cursor.writeUint32OrThrow(this.time) 39 | this.other.writeOrThrow(cursor) 40 | cursor.writeUint8OrThrow(this.owneds.length) 41 | 42 | for (const owned of this.owneds) 43 | owned.writeOrThrow(cursor) 44 | 45 | return 46 | } 47 | 48 | static readOrThrow(cursor: Cursor) { 49 | const time = cursor.readUint32OrThrow() 50 | const other = TypedAddress.readOrThrow(cursor) 51 | 52 | const owneds = new Array(cursor.readUint8OrThrow()) 53 | 54 | for (let i = 0; i < owneds.length; i++) 55 | owneds[i] = TypedAddress.readOrThrow(cursor) 56 | 57 | cursor.offset += cursor.remaining 58 | 59 | return new NetinfoCell(time, other, owneds) 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_extend2/cell.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "@hazae41/binary" 2 | import { Cursor } from "@hazae41/cursor" 3 | import { RelayExtend2Link } from "mods/tor/binary/cells/relayed/relay_extend2/link.js" 4 | 5 | export class RelayExtend2Cell { 6 | readonly #class = RelayExtend2Cell 7 | 8 | static readonly early = true 9 | static readonly stream = false 10 | static readonly rcommand = 14 11 | 12 | static readonly types = { 13 | /** 14 | * The old, slow, and insecure handshake 15 | * @deprecated 16 | */ 17 | TAP: 0, 18 | /** 19 | * The new, quick, and secure handshake 20 | */ 21 | NTOR: 2 22 | } as const 23 | 24 | constructor( 25 | readonly type: number, 26 | readonly links: RelayExtend2Link[], 27 | readonly data: T 28 | ) { } 29 | 30 | get early(): true { 31 | return this.#class.early 32 | } 33 | 34 | get stream(): false { 35 | return this.#class.stream 36 | } 37 | 38 | get rcommand(): 14 { 39 | return this.#class.rcommand 40 | } 41 | 42 | sizeOrThrow() { 43 | return 0 44 | + 1 45 | + this.links.reduce((p, c) => p + c.sizeOrThrow(), 0) 46 | + 2 47 | + 2 48 | + this.data.sizeOrThrow() 49 | } 50 | 51 | writeOrThrow(cursor: Cursor) { 52 | cursor.writeUint8OrThrow(this.links.length) 53 | 54 | for (const link of this.links) 55 | link.writeOrThrow(cursor) 56 | 57 | cursor.writeUint16OrThrow(this.type) 58 | 59 | const size = this.data.sizeOrThrow() 60 | cursor.writeUint16OrThrow(size) 61 | 62 | this.data.writeOrThrow(cursor) 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/mods/tor/algorithms/kdftor.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes" 2 | import { Cursor } from "@hazae41/cursor" 3 | import { HASH_LEN, KEY_LEN } from "mods/tor/constants.js" 4 | 5 | export class InvalidKdfKeyHashError extends Error { 6 | readonly #class = InvalidKdfKeyHashError 7 | readonly name = this.#class.name 8 | 9 | constructor() { 10 | super(`Invalid KDF key hash`) 11 | } 12 | 13 | } 14 | 15 | export interface KDFTorResult { 16 | readonly keyHash: Uint8Array, 17 | readonly forwardDigest: Uint8Array, 18 | readonly backwardDigest: Uint8Array, 19 | readonly forwardKey: Uint8Array, 20 | readonly backwardKey: Uint8Array 21 | } 22 | 23 | export namespace KDFTorResult { 24 | 25 | export async function computeOrThrow(k0: Uint8Array): Promise { 26 | const ki = new Cursor(new Uint8Array(k0.length + 1)) 27 | ki.writeOrThrow(k0) 28 | 29 | const k = new Cursor(new Uint8Array(HASH_LEN * 5)) 30 | 31 | for (let i = 0; k.remaining > 0; i++) { 32 | ki.setUint8OrThrow(i) 33 | 34 | const h = new Uint8Array(await crypto.subtle.digest("SHA-1", ki.bytes)) 35 | 36 | k.writeOrThrow(h) 37 | } 38 | 39 | k.offset = 0 40 | 41 | const keyHash = k.readAndCopyOrThrow(HASH_LEN) 42 | const forwardDigest = k.readAndCopyOrThrow(HASH_LEN) 43 | const backwardDigest = k.readAndCopyOrThrow(HASH_LEN) 44 | const forwardKey = k.readAndCopyOrThrow(KEY_LEN) 45 | const backwardKey = k.readAndCopyOrThrow(KEY_LEN) 46 | 47 | return { keyHash, forwardDigest, backwardDigest, forwardKey, backwardKey } 48 | } 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/cross/cert.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes" 2 | import { Cursor } from "@hazae41/cursor" 3 | import { ExpiredCertError } from "mods/tor/certs/certs.js" 4 | import { Unimplemented } from "mods/tor/errors.js" 5 | 6 | export class CrossCert { 7 | readonly #class = CrossCert 8 | 9 | static readonly types = { 10 | RSA_TO_ED: 7 11 | } as const 12 | 13 | constructor( 14 | readonly type: number, 15 | readonly key: Uint8Array<32>, 16 | readonly expiration: Date, 17 | readonly payload: Uint8Array, 18 | readonly signature: Uint8Array 19 | ) { } 20 | 21 | verifyOrThrow() { 22 | const now = new Date() 23 | 24 | if (now > this.expiration) 25 | throw new ExpiredCertError() 26 | 27 | return true 28 | } 29 | 30 | sizeOrThrow(): never { 31 | throw new Unimplemented() 32 | } 33 | 34 | writeOrThrow(cursor: Cursor): never { 35 | throw new Unimplemented() 36 | } 37 | 38 | static readOrThrow(cursor: Cursor) { 39 | const type = cursor.readUint8OrThrow() 40 | const length = cursor.readUint16OrThrow() // TODO: check length 41 | 42 | const start = cursor.offset 43 | 44 | const key = cursor.readAndCopyOrThrow(32) 45 | 46 | const expDateHours = cursor.readUint32OrThrow() 47 | const expiration = new Date(expDateHours * 60 * 60 * 1000) 48 | 49 | const content = cursor.offset - start 50 | 51 | cursor.offset = start 52 | 53 | const payload = cursor.readAndCopyOrThrow(content) 54 | 55 | const sigLength = cursor.readUint8OrThrow() 56 | const signature = cursor.readAndCopyOrThrow(sigLength) 57 | 58 | return new CrossCert(type, key, expiration, payload, signature) 59 | } 60 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/rsa/cert.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | import { X509 } from "@hazae41/x509"; 4 | import { ExpiredCertError, PrematureCertError } from "mods/tor/certs/certs.js"; 5 | 6 | export class RsaCert { 7 | 8 | static readonly types = { 9 | RSA_SELF: 2, 10 | RSA_TO_TLS: 1, 11 | RSA_TO_AUTH: 3 12 | } as const 13 | 14 | constructor( 15 | readonly type: number, 16 | readonly data: Uint8Array, 17 | readonly x509: X509.Certificate 18 | ) { } 19 | 20 | async sha1OrThrow() { 21 | const publicKey = X509.writeToBytesOrThrow(this.x509.tbsCertificate.subjectPublicKeyInfo) 22 | 23 | return new Uint8Array(await crypto.subtle.digest("SHA-1", publicKey)) as Uint8Array<20> 24 | } 25 | 26 | verifyOrThrow() { 27 | const now = new Date() 28 | 29 | if (now > this.x509.tbsCertificate.validity.notAfter.value) 30 | throw new ExpiredCertError() 31 | if (now < this.x509.tbsCertificate.validity.notBefore.value) 32 | throw new PrematureCertError() 33 | 34 | return true 35 | } 36 | 37 | sizeOrThrow() { 38 | return 1 + 2 + this.data.length 39 | } 40 | 41 | writeOrThrow(cursor: Cursor) { 42 | cursor.writeUint8OrThrow(this.type) 43 | cursor.writeUint16OrThrow(this.data.length) 44 | cursor.writeOrThrow(this.data) 45 | } 46 | 47 | static readOrThrow(cursor: Cursor) { 48 | const type = cursor.readUint8OrThrow() 49 | const length = cursor.readUint16OrThrow() 50 | 51 | const data = cursor.readAndCopyOrThrow(length) 52 | const x509 = X509.readAndResolveFromBytesOrThrow(X509.Certificate, data) 53 | 54 | return new RsaCert(type, data, x509) 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /test/website/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import inject from "@rollup/plugin-inject"; 2 | import ts from "@rollup/plugin-typescript"; 3 | import dts from "rollup-plugin-dts"; 4 | import externals from "rollup-plugin-node-externals"; 5 | 6 | export const config = [ 7 | { 8 | input: "./src/index.ts", 9 | output: [{ 10 | dir: "./dist/esm", 11 | format: "esm", 12 | exports: "named", 13 | preserveModules: true, 14 | sourcemap: true, 15 | entryFileNames: "[name].mjs", 16 | }, { 17 | dir: "./dist/cjs", 18 | format: "cjs", 19 | exports: "named", 20 | preserveModules: true, 21 | sourcemap: true, 22 | entryFileNames: "[name].cjs", 23 | }], 24 | plugins: [externals(), ts()] 25 | }, 26 | { 27 | input: "./src/index.ts", 28 | output: [{ 29 | dir: "./dist/types", 30 | format: "esm", 31 | exports: "named", 32 | preserveModules: true, 33 | sourcemap: false, 34 | entryFileNames: "[name].d.ts", 35 | }], 36 | plugins: [externals(), ts(), dts()] 37 | }, 38 | { 39 | input: "./src/index.test.ts", 40 | output: [{ 41 | dir: "./dist/test", 42 | format: "esm", 43 | exports: "named", 44 | preserveModules: true, 45 | sourcemap: true, 46 | entryFileNames: "[name].mjs", 47 | }], 48 | plugins: [externals({ devDeps: true }), ts(), inject({ crypto: "node:crypto" })], 49 | }, 50 | { 51 | input: "./src/index.bench.ts", 52 | output: [{ 53 | dir: "./dist/bench", 54 | format: "esm", 55 | exports: "named", 56 | preserveModules: true, 57 | sourcemap: true, 58 | entryFileNames: "[name].mjs", 59 | }], 60 | plugins: [externals({ devDeps: true }), ts()], 61 | }, 62 | ] 63 | 64 | export default config -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_end/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor"; 2 | import { RelayEndReason, RelayEndReasonExitPolicy, RelayEndReasonOther } from "mods/tor/binary/cells/relayed/relay_end/reason.js"; 3 | 4 | export class RelayEndCell { 5 | readonly #class = RelayEndCell 6 | 7 | static readonly early = false 8 | static readonly stream = true 9 | static readonly rcommand = 3 10 | 11 | static readonly reasons = { 12 | REASON_UNKNOWN: 0, 13 | REASON_MISC: 1, 14 | REASON_RESOLVEFAILED: 2, 15 | REASON_CONNECTREFUSED: 3, 16 | REASON_EXITPOLICY: 4, 17 | REASON_DESTROY: 5, 18 | REASON_DONE: 6, 19 | REASON_TIMEOUT: 7, 20 | REASON_NOROUTE: 8, 21 | REASON_HIBERNATING: 9, 22 | REASON_INTERNAL: 10, 23 | REASON_RESOURCELIMIT: 11, 24 | REASON_CONNRESET: 12, 25 | REASON_TORPROTOCOL: 13, 26 | REASON_NOTDIRECTORY: 14, 27 | } as const 28 | 29 | constructor( 30 | readonly reason: RelayEndReason 31 | ) { } 32 | 33 | get early(): false { 34 | return this.#class.early 35 | } 36 | 37 | get stream(): true { 38 | return this.#class.stream 39 | } 40 | 41 | get rcommand(): 3 { 42 | return this.#class.rcommand 43 | } 44 | 45 | sizeOrThrow() { 46 | return 1 + this.reason.sizeOrThrow() 47 | } 48 | 49 | writeOrThrow(cursor: Cursor) { 50 | cursor.writeUint8OrThrow(this.reason.id) 51 | this.reason.writeOrThrow(cursor) 52 | } 53 | 54 | static readOrThrow(cursor: Cursor) { 55 | const reasonId = cursor.readUint8OrThrow() 56 | 57 | const reason = reasonId === this.reasons.REASON_EXITPOLICY 58 | ? RelayEndReasonExitPolicy.readOrThrow(cursor) 59 | : new RelayEndReasonOther(reasonId) 60 | 61 | return new RelayEndCell(reason) 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /test/website/src/libs/tors/index.ts: -------------------------------------------------------------------------------- 1 | import { Box, Deferred, Stack } from "@hazae41/box"; 2 | import { Disposer } from "@hazae41/disposer"; 3 | import { createSnowflakeStream, TorClientDuplex } from "@hazae41/echalote"; 4 | import { Pool } from "@hazae41/piscine"; 5 | import { SizedPool } from "libs/pool"; 6 | import { createWebSocketDuplex } from "libs/transport/socket"; 7 | 8 | export async function createTorOrThrow(): Promise { 9 | const ws = await createWebSocketDuplex("wss://snowflake.torproject.net/") 10 | 11 | const tcp = createSnowflakeStream(ws) 12 | // const tcp = await createMeekStream("http://localhost:8080/") 13 | 14 | const tor = new TorClientDuplex() 15 | 16 | tcp.outer.readable.pipeTo(tor.inner.writable).catch(console.error) 17 | tor.inner.readable.pipeTo(tcp.outer.writable).catch(console.error) 18 | 19 | await tor.waitOrThrow() 20 | 21 | return tor 22 | } 23 | 24 | export function createTorEntry(pool: Pool, index: number, tor: TorClientDuplex) { 25 | using stack = new Box(new Stack()) 26 | 27 | const entry = new Box(tor) 28 | stack.getOrThrow().push(tor) 29 | 30 | const onCloseOrError = async (reason?: unknown) => void pool.restart(index) 31 | 32 | stack.getOrThrow().push(new Deferred(tor.events.on("close", onCloseOrError, { passive: true }))) 33 | stack.getOrThrow().push(new Deferred(tor.events.on("error", onCloseOrError, { passive: true }))) 34 | 35 | const unstack = stack.unwrapOrThrow() 36 | 37 | return new Disposer(entry, () => unstack[Symbol.dispose]()) 38 | } 39 | 40 | export function createTorPool(size: number) { 41 | const pool: Pool = new Pool(async (params) => { 42 | const { index } = params 43 | 44 | const tor = await createTorOrThrow() 45 | 46 | return createTorEntry(pool, index, tor) 47 | }) 48 | 49 | return new Disposer(SizedPool.start(pool, size), () => { }) 50 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_connected/cell.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | import { Dates } from "libs/dates/dates.js" 3 | import { Address4, Address6 } from "mods/tor/binary/address.js" 4 | import { Unimplemented } from "mods/tor/errors.js" 5 | 6 | export class UnknownAddressType extends Error { 7 | readonly #class = UnknownAddressType 8 | readonly name = this.#class.name 9 | 10 | constructor( 11 | readonly type: number 12 | ) { 13 | super(`Unknown address type ${type}`) 14 | } 15 | 16 | } 17 | 18 | export class RelayConnectedCell { 19 | readonly #class = RelayConnectedCell 20 | 21 | static readonly early = false 22 | static readonly stream = true 23 | static readonly rcommand = 4 24 | 25 | constructor( 26 | readonly address: Address4 | Address6, 27 | readonly ttl: Date 28 | ) { } 29 | 30 | get early(): false { 31 | return this.#class.early 32 | } 33 | 34 | get stream(): true { 35 | return this.#class.stream 36 | } 37 | 38 | get rcommand(): 4 { 39 | return this.#class.rcommand 40 | } 41 | 42 | sizeOrThrow(): never { 43 | throw new Unimplemented() 44 | } 45 | 46 | writeOrThrow(cursor: Cursor): never { 47 | throw new Unimplemented() 48 | } 49 | 50 | static readOrThrow(cursor: Cursor) { 51 | const ipv4 = Address4.readOrThrow(cursor) 52 | 53 | if (ipv4.address !== "0.0.0.0") { 54 | const ttlv = cursor.readUint32OrThrow() 55 | const ttl = Dates.fromSecondsDelay(ttlv) 56 | 57 | return new RelayConnectedCell(ipv4, ttl) 58 | } 59 | 60 | const type = cursor.readUint8OrThrow() 61 | 62 | if (type !== 6) 63 | throw new UnknownAddressType(type) 64 | 65 | const ipv6 = Address6.readOrThrow(cursor) 66 | 67 | const ttlv = cursor.readUint32OrThrow() 68 | const ttl = Dates.fromSecondsDelay(ttlv) 69 | 70 | return new RelayConnectedCell(ipv6, ttl) 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/address.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | 3 | export class TypedAddress { 4 | 5 | static readonly types = { 6 | IPv4: 4, 7 | IPv6: 6 8 | } as const 9 | 10 | constructor( 11 | readonly type: number, 12 | readonly value: Uint8Array 13 | ) { } 14 | 15 | sizeOrThrow() { 16 | return 1 + 1 + this.value.length 17 | } 18 | 19 | writeOrThrow(cursor: Cursor) { 20 | cursor.writeUint8OrThrow(this.type) 21 | cursor.writeUint8OrThrow(this.value.length) 22 | cursor.writeOrThrow(this.value) 23 | } 24 | 25 | static readOrThrow(cursor: Cursor) { 26 | const type = cursor.readUint8OrThrow() 27 | const length = cursor.readUint8OrThrow() 28 | const value = cursor.readAndCopyOrThrow(length) 29 | 30 | return new TypedAddress(type, value) 31 | } 32 | 33 | } 34 | 35 | export class Address4 { 36 | 37 | /** 38 | * IPv4 address 39 | * @param address xxx.xxx.xxx.xxx 40 | */ 41 | constructor( 42 | readonly address: string 43 | ) { } 44 | 45 | sizeOrThrow() { 46 | return 4 47 | } 48 | 49 | writeOrThrow(cursor: Cursor) { 50 | const parts = this.address.split(".") 51 | 52 | for (let i = 0; i < 4; i++) 53 | cursor.writeUint8OrThrow(Number(parts[i])) 54 | 55 | return 56 | } 57 | 58 | static readOrThrow(cursor: Cursor) { 59 | const parts = new Array(4) 60 | 61 | for (let i = 0; i < 4; i++) 62 | parts[i] = String(cursor.readUint8OrThrow()) 63 | 64 | return new Address4(parts.join(".")) 65 | } 66 | 67 | } 68 | 69 | export class Address6 { 70 | 71 | /** 72 | * IPv6 address 73 | * @param address [xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx] 74 | */ 75 | constructor( 76 | readonly address: `[${string}]` 77 | ) { } 78 | 79 | sizeOrThrow() { 80 | return 16 81 | } 82 | 83 | writeOrThrow(cursor: Cursor) { 84 | const parts = this.address.slice(1, -1).split(":") 85 | 86 | for (let i = 0; i < 8; i++) 87 | cursor.writeUint16OrThrow(Number(parts[i])) 88 | 89 | return 90 | } 91 | 92 | static readOrThrow(cursor: Cursor) { 93 | const parts = new Array(8) 94 | 95 | for (let i = 0; i < 8; i++) 96 | parts[i] = String(cursor.readUint16OrThrow()) 97 | 98 | return new Address6(`[${parts.join(":")}]`) 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_sendme/cell.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { Uint8Array } from "@hazae41/bytes"; 3 | import { Cursor } from "@hazae41/cursor"; 4 | 5 | export class RelaySendmeCircuitCell { 6 | readonly #class = RelaySendmeCircuitCell 7 | 8 | static readonly early = false 9 | static readonly stream = false 10 | static readonly rcommand = 5 11 | 12 | static readonly versions = { 13 | 0: 0, 14 | 1: 1 15 | } as const 16 | 17 | constructor( 18 | readonly version: number, 19 | readonly fragment: T 20 | ) { } 21 | 22 | get early(): false { 23 | return this.#class.early 24 | } 25 | 26 | get stream(): false { 27 | return this.#class.stream 28 | } 29 | 30 | get rcommand(): 5 { 31 | return this.#class.rcommand 32 | } 33 | 34 | sizeOrThrow() { 35 | return 1 + 2 + this.fragment.sizeOrThrow() 36 | } 37 | 38 | writeOrThrow(cursor: Cursor) { 39 | cursor.writeUint8OrThrow(this.version) 40 | 41 | const size = this.fragment.sizeOrThrow() 42 | cursor.writeUint16OrThrow(size) 43 | 44 | this.fragment.writeOrThrow(cursor) 45 | } 46 | 47 | static readOrThrow(cursor: Cursor) { 48 | const version = cursor.readUint8OrThrow() 49 | const length = cursor.readUint16OrThrow() 50 | const bytes = cursor.readAndCopyOrThrow(length) 51 | const data = new Opaque(bytes) 52 | 53 | return new RelaySendmeCircuitCell(version, data) 54 | } 55 | 56 | } 57 | 58 | export class RelaySendmeStreamCell { 59 | readonly #class = RelaySendmeStreamCell 60 | 61 | static readonly early = false 62 | static readonly stream = true 63 | static readonly rcommand = 5 64 | 65 | static readonly versions = { 66 | 0: 0, 67 | 1: 1 68 | } as const 69 | 70 | constructor() { } 71 | 72 | get early(): false { 73 | return this.#class.early 74 | } 75 | 76 | get stream(): true { 77 | return this.#class.stream 78 | } 79 | 80 | get rcommand(): 5 { 81 | return this.#class.rcommand 82 | } 83 | 84 | sizeOrThrow() { 85 | return 0 86 | } 87 | 88 | writeOrThrow(cursor: Cursor) { 89 | return 90 | } 91 | 92 | static readOrThrow(cursor: Cursor) { 93 | return new RelaySendmeStreamCell() 94 | } 95 | 96 | } 97 | 98 | export class RelaySendmeDigest { 99 | 100 | constructor( 101 | readonly digest: Uint8Array<20> 102 | ) { } 103 | 104 | sizeOrThrow() { 105 | return this.digest.length 106 | } 107 | 108 | writeOrThrow(cursor: Cursor) { 109 | cursor.writeOrThrow(this.digest) 110 | } 111 | 112 | static readOrThrow(cursor: Cursor) { 113 | return new RelaySendmeDigest(cursor.readAndCopyOrThrow(20)) 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@hazae41/echalote", 4 | "version": "0.4.18", 5 | "author": "hazae41", 6 | "license": "MIT", 7 | "description": "Zero-copy Tor protocol for the web", 8 | "homepage": "https://github.com/hazae41/echalote", 9 | "repository": "github:hazae41/echalote", 10 | "main": "./dist/cjs/src/index.cjs", 11 | "module": "./dist/esm/src/index.mjs", 12 | "types": "./dist/types/index.d.ts", 13 | "sideEffects": false, 14 | "files": [ 15 | "./dist/esm", 16 | "./dist/cjs", 17 | "./dist/types" 18 | ], 19 | "scripts": { 20 | "macro": "saumon build -r ./src", 21 | "build": "rimraf dist && rollup -c", 22 | "test": "node ./dist/test/index.test.mjs", 23 | "bench": "node ./dist/bench/index.bench.mjs", 24 | "fallbacks": "cd ./tools/fallbacks && deno run -A ./parser.ts && deno fmt ./fallbacks.json", 25 | "prepare": "npm run build" 26 | }, 27 | "peerDependencies": { 28 | "@hazae41/aes.wasm": "^1.0.0", 29 | "@hazae41/base16": "^1.0.18", 30 | "@hazae41/base64": "^1.0.15", 31 | "@hazae41/ed25519": "^2.1.21", 32 | "@hazae41/rsa.wasm": "^1.0.10", 33 | "@hazae41/sha1": "^1.1.14", 34 | "@hazae41/x25519": "^2.2.9" 35 | }, 36 | "dependencies": { 37 | "@hazae41/asn1": "^1.3.31", 38 | "@hazae41/binary": "^1.3.5", 39 | "@hazae41/bitset": "^1.0.1", 40 | "@hazae41/bytes": "^1.2.11", 41 | "@hazae41/cadenas": "^0.4.2", 42 | "@hazae41/cascade": "^2.2.2", 43 | "@hazae41/cursor": "^1.2.4", 44 | "@hazae41/fleche": "^1.4.5", 45 | "@hazae41/future": "^1.0.3", 46 | "@hazae41/kcp": "^1.1.3", 47 | "@hazae41/mutex": "^2.1.0", 48 | "@hazae41/option": "^1.1.4", 49 | "@hazae41/plume": "^3.0.5", 50 | "@hazae41/smux": "^1.1.3", 51 | "@hazae41/x509": "^1.2.10" 52 | }, 53 | "devDependencies": { 54 | "@hazae41/deimos": "^1.0.6", 55 | "@hazae41/phobos": "^1.0.10", 56 | "@hazae41/rimraf": "^1.0.1", 57 | "@hazae41/saumon": "^0.2.14", 58 | "@rollup/plugin-inject": "^5.0.5", 59 | "@rollup/plugin-typescript": "^11.1.6", 60 | "@types/node": "^22.5.3", 61 | "rollup": "^4.21.2", 62 | "rollup-plugin-dts": "^6.1.1", 63 | "rollup-plugin-node-externals": "^7.1.3", 64 | "tslib": "^2.7.0", 65 | "typescript": "^5.5.4" 66 | }, 67 | "exports": { 68 | ".": { 69 | "types": "./dist/types/index.d.ts", 70 | "import": "./dist/esm/src/index.mjs", 71 | "require": "./dist/cjs/src/index.cjs" 72 | } 73 | }, 74 | "keywords": [ 75 | "tor", 76 | "onion", 77 | "protocol", 78 | "browser", 79 | "buffer", 80 | "stream", 81 | "streaming", 82 | "http", 83 | "https", 84 | "privacy", 85 | "tls", 86 | "packets", 87 | "binary", 88 | "encoding", 89 | "decoding", 90 | "compression", 91 | "cryptography", 92 | "crypto", 93 | "typescript", 94 | "esmodules", 95 | "tested", 96 | "unit-tested" 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /src/mods/tor/ciphers.ts: -------------------------------------------------------------------------------- 1 | import { AES_256_CBC, Cipher, Ciphers, DHE_RSA, SHA } from "@hazae41/cadenas" 2 | 3 | export namespace TorCiphers { 4 | 5 | export const TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = new Cipher(0xc00a, DHE_RSA, AES_256_CBC, SHA) 6 | export const TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = new Cipher(0xc014, DHE_RSA, AES_256_CBC, SHA) 7 | 8 | export const TLS_DHE_RSA_WITH_AES_256_CBC_SHA = Ciphers.TLS_DHE_RSA_WITH_AES_256_CBC_SHA 9 | export const TLS_DHE_DSS_WITH_AES_256_CBC_SHA = new Cipher(0x0038, DHE_RSA, AES_256_CBC, SHA) 10 | 11 | export const TLS_ECDH_RSA_WITH_AES_256_CBC_SHA = new Cipher(0xc00f, DHE_RSA, AES_256_CBC, SHA) 12 | export const TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA = new Cipher(0xc005, DHE_RSA, AES_256_CBC, SHA) 13 | 14 | export const TLS_RSA_WITH_AES_256_CBC_SHA = new Cipher(0x0035, DHE_RSA, AES_256_CBC, SHA) 15 | 16 | export const TLS_ECDHE_ECDSA_WITH_RC4_128_SHA = new Cipher(0xc007, DHE_RSA, AES_256_CBC, SHA) 17 | export const TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = new Cipher(0xc009, DHE_RSA, AES_256_CBC, SHA) 18 | 19 | export const TLS_ECDHE_RSA_WITH_RC4_128_SHA = new Cipher(0xc011, DHE_RSA, AES_256_CBC, SHA) 20 | export const TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = new Cipher(0xc013, DHE_RSA, AES_256_CBC, SHA) 21 | 22 | export const TLS_DHE_RSA_WITH_AES_128_CBC_SHA = new Cipher(0x0033, DHE_RSA, AES_256_CBC, SHA) 23 | export const TLS_DHE_DSS_WITH_AES_128_CBC_SHA = new Cipher(0x0032, DHE_RSA, AES_256_CBC, SHA) 24 | 25 | export const TLS_ECDH_RSA_WITH_RC4_128_SHA = new Cipher(0xc00c, DHE_RSA, AES_256_CBC, SHA) 26 | export const TLS_ECDH_RSA_WITH_AES_128_CBC_SHA = new Cipher(0xc00e, DHE_RSA, AES_256_CBC, SHA) 27 | 28 | export const TLS_ECDH_ECDSA_WITH_RC4_128_SHA = new Cipher(0xc002, DHE_RSA, AES_256_CBC, SHA) 29 | export const TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA = new Cipher(0xc004, DHE_RSA, AES_256_CBC, SHA) 30 | 31 | export const TLS_RSA_WITH_RC4_128_MD5 = new Cipher(0x0004, DHE_RSA, AES_256_CBC, SHA) 32 | export const TLS_RSA_WITH_RC4_128_SHA = new Cipher(0x0005, DHE_RSA, AES_256_CBC, SHA) 33 | 34 | export const TLS_RSA_WITH_AES_128_CBC_SHA = new Cipher(0x002f, DHE_RSA, AES_256_CBC, SHA) 35 | 36 | export const TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA = new Cipher(0xc008, DHE_RSA, AES_256_CBC, SHA) 37 | export const TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA = new Cipher(0xc012, DHE_RSA, AES_256_CBC, SHA) 38 | 39 | export const TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA = new Cipher(0x0016, DHE_RSA, AES_256_CBC, SHA) 40 | export const TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA = new Cipher(0x0013, DHE_RSA, AES_256_CBC, SHA) 41 | 42 | export const TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA = new Cipher(0xc00d, DHE_RSA, AES_256_CBC, SHA) 43 | export const TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA = new Cipher(0xc003, DHE_RSA, AES_256_CBC, SHA) 44 | 45 | export const SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA = new Cipher(0xfeff, DHE_RSA, AES_256_CBC, SHA) 46 | export const TLS_RSA_WITH_3DES_EDE_CBC_SHA = new Cipher(0x000a, DHE_RSA, AES_256_CBC, SHA) 47 | 48 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | ```bash 6 | npm i @hazae41/echalote 7 | ``` 8 | 9 | [**Node Package 📦**](https://www.npmjs.com/package/@hazae41/echalote) • [**Online Demo 🌐**](https://echalote-example-next.vercel.app) • [**Next.js CodeSandbox 🪣**](https://codesandbox.io/p/github/hazae41/echalote-example-next) 10 | 11 | ## Use at your own risk 12 | 13 | This is experimental software in early development 14 | 15 | 1. It has security issues 16 | 2. Things change quickly 17 | 18 | ## Features 19 | 20 | ### Current features 21 | - 100% TypeScript and ESM 22 | - Zero-copy reading and writing 23 | - Works in the browser 24 | - All cryptography use either WebCrypto or reproducible WebAssembly ports of Rust implementations 25 | - Unsafe Tor protocol (with Ed25519, ntor, kdf-tor) 26 | - Meek (HTTP) transport (without domain-fronting) 27 | - Snowflake (WebRTC/WebSocket) transport (without domain-fronting) 28 | - Unsafe TLS using [Cadenas](https://github.com/hazae41/cadenas) 29 | - HTTP and WebSocket messaging using [Fleche](https://github.com/hazae41/fleche) 30 | 31 | ### [Upcoming features](https://github.com/sponsors/hazae41) 32 | - Better security 33 | 34 | ## Usage 35 | 36 | ```typescript 37 | import { createWebSocketSnowflakeStream, TorClientDuplex, Consensus } from "@hazae41/echalote" 38 | import { Ciphers, TlsClientDuplex } from "@hazae41/cadenas" 39 | 40 | const tcp = await createWebSocketSnowflakeStream("wss://snowflake.bamsoftware.com/") 41 | const tor = new TorClientDuplex() 42 | 43 | tcp.outer.readable.pipeTo(tor.inner.writable).catch(() => {}) 44 | tor.inner.readable.pipeTo(tcp.outer.writable).catch(() => {}) 45 | 46 | await tor.waitOrThrow() 47 | 48 | using circuit = await tor.createOrThrow() 49 | const consensus = await Consensus.fetchOrThrow(circuit) 50 | 51 | const middles = consensus.microdescs.filter(it => true 52 | && it.flags.includes("Fast") 53 | && it.flags.includes("Stable") 54 | && it.flags.includes("V2Dir")) 55 | 56 | const exits = consensus.microdescs.filter(it => true 57 | && it.flags.includes("Fast") 58 | && it.flags.includes("Stable") 59 | && it.flags.includes("Exit") 60 | && !it.flags.includes("BadExit")) 61 | 62 | const middle = middles[Math.floor(Math.random() * middles.length)] 63 | const middle2 = await Consensus.Microdesc.fetchOrThrow(circuit, middle) 64 | await circuit.extendOrThrow(middle2, AbortSignal.timeout(5000)) 65 | 66 | const exit = exits[Math.floor(Math.random() * middles.length)] 67 | const exit2 = await Consensus.Microdesc.fetchOrThrow(circuit, exit) 68 | await circuit.extendOrThrow(exit2, AbortSignal.timeout(5000)) 69 | 70 | const ttcp = await circuit.openOrThrow("twitter.com", 443) 71 | 72 | const ciphers = [Ciphers.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] 73 | const ttls = new TlsClientDuplex({ host_name: url.hostname, ciphers }) 74 | 75 | ttcp.outer.readable.pipeTo(ttls.inner.writable).catch(() => { }) 76 | ttls.inner.readable.pipeTo(ttcp.outer.writable).catch(() => { }) 77 | 78 | const response = await fetch("https://twitter.com", { stream: ttls.outer }) 79 | const text = await response.text() 80 | ``` -------------------------------------------------------------------------------- /test/website/src/libs/transport/socket.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary" 2 | import { HalfDuplex } from "@hazae41/cascade" 3 | import { Future } from "@hazae41/future" 4 | 5 | export interface WebSocketDuplexParams { 6 | /** 7 | * Whether the socket should be closed when the duplex is closed 8 | * @description You don't want to reuse the socket 9 | * @description You're not using request-response 10 | */ 11 | readonly shouldCloseOnClose?: boolean 12 | 13 | /** 14 | * Whether the socket should be closed when the duplex is errored 15 | * @description You don't want to reuse the socket 16 | */ 17 | readonly shouldCloseOnError?: boolean 18 | } 19 | 20 | export class WebSocketDuplex { 21 | 22 | readonly duplex: HalfDuplex 23 | 24 | constructor( 25 | readonly socket: WebSocket, 26 | readonly params: WebSocketDuplexParams = {} 27 | ) { 28 | const { shouldCloseOnError, shouldCloseOnClose } = params 29 | 30 | this.duplex = new HalfDuplex({ 31 | output: { 32 | write(message) { 33 | socket.send(Writable.writeToBytesOrThrow(message)) 34 | }, 35 | }, 36 | close() { 37 | if (!shouldCloseOnClose) 38 | return 39 | 40 | try { 41 | socket.close() 42 | } catch { } 43 | }, 44 | error() { 45 | if (!shouldCloseOnError) 46 | return 47 | 48 | try { 49 | socket.close() 50 | } catch { } 51 | } 52 | }) 53 | 54 | socket.addEventListener("close", () => this.duplex.close()) 55 | socket.addEventListener("error", e => this.duplex.error(e)) 56 | 57 | socket.addEventListener("message", async (e: MessageEvent) => { 58 | if (typeof e.data === "string") 59 | return 60 | 61 | const bytes = new Uint8Array(e.data) 62 | const opaque = new Opaque(bytes) 63 | 64 | this.duplex.input.enqueue(opaque) 65 | }) 66 | } 67 | 68 | [Symbol.dispose]() { 69 | this.close() 70 | } 71 | 72 | get outer() { 73 | return this.duplex.outer 74 | } 75 | 76 | get closing() { 77 | return this.duplex.closing 78 | } 79 | 80 | get closed() { 81 | return this.duplex.closed 82 | } 83 | 84 | error(reason?: unknown) { 85 | this.duplex.error(reason) 86 | } 87 | 88 | close() { 89 | this.duplex.close() 90 | } 91 | 92 | } 93 | 94 | export async function createWebSocketDuplex(url: string) { 95 | const socket = new WebSocket(url) 96 | socket.binaryType = "arraybuffer" 97 | 98 | const future = new Future() 99 | 100 | const onOpen = () => future.resolve() 101 | const onError = (e: Event) => future.reject(e) 102 | 103 | try { 104 | socket.addEventListener("open", onOpen, { passive: true }) 105 | socket.addEventListener("error", onError, { passive: true }) 106 | 107 | await future.promise 108 | 109 | return new WebSocketDuplex(socket) 110 | } finally { 111 | socket.removeEventListener("open", onOpen) 112 | socket.removeEventListener("error", onError) 113 | } 114 | } -------------------------------------------------------------------------------- /test/website/pages/dir.tsx: -------------------------------------------------------------------------------- 1 | import { Cadenas } from "@hazae41/cadenas"; 2 | import { Consensus, Echalote } from "@hazae41/echalote"; 3 | import { Ed25519 } from "@hazae41/ed25519"; 4 | import { fetch } from "@hazae41/fleche"; 5 | import { Sha1 } from "@hazae41/sha1"; 6 | import { X25519 } from "@hazae41/x25519"; 7 | import { createTorOrThrow, openAsOrThrow } from "libs/circuits/circuits"; 8 | import { DependencyList, useCallback, useEffect, useState } from "react"; 9 | 10 | function useAsyncMemo(factory: () => Promise, deps: DependencyList) { 11 | const [state, setState] = useState() 12 | 13 | useEffect(() => { 14 | factory().then(setState) 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, deps) 17 | 18 | return state 19 | } 20 | 21 | export default function Page() { 22 | 23 | const [stream, setStream] = useState>() 24 | 25 | useEffect(() => { 26 | (async () => { 27 | Ed25519.set(await Ed25519.fromSafeOrBerith()) 28 | X25519.set(await X25519.fromSafeOrBerith()) 29 | Sha1.set(await Sha1.fromMorax()) 30 | 31 | Echalote.Console.debugging = true 32 | Cadenas.Console.debugging = true 33 | 34 | const tor = await createTorOrThrow().then(r => r.unwrap()) 35 | const circuit = await tor.tryCreate().then(r => r.unwrap()) 36 | 37 | const consensus = await Consensus.fetchOrThrow(circuit) 38 | 39 | const seconds = consensus.microdescs.filter(it => true 40 | && it.flags.includes("Fast") 41 | && it.flags.includes("Stable") 42 | && it.flags.includes("V2Dir")) 43 | 44 | const thirds = consensus.microdescs.filter(it => true 45 | && it.flags.includes("Fast") 46 | && it.flags.includes("Stable") 47 | && it.flags.includes("Exit") 48 | && !it.flags.includes("BadExit")) 49 | 50 | const second = await Consensus.Microdesc.fetchOrThrow(circuit, seconds[Math.floor(Math.random() * seconds.length)]) 51 | await circuit.extendOrThrow(second) 52 | 53 | const third = await Consensus.Microdesc.fetchOrThrow(circuit, thirds[Math.floor(Math.random() * thirds.length)]) 54 | await circuit.extendOrThrow(third) 55 | 56 | const stream = await openAsOrThrow(circuit, `https://eth.llamarpc.com`) 57 | 58 | setStream(stream.inner) 59 | })().catch(e => console.error({ e })) 60 | }, []) 61 | 62 | const onClick = useCallback(async () => { 63 | try { 64 | if (!stream) return 65 | 66 | const start = Date.now() 67 | 68 | const body = JSON.stringify({ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 67 }) 69 | const headers = { "content-type": "application/json" } 70 | 71 | const response = await fetch(`https://eth.llamarpc.com`, { stream, method: "POST", headers, body, preventClose: true, preventAbort: true, preventCancel: true }) 72 | console.log(response, Date.now() - start) 73 | 74 | const data = await response.text() 75 | console.log(data, Date.now() - start) 76 | 77 | console.log("Done!!!", Date.now() - start) 78 | } catch (e: unknown) { 79 | console.error("onClick", { e }) 80 | } 81 | }, [stream]) 82 | 83 | return <> 84 | 87 | 88 | } -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/stream.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary" 2 | import { Bytes } from "@hazae41/bytes" 3 | import { FullDuplex } from "@hazae41/cascade" 4 | import { Future } from "@hazae41/future" 5 | import { Awaitable } from "libs/promises/index.js" 6 | import { SecretTurboReader } from "./reader.js" 7 | import { SecretTurboWriter } from "./writer.js" 8 | 9 | export interface TurboDuplexParams { 10 | readonly client?: Uint8Array 11 | 12 | close?(this: undefined): Awaitable 13 | error?(this: undefined, reason?: unknown): Awaitable 14 | } 15 | 16 | export class TurboDuplex { 17 | 18 | readonly #secret: SecretTurboDuplex 19 | 20 | constructor( 21 | readonly params: TurboDuplexParams = {} 22 | ) { 23 | this.#secret = new SecretTurboDuplex(params) 24 | } 25 | 26 | [Symbol.dispose]() { 27 | this.close() 28 | } 29 | 30 | get client() { 31 | return this.#secret.client 32 | } 33 | 34 | get inner() { 35 | return this.#secret.inner 36 | } 37 | 38 | get outer() { 39 | return this.#secret.outer 40 | } 41 | 42 | get closing() { 43 | return this.#secret.closing 44 | } 45 | 46 | get closed() { 47 | return this.#secret.closed 48 | } 49 | 50 | error(reason?: unknown) { 51 | this.#secret.error(reason) 52 | } 53 | 54 | close() { 55 | this.#secret.close() 56 | } 57 | 58 | } 59 | 60 | export class SecretTurboDuplex { 61 | readonly #class = SecretTurboDuplex 62 | 63 | static readonly token = new Uint8Array([0x12, 0x93, 0x60, 0x5d, 0x27, 0x81, 0x75, 0xf5]) 64 | 65 | readonly duplex: FullDuplex 66 | 67 | readonly reader: SecretTurboReader 68 | readonly writer: SecretTurboWriter 69 | 70 | readonly client: Uint8Array 71 | 72 | readonly resolveOnStart = new Future() 73 | 74 | constructor( 75 | readonly params: TurboDuplexParams = {} 76 | ) { 77 | const { client = Bytes.random(8) } = params 78 | 79 | this.client = client 80 | 81 | this.reader = new SecretTurboReader(this) 82 | this.writer = new SecretTurboWriter(this) 83 | 84 | this.duplex = new FullDuplex({ 85 | input: { 86 | write: c => this.reader.onWrite(c), 87 | }, 88 | output: { 89 | start: () => this.writer.onStart(), 90 | write: c => this.writer.onWrite(c), 91 | }, 92 | error: e => this.params.error?.call(undefined, e), 93 | close: () => this.params.close?.call(undefined), 94 | }) 95 | 96 | this.resolveOnStart.resolve() 97 | } 98 | 99 | get class() { 100 | return this.#class 101 | } 102 | 103 | [Symbol.dispose]() { 104 | this.close() 105 | } 106 | 107 | get inner() { 108 | return this.duplex.inner 109 | } 110 | 111 | get outer() { 112 | return this.duplex.outer 113 | } 114 | 115 | get input() { 116 | return this.duplex.input 117 | } 118 | 119 | get output() { 120 | return this.duplex.output 121 | } 122 | 123 | get closing() { 124 | return this.duplex.closing 125 | } 126 | 127 | get closed() { 128 | return this.duplex.closed 129 | } 130 | 131 | error(reason?: unknown) { 132 | this.duplex.error(reason) 133 | } 134 | 135 | close() { 136 | this.duplex.close() 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/errors.ts: -------------------------------------------------------------------------------- 1 | import { UnknownAddressType } from "./relayed/relay_connected/cell.js" 2 | 3 | export type CellError = 4 | | InvalidCellError 5 | | InvalidCommandError 6 | | UnknownCircuitError 7 | | ExpectedCircuitError 8 | | UnexpectedCircuitError 9 | 10 | export class InvalidCellError extends Error { 11 | readonly #class = InvalidCellError 12 | readonly name = this.#class.name 13 | 14 | constructor() { 15 | super(`Invalid cell`) 16 | } 17 | 18 | } 19 | 20 | export class InvalidCommandError extends Error { 21 | readonly #class = InvalidCommandError 22 | readonly name = this.#class.name 23 | 24 | constructor() { 25 | super(`Invalid command`) 26 | } 27 | 28 | } 29 | 30 | export class UnknownCircuitError extends Error { 31 | readonly #class = UnknownCircuitError 32 | readonly name = this.#class.name 33 | 34 | constructor() { 35 | super(`Unknown circuit`) 36 | } 37 | 38 | } 39 | 40 | export class ExpectedCircuitError extends Error { 41 | readonly #class = ExpectedCircuitError 42 | readonly name = this.#class.name 43 | 44 | constructor() { 45 | super(`Expected a circuit`) 46 | } 47 | 48 | } 49 | 50 | export class UnexpectedCircuitError extends Error { 51 | readonly #class = UnexpectedCircuitError 52 | readonly name = this.#class.name 53 | 54 | constructor() { 55 | super(`Unexpected a circuit`) 56 | } 57 | 58 | } 59 | 60 | export type RelayCellError = 61 | | InvalidRelayCommandError 62 | | UnknownStreamError 63 | | ExpectedStreamError 64 | | UnexpectedStreamError 65 | | InvalidRelayCellDigestError 66 | | UnrecognisedRelayCellError 67 | | UnknownAddressType 68 | | InvalidRelaySendmeCellDigestError 69 | 70 | export class InvalidRelayCommandError extends Error { 71 | readonly #class = InvalidRelayCommandError 72 | readonly name = this.#class.name 73 | 74 | constructor() { 75 | super(`Invalid relay command`) 76 | } 77 | 78 | } 79 | 80 | export class UnknownStreamError extends Error { 81 | readonly #class = UnknownStreamError 82 | readonly name = this.#class.name 83 | 84 | constructor() { 85 | super(`Unknown stream`) 86 | } 87 | 88 | } 89 | 90 | export class ExpectedStreamError extends Error { 91 | readonly #class = ExpectedStreamError 92 | readonly name = this.#class.name 93 | 94 | constructor() { 95 | super(`Expected a stream`) 96 | } 97 | 98 | } 99 | 100 | export class UnexpectedStreamError extends Error { 101 | readonly #class = UnexpectedStreamError 102 | readonly name = this.#class.name 103 | 104 | constructor() { 105 | super(`Unexpected a stream`) 106 | } 107 | 108 | } 109 | 110 | export class InvalidRelayCellDigestError extends Error { 111 | readonly #class = InvalidRelayCellDigestError 112 | readonly name = this.#class.name 113 | 114 | constructor() { 115 | super(`Invalid RELAY cell digest`) 116 | } 117 | 118 | } 119 | 120 | export class InvalidRelaySendmeCellDigestError extends Error { 121 | readonly #class = InvalidRelaySendmeCellDigestError 122 | readonly name = this.#class.name 123 | 124 | constructor() { 125 | super(`Invalid RELAY_SENDME cell digest`) 126 | } 127 | 128 | } 129 | 130 | export class UnrecognisedRelayCellError extends Error { 131 | readonly #class = UnrecognisedRelayCellError 132 | readonly name = this.#class.name 133 | 134 | constructor() { 135 | super(`Unrecognised relay cell`) 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/certs/cell.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from "@hazae41/binary" 2 | import { Cursor } from "@hazae41/cursor" 3 | import { Mutable } from "libs/typescript/typescript.js" 4 | import { CrossCert } from "mods/tor/binary/certs/cross/cert.js" 5 | import { Ed25519Cert } from "mods/tor/binary/certs/ed25519/cert.js" 6 | import { RsaCert } from "mods/tor/binary/certs/rsa/cert.js" 7 | import { Certs, DuplicatedCertError, UnknownCertError } from "mods/tor/certs/certs.js" 8 | import { Unimplemented } from "mods/tor/errors.js" 9 | 10 | export class CertsCell { 11 | readonly #class = CertsCell 12 | 13 | static readonly old = false 14 | static readonly circuit = false 15 | static readonly command = 129 16 | 17 | constructor( 18 | readonly certs: Partial 19 | ) { } 20 | 21 | get circuit(): false { 22 | return this.#class.circuit 23 | } 24 | 25 | get command(): 129 { 26 | return this.#class.command 27 | } 28 | 29 | sizeOrThrow(): never { 30 | throw new Unimplemented() 31 | } 32 | 33 | writeOrThrow(cursor: Cursor): never { 34 | throw new Unimplemented() 35 | } 36 | 37 | static readOrThrow(cursor: Cursor) { 38 | const certs: Partial> = {} 39 | 40 | const count = cursor.readUint8OrThrow() 41 | 42 | for (let i = 0; i < count; i++) { 43 | const offset = cursor.offset 44 | 45 | const type = cursor.readUint8OrThrow() 46 | const length = cursor.readUint16OrThrow() 47 | 48 | cursor.offset = offset 49 | 50 | const bytes = cursor.readOrThrow(1 + 2 + length) 51 | 52 | if (type === RsaCert.types.RSA_SELF) { 53 | if (certs.rsa_self != null) 54 | throw new DuplicatedCertError() 55 | 56 | certs.rsa_self = Readable.readFromBytesOrThrow(RsaCert, bytes) 57 | continue 58 | } 59 | 60 | if (type === RsaCert.types.RSA_TO_AUTH) { 61 | if (certs.rsa_to_auth != null) 62 | throw new DuplicatedCertError() 63 | 64 | certs.rsa_to_auth = Readable.readFromBytesOrThrow(RsaCert, bytes) 65 | continue 66 | } 67 | 68 | if (type === RsaCert.types.RSA_TO_TLS) { 69 | if (certs.rsa_to_tls != null) 70 | throw new DuplicatedCertError() 71 | 72 | certs.rsa_to_tls = Readable.readFromBytesOrThrow(RsaCert, bytes) 73 | continue 74 | } 75 | 76 | if (type === CrossCert.types.RSA_TO_ED) { 77 | if (certs.rsa_to_ed != null) 78 | throw new DuplicatedCertError() 79 | 80 | certs.rsa_to_ed = Readable.readFromBytesOrThrow(CrossCert, bytes) 81 | continue 82 | } 83 | 84 | if (type === Ed25519Cert.types.ED_TO_SIGN) { 85 | if (certs.ed_to_sign != null) 86 | throw new DuplicatedCertError() 87 | 88 | certs.ed_to_sign = Readable.readFromBytesOrThrow(Ed25519Cert, bytes) 89 | continue 90 | } 91 | 92 | if (type === Ed25519Cert.types.SIGN_TO_TLS) { 93 | if (certs.sign_to_tls != null) 94 | throw new DuplicatedCertError() 95 | 96 | certs.sign_to_tls = Readable.readFromBytesOrThrow(Ed25519Cert, bytes) 97 | continue 98 | } 99 | 100 | if (type === Ed25519Cert.types.SIGN_TO_AUTH) { 101 | if (certs.sign_to_auth != null) 102 | throw new DuplicatedCertError() 103 | 104 | certs.sign_to_auth = Readable.readFromBytesOrThrow(Ed25519Cert, bytes) 105 | continue 106 | } 107 | 108 | throw new UnknownCertError() 109 | } 110 | 111 | return new CertsCell(certs) 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/certs/ed25519/cert.ts: -------------------------------------------------------------------------------- 1 | import { Uint8Array } from "@hazae41/bytes"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | import { Ed25519 } from "@hazae41/ed25519"; 4 | import { SignedWithEd25519Key } from "mods/tor/binary/certs/ed25519/extensions/signer.js"; 5 | import { ExpiredCertError, InvalidSignatureError } from "mods/tor/certs/certs.js"; 6 | 7 | export interface Extensions { 8 | signer?: SignedWithEd25519Key 9 | } 10 | 11 | export class UnknownCertExtensionError extends Error { 12 | readonly #class = UnknownCertExtensionError 13 | readonly name = this.#class.name 14 | 15 | constructor( 16 | readonly type: number 17 | ) { 18 | super(`Unknown certificate extension ${type}`) 19 | } 20 | 21 | } 22 | 23 | export class Ed25519Cert { 24 | 25 | static readonly types = { 26 | ED_TO_SIGN: 4, 27 | SIGN_TO_TLS: 5, 28 | SIGN_TO_AUTH: 6, 29 | } as const 30 | 31 | static readonly flags = { 32 | AFFECTS_VALIDATION: 1 33 | } as const 34 | 35 | constructor( 36 | readonly type: number, 37 | readonly version: number, 38 | readonly certType: number, 39 | readonly expiration: Date, 40 | readonly certKeyType: number, 41 | readonly certKey: Uint8Array<32>, 42 | readonly extensions: Extensions, 43 | readonly payload: Uint8Array, 44 | readonly signature: Uint8Array<64> 45 | ) { } 46 | 47 | async verifyOrThrow() { 48 | const now = new Date() 49 | 50 | if (now > this.expiration) 51 | throw new ExpiredCertError() 52 | 53 | if (!this.extensions.signer) 54 | return true // TODO maybe do additionnal check? 55 | 56 | using signer = await Ed25519.get().getOrThrow().VerifyingKey.importOrThrow(this.extensions.signer.key) 57 | using signature = Ed25519.get().getOrThrow().Signature.importOrThrow(this.signature) 58 | 59 | const verified = await signer.verifyOrThrow(this.payload, signature) 60 | 61 | if (verified !== true) 62 | throw new InvalidSignatureError() 63 | 64 | return true 65 | } 66 | 67 | static readOrThrow(cursor: Cursor) { 68 | const type = cursor.readUint8OrThrow() 69 | const length = cursor.readUint16OrThrow() // TODO check length 70 | 71 | const start = cursor.offset 72 | 73 | const version = cursor.readUint8OrThrow() 74 | const certType = cursor.readUint8OrThrow() 75 | 76 | const expDateHours = cursor.readUint32OrThrow() 77 | const expiration = new Date(expDateHours * 60 * 60 * 1000) 78 | 79 | const certKeyType = cursor.readUint8OrThrow() 80 | const certKey = cursor.readAndCopyOrThrow(32) 81 | 82 | const nextensions = cursor.readUint8OrThrow() 83 | const extensions: Extensions = {} 84 | 85 | for (let i = 0; i < nextensions; i++) { 86 | const length = cursor.readUint16OrThrow() 87 | const type = cursor.readUint8OrThrow() 88 | const flags = cursor.readUint8OrThrow() 89 | 90 | if (type === SignedWithEd25519Key.type) { 91 | extensions.signer = SignedWithEd25519Key.readOrThrow(cursor) 92 | continue 93 | } 94 | 95 | if (flags === this.flags.AFFECTS_VALIDATION) 96 | throw new UnknownCertExtensionError(type) 97 | 98 | cursor.readOrThrow(length) 99 | } 100 | 101 | const content = cursor.offset - start 102 | 103 | cursor.offset = start 104 | 105 | const payload = cursor.readAndCopyOrThrow(content) 106 | const signature = cursor.readAndCopyOrThrow(64) 107 | 108 | return new Ed25519Cert(type, version, certType, expiration, certKeyType, certKey, extensions, payload, signature) 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/relayed/relay_extend2/link.ts: -------------------------------------------------------------------------------- 1 | import { Cursor } from "@hazae41/cursor" 2 | 3 | export type RelayExtend2Link = 4 | | RelayExtend2LinkIPv4 5 | | RelayExtend2LinkIPv6 6 | | RelayExtend2LinkLegacyID 7 | | RelayExtend2LinkModernID 8 | 9 | export namespace RelayExtend2Link { 10 | 11 | export function fromAddressString(address: string) { 12 | return address.startsWith("[") 13 | ? RelayExtend2LinkIPv6.from(address) 14 | : RelayExtend2LinkIPv4.from(address) 15 | } 16 | 17 | } 18 | 19 | export class RelayExtend2LinkIPv4 { 20 | readonly #class = RelayExtend2LinkIPv4 21 | 22 | static readonly type = 0 23 | 24 | constructor( 25 | readonly hostname: string, 26 | readonly port: number, 27 | ) { } 28 | 29 | static from(host: string) { 30 | const { hostname, port } = new URL(`http://${host}`) 31 | 32 | return new RelayExtend2LinkIPv4(hostname, Number(port)) 33 | } 34 | 35 | sizeOrThrow() { 36 | return 1 + 1 + (4 * 1) + 2 37 | } 38 | 39 | writeOrThrow(cursor: Cursor) { 40 | cursor.writeUint8OrThrow(this.#class.type) 41 | cursor.writeUint8OrThrow(4 + 2) 42 | 43 | const [a, b, c, d] = this.hostname.split(".") 44 | cursor.writeUint8OrThrow(Number(a)) 45 | cursor.writeUint8OrThrow(Number(b)) 46 | cursor.writeUint8OrThrow(Number(c)) 47 | cursor.writeUint8OrThrow(Number(d)) 48 | 49 | cursor.writeUint16OrThrow(this.port) 50 | } 51 | 52 | } 53 | 54 | export class RelayExtend2LinkIPv6 { 55 | readonly #class = RelayExtend2LinkIPv6 56 | 57 | static readonly type = 1 58 | 59 | constructor( 60 | readonly hostname: string, 61 | readonly port: number, 62 | ) { } 63 | 64 | static from(addrress: string) { 65 | const { hostname, port } = new URL(`http://${addrress}`) 66 | 67 | const ip = hostname.slice(1, -1) 68 | 69 | return new RelayExtend2LinkIPv6(ip, Number(port)) 70 | } 71 | 72 | sizeOrThrow() { 73 | return 1 + 1 + (8 * 2) + 2 74 | } 75 | 76 | writeOrThrow(cursor: Cursor) { 77 | cursor.writeUint8OrThrow(this.#class.type) 78 | cursor.writeUint8OrThrow(16 + 2) 79 | 80 | const [a, b, c, d, e, f, g, h] = this.hostname.split(":") 81 | cursor.writeUint16OrThrow(Number(`0x${a}`) || 0) 82 | cursor.writeUint16OrThrow(Number(`0x${b}`) || 0) 83 | cursor.writeUint16OrThrow(Number(`0x${c}`) || 0) 84 | cursor.writeUint16OrThrow(Number(`0x${d}`) || 0) 85 | cursor.writeUint16OrThrow(Number(`0x${e}`) || 0) 86 | cursor.writeUint16OrThrow(Number(`0x${f}`) || 0) 87 | cursor.writeUint16OrThrow(Number(`0x${g}`) || 0) 88 | cursor.writeUint16OrThrow(Number(`0x${h}`) || 0) 89 | 90 | cursor.writeUint16OrThrow(this.port) 91 | } 92 | 93 | } 94 | 95 | export class RelayExtend2LinkLegacyID { 96 | readonly #class = RelayExtend2LinkLegacyID 97 | 98 | static readonly type = 2 99 | 100 | constructor( 101 | readonly fingerprint: Uint8Array 102 | ) { } 103 | 104 | sizeOrThrow() { 105 | return 1 + 1 + this.fingerprint.length 106 | } 107 | 108 | writeOrThrow(cursor: Cursor) { 109 | cursor.writeUint8OrThrow(this.#class.type) 110 | cursor.writeUint8OrThrow(20) 111 | cursor.writeOrThrow(this.fingerprint) 112 | } 113 | 114 | } 115 | 116 | export class RelayExtend2LinkModernID { 117 | readonly #class = RelayExtend2LinkModernID 118 | 119 | static readonly type = 3 120 | 121 | constructor( 122 | readonly fingerprint: Uint8Array 123 | ) { } 124 | 125 | sizeOrThrow() { 126 | return 1 + 1 + this.fingerprint.length 127 | } 128 | 129 | writeOrThrow(cursor: Cursor) { 130 | cursor.writeUint8OrThrow(this.#class.type) 131 | cursor.writeUint8OrThrow(32) 132 | cursor.writeOrThrow(this.fingerprint) 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /src/mods/tor/algorithms/ntor/ntor.ts: -------------------------------------------------------------------------------- 1 | import { Bytes, Uint8Array } from "@hazae41/bytes" 2 | import { Cursor } from "@hazae41/cursor" 3 | import { HASH_LEN, KEY_LEN } from "mods/tor/constants.js" 4 | 5 | export class InvalidNtorAuthError extends Error { 6 | readonly #class = InvalidNtorAuthError 7 | readonly name = this.#class.name 8 | 9 | constructor() { 10 | super(`Invalid Ntor auth`) 11 | } 12 | 13 | } 14 | 15 | export class NtorResponse { 16 | 17 | constructor( 18 | readonly public_y: Uint8Array<32>, 19 | readonly auth: Uint8Array<32> 20 | ) { } 21 | 22 | static readOrThrow(cursor: Cursor) { 23 | const publicY = cursor.readAndCopyOrThrow(32) 24 | const auth = cursor.readAndCopyOrThrow(32) 25 | 26 | return new NtorResponse(publicY, auth) 27 | } 28 | 29 | } 30 | 31 | export class NtorRequest { 32 | 33 | constructor( 34 | readonly public_x: Uint8Array<32>, 35 | readonly relayid_rsa: Uint8Array<20>, 36 | readonly ntor_onion_key: Uint8Array<32> 37 | ) { } 38 | 39 | sizeOrThrow() { 40 | return 0 41 | + this.relayid_rsa.length 42 | + this.ntor_onion_key.length 43 | + this.public_x.length 44 | } 45 | 46 | writeOrThrow(cursor: Cursor) { 47 | cursor.writeOrThrow(this.relayid_rsa) 48 | cursor.writeOrThrow(this.ntor_onion_key) 49 | cursor.writeOrThrow(this.public_x) 50 | } 51 | 52 | } 53 | 54 | export interface NtorResult { 55 | readonly auth: Uint8Array<32>, 56 | readonly nonce: Uint8Array, 57 | readonly forwardDigest: Uint8Array, 58 | readonly backwardDigest: Uint8Array, 59 | readonly forwardKey: Uint8Array, 60 | readonly backwardKey: Uint8Array 61 | } 62 | 63 | export namespace NtorResult { 64 | 65 | export async function finalizeOrThrow( 66 | shared_xy: Uint8Array<32>, 67 | shared_xb: Uint8Array<32>, 68 | relayid_rsa: Uint8Array<20>, 69 | public_b: Uint8Array<32>, 70 | public_x: Uint8Array<32>, 71 | public_y: Uint8Array<32> 72 | ): Promise { 73 | const protoid = "ntor-curve25519-sha256-1" 74 | 75 | const secret_input = new Cursor(new Uint8Array(32 + 32 + 20 + 32 + 32 + 32 + protoid.length)) 76 | secret_input.writeOrThrow(shared_xy) 77 | secret_input.writeOrThrow(shared_xb) 78 | secret_input.writeOrThrow(relayid_rsa) 79 | secret_input.writeOrThrow(public_b) 80 | secret_input.writeOrThrow(public_x) 81 | secret_input.writeOrThrow(public_y) 82 | secret_input.writeUtf8OrThrow(protoid) 83 | 84 | const t_mac = Bytes.fromUtf8(`${protoid}:mac`) 85 | const t_key = Bytes.fromUtf8(`${protoid}:key_extract`) 86 | const t_verify = Bytes.fromUtf8(`${protoid}:verify`) 87 | 88 | const kt_verify = await crypto.subtle.importKey("raw", t_verify, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) 89 | const verify = new Uint8Array(await crypto.subtle.sign("HMAC", kt_verify, secret_input.bytes)) 90 | 91 | const server = "Server" 92 | 93 | const auth_input = new Cursor(new Uint8Array(32 + 20 + 32 + 32 + 32 + protoid.length + server.length)) 94 | auth_input.writeOrThrow(verify) 95 | auth_input.writeOrThrow(relayid_rsa) 96 | auth_input.writeOrThrow(public_b) 97 | auth_input.writeOrThrow(public_y) 98 | auth_input.writeOrThrow(public_x) 99 | auth_input.writeUtf8OrThrow(protoid) 100 | auth_input.writeUtf8OrThrow(server) 101 | 102 | const t_mac_key = await crypto.subtle.importKey("raw", t_mac, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) 103 | const auth = new Uint8Array(await crypto.subtle.sign("HMAC", t_mac_key, auth_input.bytes)) as Uint8Array<32> 104 | 105 | const m_expand = Bytes.fromUtf8(`${protoid}:key_expand`) 106 | 107 | const secret_input_key = await crypto.subtle.importKey("raw", secret_input.bytes, "HKDF", false, ["deriveBits"]) 108 | const key_params = { name: "HKDF", hash: "SHA-256", info: m_expand, salt: t_key } 109 | const key_bytes = new Uint8Array(await crypto.subtle.deriveBits(key_params, secret_input_key, 8 * ((HASH_LEN * 3) + (KEY_LEN * 2)))) 110 | 111 | const key = new Cursor(key_bytes) 112 | const forwardDigest = key.readAndCopyOrThrow(HASH_LEN) 113 | const backwardDigest = key.readAndCopyOrThrow(HASH_LEN) 114 | const forwardKey = key.readAndCopyOrThrow(KEY_LEN) 115 | const backwardKey = key.readAndCopyOrThrow(KEY_LEN) 116 | const nonce = key.readAndCopyOrThrow(HASH_LEN) 117 | 118 | return { forwardDigest, backwardDigest, forwardKey, backwardKey, auth, nonce } 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /src/mods/snowflake/turbo/frame.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { Bitset } from "@hazae41/bitset"; 3 | import { Cursor } from "@hazae41/cursor"; 4 | 5 | export type TurboFrameError = 6 | | UnexpectedContinuationError 7 | | FragmentOverflowError 8 | 9 | export class FragmentOverflowError extends Error { 10 | readonly #class = FragmentOverflowError 11 | readonly name = this.#class.name 12 | 13 | constructor() { 14 | super(`Fragment size is greater than or equals to 2**20`) 15 | } 16 | 17 | } 18 | 19 | export class UnexpectedContinuationError extends Error { 20 | readonly #class = UnexpectedContinuationError 21 | readonly name = this.#class.name 22 | 23 | constructor() { 24 | super(`Unexpected continuation bit on third byte`) 25 | } 26 | 27 | } 28 | 29 | export interface TurboFrameParams { 30 | readonly padding: boolean, 31 | readonly fragment: T 32 | } 33 | 34 | export class TurboFrame { 35 | 36 | private constructor( 37 | readonly padding: boolean, 38 | readonly fragment: T, 39 | readonly fragmentSize: number 40 | ) { } 41 | 42 | static createOrThrow(params: TurboFrameParams): TurboFrame { 43 | const { padding, fragment } = params 44 | 45 | const fragmentSize = fragment.sizeOrThrow() 46 | 47 | if (fragmentSize >= (2 ** 20)) 48 | throw new FragmentOverflowError() 49 | 50 | return new TurboFrame(padding, fragment, fragmentSize) 51 | } 52 | 53 | sizeOrThrow() { 54 | if (this.fragmentSize < (2 ** 6)) 55 | return 1 + this.fragmentSize 56 | if (this.fragmentSize < (2 ** 13)) 57 | return 2 + this.fragmentSize 58 | if (this.fragmentSize < (2 ** 20)) 59 | return 3 + this.fragmentSize 60 | 61 | throw new FragmentOverflowError() 62 | } 63 | 64 | writeOrThrow(cursor: Cursor) { 65 | if (this.fragmentSize < (2 ** 6)) 66 | return this.writeOrThrow6(cursor, this.fragmentSize) 67 | if (this.fragmentSize < (2 ** 13)) 68 | return this.writeOrThrow13(cursor, this.fragmentSize) 69 | if (this.fragmentSize < (2 ** 20)) 70 | return this.writeOrThrow20(cursor, this.fragmentSize) 71 | 72 | throw new FragmentOverflowError() 73 | } 74 | 75 | writeOrThrow6(cursor: Cursor, size: number) { 76 | const first = new Bitset(size, 8) 77 | first.setBE(0, !this.padding) 78 | first.setBE(1, false) 79 | first.unsign() 80 | 81 | cursor.writeUint8OrThrow(first.value) 82 | this.fragment.writeOrThrow(cursor) 83 | } 84 | 85 | writeOrThrow13(cursor: Cursor, size: number) { 86 | let bits = "" 87 | bits += this.padding ? "0" : "1" 88 | bits += "1" 89 | 90 | const length = size.toString(2).padStart(13, "0") 91 | 92 | bits += length.slice(0, 6) 93 | bits += "0" 94 | bits += length.slice(6, 13) 95 | 96 | cursor.writeUint16OrThrow(parseInt(bits, 2)) 97 | this.fragment.writeOrThrow(cursor) 98 | } 99 | 100 | writeOrThrow20(cursor: Cursor, size: number) { 101 | let bits = "" 102 | bits += this.padding ? "0" : "1" 103 | bits += "1" 104 | 105 | const length = size.toString(2).padStart(20, "0") 106 | 107 | bits += length.slice(0, 6) 108 | bits += "1" 109 | bits += length.slice(6, 13) 110 | bits += "0" 111 | bits += length.slice(13, 20) 112 | 113 | cursor.writeUint24OrThrow(parseInt(bits, 2)) 114 | this.fragment.writeOrThrow(cursor) 115 | } 116 | 117 | /** 118 | * Read from bytes 119 | * @param binary bytes 120 | */ 121 | static readOrThrow(cursor: Cursor) { 122 | let lengthBits = "" 123 | 124 | const first = cursor.readUint8OrThrow() 125 | const bits = new Bitset(first, 8) 126 | 127 | const padding = !bits.getBE(0) 128 | const continuation = bits.getBE(1) 129 | 130 | lengthBits += bits.last(6).toString() 131 | 132 | if (continuation) { 133 | const second = cursor.readUint8OrThrow() 134 | const bits2 = new Bitset(second, 8) 135 | const continuation2 = bits2.getBE(0) 136 | 137 | lengthBits += bits2.last(7).toString() 138 | 139 | if (continuation2) { 140 | const third = cursor.readUint8OrThrow() 141 | const bits3 = new Bitset(third, 8) 142 | const continuation3 = bits3.getBE(0) 143 | 144 | lengthBits += bits3.last(7).toString() 145 | 146 | if (continuation3) 147 | throw new UnexpectedContinuationError() 148 | } 149 | } 150 | 151 | const length = parseInt(lengthBits, 2) 152 | const bytes = cursor.readAndCopyOrThrow(length) 153 | const fragment = new Opaque(bytes) 154 | 155 | return TurboFrame.createOrThrow({ padding, fragment }) 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /test/website/src/libs/sockets/index.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary" 2 | import { Box, Deferred, Stack } from "@hazae41/box" 3 | import { Disposer } from "@hazae41/disposer" 4 | import { Circuit } from "@hazae41/echalote" 5 | import { Fleche } from "@hazae41/fleche" 6 | import { Future } from "@hazae41/future" 7 | import { Pool } from "@hazae41/piscine" 8 | import { openAsOrThrow } from "libs/circuits" 9 | import { SizedPool } from "libs/pool" 10 | 11 | export async function createFlecheWebSocketOrThrow(stream: ReadableWritablePair, url: URL, signal = new AbortController().signal): Promise> { 12 | using stack = new Stack() 13 | 14 | const timeout = AbortSignal.timeout(5000) 15 | const subsignal = AbortSignal.any([signal, timeout]) 16 | 17 | subsignal.throwIfAborted() 18 | 19 | const socket = new Fleche.WebSocket(url) 20 | const future = new Future() 21 | 22 | using dsocket = new Box(new Disposer(socket, () => socket.close())) 23 | 24 | const onOpen = () => future.resolve() 25 | const onError = (cause: unknown) => future.reject(new Error("Errored", { cause })) 26 | const onAbort = () => future.reject(new Error("Aborted", { cause: subsignal.reason })) 27 | 28 | socket.addEventListener("open", onOpen, { passive: true }) 29 | stack.push(new Deferred(() => socket.removeEventListener("open", onOpen))) 30 | 31 | socket.addEventListener("error", onError, { passive: true }) 32 | stack.push(new Deferred(() => socket.removeEventListener("error", onError))) 33 | 34 | timeout.addEventListener("abort", onAbort, { passive: true }) 35 | stack.push(new Deferred(() => timeout.removeEventListener("abort", onAbort))) 36 | 37 | stream.readable.pipeTo(socket.inner.writable, { preventCancel: true }).catch(onError) 38 | socket.inner.readable.pipeTo(stream.writable, { preventAbort: true, preventClose: true }).catch(onError) 39 | 40 | await future.promise 41 | 42 | return dsocket.unwrapOrThrow() 43 | } 44 | 45 | export function createFlecheWebSocketPool(circuits: SizedPool, url: URL, size: number) { 46 | let update = Date.now() 47 | 48 | const pool: Pool> = new Pool>(async (params) => { 49 | const { index, signal } = params 50 | const [uuid] = crypto.randomUUID().split("-") 51 | 52 | while (!signal.aborted) { 53 | const start = Date.now() 54 | 55 | try { 56 | using stack = new Box(new Stack()) 57 | 58 | using substack = new Box(new Stack()) 59 | 60 | const circuit = await circuits.pool.takeCryptoRandomOrThrow() 61 | substack.getOrThrow().push(circuit) 62 | 63 | console.log(`Socket ${uuid} took circuit`) 64 | 65 | const stream = await openAsOrThrow(circuit, url.origin) 66 | substack.getOrThrow().push(stream) 67 | 68 | console.log(`Socket ${uuid} opened stream`) 69 | 70 | const socket = await createFlecheWebSocketOrThrow(stream.get(), url, signal) 71 | substack.getOrThrow().push(socket) 72 | 73 | console.log(`Socket ${uuid} opened`) 74 | 75 | const unsubstack = substack.unwrapOrThrow() 76 | 77 | const entry = new Box(new Disposer(socket.get(), () => unsubstack[Symbol.dispose]())) 78 | stack.getOrThrow().push(entry) 79 | 80 | const onCloseOrError = async (reason?: unknown) => pool.restart(index) 81 | 82 | socket.get().addEventListener("close", onCloseOrError, { passive: true }) 83 | stack.getOrThrow().push(new Deferred(() => socket.get().removeEventListener("close", onCloseOrError))) 84 | 85 | socket.get().addEventListener("error", onCloseOrError, { passive: true }) 86 | stack.getOrThrow().push(new Deferred(() => socket.get().removeEventListener("error", onCloseOrError))) 87 | 88 | const unstack = stack.unwrapOrThrow() 89 | 90 | console.log(`Socket ${uuid} ready`) 91 | 92 | return new Disposer(entry.moveOrThrow(), () => unstack[Symbol.dispose]()) 93 | } catch (e: unknown) { 94 | console.error(`Socket ${uuid} errored`, { e }) 95 | 96 | if (start < update) 97 | continue 98 | throw e 99 | } 100 | } 101 | 102 | throw new Error("Aborted", { cause: signal.reason }) 103 | }) 104 | 105 | const onStarted = () => { 106 | update = Date.now() 107 | 108 | for (const entry of pool.errEntries) 109 | pool.restart(entry.index) 110 | 111 | return 112 | } 113 | 114 | const stack = new Stack() 115 | 116 | circuits.pool.events.on("started", onStarted, { passive: true }) 117 | stack.push(new Deferred(() => circuits.pool.events.off("started", onStarted))) 118 | 119 | return new Disposer(SizedPool.start(pool, size), () => stack[Symbol.dispose]()) 120 | } -------------------------------------------------------------------------------- /test/website/pages/http.tsx: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { Disposer } from "@hazae41/disposer"; 3 | import { Circuit, Consensus, TorClientDuplex } from "@hazae41/echalote"; 4 | import { Ed25519 } from "@hazae41/ed25519"; 5 | import { fetch } from "@hazae41/fleche"; 6 | import { Mutex } from "@hazae41/mutex"; 7 | import { Pool } from "@hazae41/piscine"; 8 | import { Ok, Result } from "@hazae41/result"; 9 | import { Sha1 } from "@hazae41/sha1"; 10 | import { X25519 } from "@hazae41/x25519"; 11 | import { createCircuitPool, createStreamPool, createTorOrThrow, createTorPool } from "libs/circuits/circuits"; 12 | import { DependencyList, useCallback, useEffect, useMemo, useState } from "react"; 13 | 14 | async function superfetch(stream: ReadableWritablePair, Writable>) { 15 | const start = Date.now() 16 | 17 | const body = JSON.stringify({ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 67 }) 18 | const headers = { "content-type": "application/json" } 19 | 20 | const res = await fetch("https://eth.llamarpc.com", { method: "POST", headers, body, stream, preventClose: true, preventAbort: true, preventCancel: true }) 21 | 22 | console.log(await res.text(), `${Date.now() - start}ms`) 23 | } 24 | 25 | function useAsyncMemo(factory: () => Promise, deps: DependencyList) { 26 | const [state, setState] = useState() 27 | 28 | useEffect(() => { 29 | factory().then(setState) 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, deps) 32 | 33 | return state 34 | } 35 | 36 | export interface TorAndCircuits { 37 | tor: TorClientDuplex 38 | circuits: Mutex>> 39 | } 40 | 41 | export default function Page() { 42 | 43 | const tors = useAsyncMemo(async () => { 44 | // const ed25519 = Ed25519.fromNoble(noble_ed25519.ed25519) 45 | // const x25519 = X25519.fromNoble(noble_ed25519.x25519) 46 | // const sha1 = Sha1.fromNoble(noble_sha1.sha1) 47 | console.log("ed25519", Ed25519) 48 | 49 | Ed25519.set(await Ed25519.fromSafeOrBerith()) 50 | X25519.set(await X25519.fromSafeOrBerith()) 51 | Sha1.set(await Sha1.fromMorax()) 52 | 53 | // Echalote.Console.debugging = true 54 | // Cadenas.Console.debugging = true 55 | 56 | return createTorPool(async () => { 57 | return await createTorOrThrow() 58 | }, { capacity: 1 }) 59 | }, []) 60 | 61 | const consensus = useAsyncMemo(async () => { 62 | if (!tors) return 63 | 64 | return await Result.unthrow>(async t => { 65 | await new Promise(r => setTimeout(r, 1000)) 66 | console.log("Getting Tor...") 67 | const tor = await tors.inner.getCryptoRandomOrThrow().then(r => r.throw(t).inner.inner) 68 | console.log("Creating circuit...") 69 | using circuit = await tor.tryCreate(AbortSignal.timeout(5000)).then(r => r.throw(t)) 70 | 71 | console.log("Fetching consensus...") 72 | const consensus = await Consensus.tryFetch(circuit).then(r => r.throw(t)) 73 | 74 | return new Ok(consensus) 75 | }).then(r => r.unwrap()) 76 | }, [tors]) 77 | 78 | const circuits = useMemo(() => { 79 | if (!tors || !consensus) return 80 | 81 | return createCircuitPool(tors, consensus, { capacity: 9 }) 82 | }, [tors, consensus]) 83 | 84 | const streams = useMemo(() => { 85 | if (!circuits) return 86 | 87 | const url = new URL("https://eth.llamarpc.com") 88 | return createStreamPool(url, circuits, { capacity: 3 }) 89 | }, [circuits]) 90 | 91 | const onClick = useCallback(async () => { 92 | try { 93 | if (!streams || streams.locked) return 94 | if (!circuits || circuits.locked) return 95 | 96 | // const circuit = await circuits.inner.tryGetRandom().then(r => r.unwrap().result.get().inner) 97 | // const stream = await circuit.openAsOrThrow("https://eth.llamarpc.com") 98 | const start = Date.now() 99 | 100 | const stream = await streams.inner.tryGetRandom().then(r => r.unwrap().unwrap().inner.inner.inner) 101 | 102 | await stream.lock(async stream => { 103 | await superfetch(stream) 104 | // await new Promise(r => setTimeout(r, 100)) 105 | }) 106 | 107 | console.log(Date.now() - start) 108 | } catch (e: unknown) { 109 | console.error("onClick", { e }) 110 | } 111 | }, [streams, circuits]) 112 | 113 | const [_, setCounter] = useState(0) 114 | 115 | useEffect(() => { 116 | if (!circuits) return 117 | 118 | const onCreatedOrDeleted = async () => setCounter(c => c + 1) 119 | 120 | circuits.inner.events.on("created", onCreatedOrDeleted, { passive: true }) 121 | circuits.inner.events.on("deleted", onCreatedOrDeleted, { passive: true }) 122 | 123 | return () => { 124 | circuits.inner.events.off("created", onCreatedOrDeleted) 125 | circuits.inner.events.off("deleted", onCreatedOrDeleted) 126 | } 127 | }, [circuits]) 128 | 129 | return <> 130 | 133 | {circuits 134 | ?
135 | Circuit pool size: {circuits.inner.size} / {circuits.inner.capacity} 136 |
137 | :
138 | Loading... 139 |
} 140 | 141 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/cell.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Readable, Writable } from "@hazae41/binary"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | import { SecretCircuit } from "mods/tor/circuit.js"; 4 | import { SecretTorClientDuplex } from "mods/tor/client.js"; 5 | import { ExpectedCircuitError, InvalidCommandError, UnexpectedCircuitError } from "./errors.js"; 6 | 7 | export interface Cellable { 8 | readonly old: false 9 | readonly circuit: boolean, 10 | readonly command: number 11 | } 12 | 13 | export namespace Cellable { 14 | 15 | export interface Circuitful { 16 | readonly old: false 17 | readonly circuit: true, 18 | readonly command: number 19 | } 20 | 21 | export interface Circuitless { 22 | readonly old: false 23 | readonly circuit: false, 24 | readonly command: number 25 | } 26 | 27 | } 28 | 29 | export type Cell = 30 | | Cell.Circuitful 31 | | Cell.Circuitless 32 | 33 | export namespace Cell { 34 | 35 | export type PAYLOAD_LEN = 509 36 | export const PAYLOAD_LEN = 509 37 | 38 | export class Raw { 39 | 40 | constructor( 41 | readonly circuit: number, 42 | readonly command: number, 43 | readonly fragment: T 44 | ) { } 45 | 46 | unpackOrNull(tor: SecretTorClientDuplex) { 47 | if (this.circuit === 0) 48 | return new Circuitless(undefined, this.command, this.fragment) 49 | 50 | const circuit = tor.circuits.inner.get(this.circuit) 51 | 52 | if (circuit == null) 53 | return undefined 54 | 55 | return new Circuitful(circuit, this.command, this.fragment) 56 | } 57 | 58 | sizeOrThrow() { 59 | return this.command >= 128 60 | ? 4 + 1 + 2 + this.fragment.sizeOrThrow() 61 | : 4 + 1 + PAYLOAD_LEN 62 | } 63 | 64 | writeOrThrow(cursor: Cursor) { 65 | if (this.command >= 128) { 66 | cursor.writeUint32OrThrow(this.circuit) 67 | cursor.writeUint8OrThrow(this.command) 68 | 69 | const size = this.fragment.sizeOrThrow() 70 | cursor.writeUint16OrThrow(size) 71 | 72 | this.fragment.writeOrThrow(cursor) 73 | 74 | return 75 | } 76 | 77 | cursor.writeUint32OrThrow(this.circuit) 78 | cursor.writeUint8OrThrow(this.command) 79 | 80 | const payload = cursor.readOrThrow(PAYLOAD_LEN) 81 | const subcursor = new Cursor(payload) 82 | 83 | this.fragment.writeOrThrow(subcursor) 84 | 85 | subcursor.fillOrThrow(0, subcursor.remaining) 86 | } 87 | 88 | static readOrThrow(cursor: Cursor) { 89 | const circuit = cursor.readUint32OrThrow() 90 | const command = cursor.readUint8OrThrow() 91 | 92 | if (command >= 128) { 93 | const length = cursor.readUint16OrThrow() 94 | const bytes = cursor.readAndCopyOrThrow(length) 95 | const payload = new Opaque(bytes) 96 | 97 | return new Raw(circuit, command, payload) 98 | } 99 | 100 | const bytes = cursor.readAndCopyOrThrow(PAYLOAD_LEN) 101 | const payload = new Opaque(bytes) 102 | 103 | return new Raw(circuit, command, payload) 104 | } 105 | 106 | } 107 | 108 | export class Circuitful { 109 | readonly #raw: Raw 110 | 111 | constructor( 112 | readonly circuit: SecretCircuit, 113 | readonly command: number, 114 | readonly fragment: T 115 | ) { 116 | this.#raw = new Raw(circuit.id, command, fragment) 117 | } 118 | 119 | static from(circuit: SecretCircuit, cellable: T) { 120 | return new Circuitful(circuit, cellable.command, cellable) 121 | } 122 | 123 | sizeOrThrow() { 124 | return this.#raw.sizeOrThrow() 125 | } 126 | 127 | writeOrThrow(cursor: Cursor) { 128 | this.#raw.writeOrThrow(cursor) 129 | } 130 | 131 | static intoOrThrow(cell: Cell, readable: Cellable.Circuitful & Readable) { 132 | if (cell.command !== readable.command) 133 | throw new InvalidCommandError() 134 | if (cell.circuit == null) 135 | throw new ExpectedCircuitError() 136 | 137 | const fragment = cell.fragment.readIntoOrThrow(readable) 138 | 139 | return new Circuitful(cell.circuit, readable.command, fragment) 140 | } 141 | 142 | } 143 | 144 | export class Circuitless { 145 | readonly #raw: Raw 146 | 147 | constructor( 148 | readonly circuit: undefined, 149 | readonly command: number, 150 | readonly fragment: T 151 | ) { 152 | this.#raw = new Raw(0, command, fragment) 153 | } 154 | 155 | static from(circuit: undefined, cellable: T) { 156 | return new Circuitless(circuit, cellable.command, cellable) 157 | } 158 | 159 | sizeOrThrow() { 160 | return this.#raw.sizeOrThrow() 161 | } 162 | 163 | writeOrThrow(cursor: Cursor) { 164 | this.#raw.writeOrThrow(cursor) 165 | } 166 | 167 | static intoOrThrow(cell: Cell, readable: Cellable.Circuitless & Readable) { 168 | if (cell.command !== readable.command) 169 | throw new InvalidCommandError() 170 | if (cell.circuit != null) 171 | throw new UnexpectedCircuitError() 172 | 173 | const fragment = cell.fragment.readIntoOrThrow(readable) 174 | 175 | return new Circuitless(cell.circuit, readable.command, fragment) 176 | } 177 | 178 | } 179 | 180 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/old.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Readable, Writable } from "@hazae41/binary"; 2 | import { Cursor } from "@hazae41/cursor"; 3 | import { SecretCircuit } from "mods/tor/circuit.js"; 4 | import { SecretTorClientDuplex } from "mods/tor/client.js"; 5 | import { ExpectedCircuitError, InvalidCommandError, UnexpectedCircuitError } from "./errors.js"; 6 | 7 | export interface OldCellable { 8 | readonly old: true 9 | readonly circuit: boolean, 10 | readonly command: number 11 | } 12 | 13 | export namespace OldCellable { 14 | 15 | export interface Circuitful { 16 | readonly old: true 17 | readonly circuit: true, 18 | readonly command: number 19 | } 20 | 21 | export interface Circuitless { 22 | readonly old: true 23 | readonly circuit: false, 24 | readonly command: number 25 | } 26 | 27 | } 28 | 29 | export type OldCell = 30 | | OldCell.Circuitful 31 | | OldCell.Circuitless 32 | 33 | export namespace OldCell { 34 | 35 | export type PAYLOAD_LEN = 509 36 | export const PAYLOAD_LEN = 509 37 | 38 | export class Raw { 39 | 40 | constructor( 41 | readonly circuit: number, 42 | readonly command: number, 43 | readonly fragment: T 44 | ) { } 45 | 46 | unpackOrNull(tor: SecretTorClientDuplex) { 47 | if (this.circuit === 0) 48 | return new Circuitless(undefined, this.command, this.fragment) 49 | 50 | const circuit = tor.circuits.inner.get(this.circuit) 51 | 52 | if (circuit == null) 53 | return undefined 54 | 55 | return new Circuitful(circuit, this.command, this.fragment) 56 | } 57 | 58 | sizeOrThrow() { 59 | return this.command === 7 60 | ? 2 + 1 + 2 + this.fragment.sizeOrThrow() 61 | : 2 + 1 + PAYLOAD_LEN; 62 | } 63 | 64 | writeOrThrow(cursor: Cursor) { 65 | if (this.command === 7) { 66 | cursor.writeUint16OrThrow(this.circuit) 67 | cursor.writeUint8OrThrow(this.command) 68 | 69 | const size = this.fragment.sizeOrThrow() 70 | cursor.writeUint16OrThrow(size) 71 | 72 | this.fragment.writeOrThrow(cursor) 73 | 74 | return 75 | } 76 | 77 | cursor.writeUint16OrThrow(this.circuit) 78 | cursor.writeUint8OrThrow(this.command) 79 | 80 | const payload = cursor.readOrThrow(PAYLOAD_LEN) 81 | const subcursor = new Cursor(payload) 82 | 83 | this.fragment.writeOrThrow(subcursor) 84 | 85 | subcursor.fillOrThrow(0, subcursor.remaining) 86 | } 87 | 88 | static readOrThrow(cursor: Cursor) { 89 | const circuit = cursor.readUint16OrThrow() 90 | const command = cursor.readUint8OrThrow() 91 | 92 | if (command === 7) { 93 | const length = cursor.readUint16OrThrow() 94 | const bytes = cursor.readAndCopyOrThrow(length) 95 | const payload = new Opaque(bytes) 96 | 97 | return new Raw(circuit, command, payload) 98 | } 99 | 100 | const bytes = cursor.readAndCopyOrThrow(PAYLOAD_LEN) 101 | const payload = new Opaque(bytes) 102 | 103 | return new Raw(circuit, command, payload) 104 | } 105 | 106 | } 107 | 108 | export class Circuitful { 109 | readonly #raw: Raw 110 | 111 | constructor( 112 | readonly circuit: SecretCircuit, 113 | readonly command: number, 114 | readonly fragment: T 115 | ) { 116 | this.#raw = new Raw(circuit.id, command, fragment) 117 | } 118 | 119 | static from(circuit: SecretCircuit, cellable: T) { 120 | return new Circuitful(circuit, cellable.command, cellable) 121 | } 122 | 123 | sizeOrThrow() { 124 | return this.#raw.sizeOrThrow() 125 | } 126 | 127 | writeOrThrow(cursor: Cursor) { 128 | this.#raw.writeOrThrow(cursor) 129 | } 130 | 131 | static intoOrThrow(cell: OldCell, readable: OldCellable.Circuitful & Readable) { 132 | if (cell.command !== readable.command) 133 | throw new InvalidCommandError() 134 | if (cell.circuit == null) 135 | throw new ExpectedCircuitError() 136 | 137 | const fragment = cell.fragment.readIntoOrThrow(readable) 138 | 139 | return new Circuitful(cell.circuit, readable.command, fragment) 140 | } 141 | 142 | } 143 | 144 | export class Circuitless { 145 | readonly #raw: Raw 146 | 147 | constructor( 148 | readonly circuit: undefined, 149 | readonly command: number, 150 | readonly fragment: T 151 | ) { 152 | this.#raw = new Raw(0, command, fragment) 153 | } 154 | 155 | static from(circuit: undefined, cellable: T) { 156 | return new Circuitless(circuit, cellable.command, cellable) 157 | } 158 | 159 | sizeOrThrow() { 160 | return this.#raw.sizeOrThrow() 161 | } 162 | 163 | writeOrThrow(cursor: Cursor) { 164 | this.#raw.writeOrThrow(cursor) 165 | } 166 | 167 | static intoOrThrow(cell: OldCell, readable: OldCellable.Circuitless & Readable) { 168 | if (cell.command !== readable.command) 169 | throw new InvalidCommandError() 170 | if (cell.circuit != null) 171 | throw new UnexpectedCircuitError() 172 | 173 | const fragment = cell.fragment.readIntoOrThrow(readable) 174 | 175 | return new Circuitless(cell.circuit, readable.command, fragment) 176 | } 177 | 178 | } 179 | } -------------------------------------------------------------------------------- /src/mods/tor/consensus/consensus.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@hazae41/phobos"; 2 | import { Consensus } from "./consensus.js"; 3 | 4 | const microdesc = `onion-key 5 | -----BEGIN RSA PUBLIC KEY----- 6 | MIGJAoGBALJcqKBDfT41bLkkBvKSMuictvSQjwiV2GUBszYb0zgOZV2D6pfIM6/Z 7 | 5oMUXbxVU0qPxvM+80h3AIoMsmsDrl91AWIS1gMPE/kKtyGnA/WaX3RfwkWvYXZz 8 | 5Dwg1Hoh2L41yNxml6QlEWEEk+sGh899od1KMYH5WdanNq/4xBNrAgMBAAE= 9 | -----END RSA PUBLIC KEY----- 10 | ntor-onion-key NaEdxqudourIdG2Zhijv+9QSWS8iEsVq6NUExXah7GM 11 | id ed25519 uZ0YqbYpBJ8Ts8lomKs8PRlxPFucUJFayt/pWGilkd0` 12 | 13 | const microdescs = `r c0der AjUfyI0L8G9s3lRSZWZB5hGdvX4 2038-01-01 00:00:00 95.216.20.80 8080 0 14 | a [2a01:4f9:2a:14af::2]:8080 15 | m mkHw/LD1moosjemRD+GqSqXzzK1kOvK3ZwTsCPGJIFs 16 | s Fast Guard Running Stable V2Dir Valid 17 | v Tor 0.4.8.8 18 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 19 | w Bandwidth=34000 20 | r rome2 AjV5EbiC8ldnbnWwfs//WIXks0U 2038-01-01 00:00:00 185.146.232.243 9001 0 21 | a [2a06:1700:0:16b::11]:9001 22 | m bqFbVmdtoHQMXRA/w4KtTKXQ5J0otxAnqz+vcX7IWyY 23 | s Exit Fast Running V2Dir Valid 24 | v Tor 0.4.8.8 25 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 26 | w Bandwidth=5800 27 | r howlin Ali9rps1FxSwvMDavh15jWYuuog 2038-01-01 00:00:00 45.141.153.214 443 0 28 | m QZQfLsxKhJ3bP1UQzfZc/lAsH5ZdO7eQNnTF+mbNr3E 29 | s Fast Guard HSDir Running Stable V2Dir Valid 30 | v Tor 0.4.8.7 31 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 32 | w Bandwidth=72000 33 | r prsv Al3bAX15RgxKP2eV1S/vu1ahM/M 2038-01-01 00:00:00 45.158.77.29 9200 0 34 | a [2a04:ecc0:8:a8:4567:491:0:1]:9200 35 | m sqAO/nlM44Npw8+bvc0xRELEhrwi+VndMBtl7Ix9H1k 36 | s Fast Guard Running Stable V2Dir Valid 37 | v Tor 0.4.8.7 38 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 39 | w Bandwidth=25000 40 | r Assange029us AmSPLxNccpaiex9Z0q5O7CXZqHc 2038-01-01 00:00:00 74.48.220.106 9001 0 41 | m wFQ5tyogkqYjeiIPWEs3bViMCwJ0DIdJMJUy3ezLh34 42 | s Fast Running Stable Valid 43 | v Tor 0.4.8.9 44 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 45 | w Bandwidth=3500 46 | r middleIsenguard AmVLi5gDd1hgzfLH9xCf9ooTSao 2038-01-01 00:00:00 87.1.222.174 10101 0 47 | m 5G6mtD7rR8gAcA+9vN4L1fPJ+qEtevLfz03AmRHcUXY 48 | s Fast Guard HSDir Running Stable V2Dir Valid 49 | v Tor 0.4.8.7 50 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 51 | w Bandwidth=12000 52 | r danon AmxXo9zkiTvDMa0fi6briUvEoPk 2038-01-01 00:00:00 57.128.174.82 3333 0 53 | m nAAEyOnyxzVAAI+1bUYxZY8qCyFwfW8PpMsTTMnnLCY 54 | s Fast Guard HSDir Running Stable V2Dir Valid 55 | v Tor 0.4.7.13 56 | pr Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 57 | w Bandwidth=12000 58 | r TheBuckeyeNetwork AmyEHFz3r3HRMJO13J1d0tN2Rp4 2038-01-01 00:00:00 174.96.88.128 9001 0 59 | m GoLC/ntpzpO0epvoeQQYHbCYL7q2nGRhDwOV+Bw5U/Y 60 | s Fast HSDir Running Stable V2Dir Valid 61 | v Tor 0.4.7.13 62 | pr Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 63 | w Bandwidth=170 64 | r b8zsRelay1 AnD0XRqclFMyLdEbk3scAj4SCKs 2038-01-01 00:00:00 66.41.17.62 9001 0 65 | m y96liLQvN7gF48flHgTRuFF/4+U/30BviSBNVwaD5oY 66 | s Fast Running V2Dir Valid 67 | v Tor 0.4.8.9 68 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 69 | w Bandwidth=480 70 | r zagreus AnQPRy0dpcG3dHXxiUD+efFa9Lg 2038-01-01 00:00:00 81.169.222.158 9001 0 71 | a [2a01:238:4224:8d00:f3a9:25e6:4cb6:f3d]:9001 72 | m 5IDAqY97qJ5MNlVqY3+0XK803WuzCAECy59qTLNRHIw 73 | s Fast HSDir Running Stable V2Dir Valid 74 | v Tor 0.4.8.8 75 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 76 | w Bandwidth=540 77 | r Digitalcourage4ipeb An51yS8SMa5fe9ThU2aW/jBAxGA 2038-01-01 00:00:00 185.220.102.244 993 0 78 | a [2a0b:f4c1:2::244]:993 79 | m 6zzVgC32SNZtYIvm7TgCe6jGtehwEcwbLb6CKsEnVy8 80 | s Exit Fast Guard HSDir Running Stable V2Dir Valid 81 | v Tor 0.4.8.9 82 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 83 | w Bandwidth=33000 84 | r Kanellos AovvSoFXgoa6oTUsc+Rgm5kVxoc 2038-01-01 00:00:00 146.0.36.87 9007 0 85 | m bd1Ctt4aYI8YFvg76X7e68iKkf5mSUW+qEnaVbzdpPA 86 | s Fast Guard HSDir Running Stable V2Dir Valid 87 | v Tor 0.4.8.9 88 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 89 | w Bandwidth=24000 90 | r changeme AqDY3fTaTmAcHGYUO7DIoPLzyFc 2038-01-01 00:00:00 157.90.77.166 9001 0 91 | m KBBrw07FttX6hBApwJRMKnMUiAPuOjT0utAX2q7XEi8 92 | s Fast Guard HSDir Running Stable V2Dir Valid 93 | v Tor 0.4.8.8 94 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 95 | w Bandwidth=21000 96 | r LV426 AqWUEt5YmgCTaby/geVvDWSztGQ 2038-01-01 00:00:00 141.147.54.226 9001 0 97 | a [2603:c020:8012:8b01:afef:180d:1d92:d3d4]:9001 98 | m 8LiidxTziH538pVjEtJ+x7G6+Xe8lSLjUwYjtAbsk/s 99 | s Fast Running Stable V2Dir Valid 100 | v Tor 0.4.8.9 101 | pr Conflux=1 Cons=1-2 Desc=1-2 DirCache=2 FlowCtrl=1-2 HSDir=2 HSIntro=4-5 HSRend=1-2 Link=1-5 LinkAuth=1,3 Microdesc=1-2 Padding=2 Relay=1-4 102 | w Bandwidth=510` 103 | 104 | test("microdesc", async () => { 105 | console.log(Consensus.Microdesc.parseOrThrow(microdesc)) 106 | }) 107 | 108 | test("microdescs", async () => { 109 | console.log(Consensus.parseOrThrow(microdescs)) 110 | }) -------------------------------------------------------------------------------- /test/website/src/libs/circuits/index.ts: -------------------------------------------------------------------------------- 1 | import { Box, Deferred, Stack } from "@hazae41/box"; 2 | import { Ciphers, TlsClientDuplex } from "@hazae41/cadenas"; 3 | import { Disposer } from "@hazae41/disposer"; 4 | import { Circuit, Consensus, TorClientDuplex } from "@hazae41/echalote"; 5 | import { fetch } from "@hazae41/fleche"; 6 | import { loopOrThrow, Pool, Retry } from "@hazae41/piscine"; 7 | import { SizedPool } from "libs/pool"; 8 | 9 | export async function openAsOrThrow(circuit: Circuit, input: RequestInfo | URL) { 10 | const req = new Request(input) 11 | const url = new URL(req.url) 12 | 13 | if (url.protocol === "http:" || url.protocol === "ws:") { 14 | const tcp = await circuit.openOrThrow(url.hostname, Number(url.port) || 80) 15 | 16 | return new Disposer(tcp.outer, () => tcp.close()) 17 | } 18 | 19 | if (url.protocol === "https:" || url.protocol === "wss:") { 20 | const tcp = await circuit.openOrThrow(url.hostname, Number(url.port) || 443) 21 | 22 | const ciphers = [Ciphers.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384] 23 | const tls = new TlsClientDuplex({ host_name: url.hostname, ciphers }) 24 | 25 | tcp.outer.readable.pipeTo(tls.inner.writable).catch(() => { }) 26 | tls.inner.readable.pipeTo(tcp.outer.writable).catch(() => { }) 27 | 28 | return new Disposer(tls.outer, () => tcp.close()) 29 | } 30 | 31 | throw new Error(url.protocol) 32 | } 33 | 34 | export function createCircuitEntry(pool: Pool, index: number, circuit: Circuit) { 35 | using stack = new Box(new Stack()) 36 | 37 | const entry = new Box(circuit) 38 | stack.getOrThrow().push(entry) 39 | 40 | const onCloseOrError = async (reason?: unknown) => void pool.restart(index) 41 | 42 | stack.getOrThrow().push(new Deferred(circuit.events.on("close", onCloseOrError, { passive: true }))) 43 | stack.getOrThrow().push(new Deferred(circuit.events.on("error", onCloseOrError, { passive: true }))) 44 | 45 | const unstack = stack.unwrapOrThrow() 46 | 47 | return new Disposer(entry, () => unstack[Symbol.dispose]()) 48 | } 49 | 50 | export function createCircuitPool(tors: SizedPool, consensus: Consensus, size: number) { 51 | const middles = consensus.microdescs.filter(it => true 52 | && it.flags.includes("Fast") 53 | && it.flags.includes("Stable") 54 | && it.flags.includes("V2Dir")) 55 | 56 | const exits = consensus.microdescs.filter(it => true 57 | && it.flags.includes("Fast") 58 | && it.flags.includes("Stable") 59 | && it.flags.includes("Exit") 60 | && !it.flags.includes("BadExit")) 61 | 62 | let update = Date.now() 63 | 64 | const pool: Pool = new Pool(async (params) => { 65 | const { index, signal } = params 66 | const [uuid] = crypto.randomUUID().split("-") 67 | 68 | while (!signal.aborted) { 69 | const start = Date.now() 70 | 71 | try { 72 | const tor = await tors.pool.getOrThrow(index % tors.size, signal) 73 | 74 | const circuit = await loopOrThrow(async () => Retry.run(async () => { 75 | try { 76 | using circuit = new Box(await tor.createOrThrow(AbortSignal.timeout(1000))) 77 | 78 | console.log(`Circuit #${uuid} opened`) 79 | 80 | /** 81 | * Try to extend to middle relay 9 times before giving up this circuit 82 | */ 83 | await loopOrThrow(async () => { 84 | const head = middles[Math.floor(Math.random() * middles.length)] 85 | const body = await Consensus.Microdesc.fetchOrThrow(circuit.getOrThrow(), head, AbortSignal.timeout(1000)) 86 | await Retry.run(() => circuit.getOrThrow().extendOrThrow(body, AbortSignal.timeout(1000))) 87 | }, { max: 3 }) 88 | 89 | console.log(`Circuit #${uuid} extended once`) 90 | 91 | /** 92 | * Try to extend to exit relay 9 times before giving up this circuit 93 | */ 94 | await loopOrThrow(async () => { 95 | const head = exits[Math.floor(Math.random() * exits.length)] 96 | const body = await Consensus.Microdesc.fetchOrThrow(circuit.getOrThrow(), head, AbortSignal.timeout(1000)) 97 | await Retry.run(() => circuit.getOrThrow().extendOrThrow(body, AbortSignal.timeout(1000))) 98 | }, { max: 3 }) 99 | 100 | console.log(`Circuit #${uuid} extended twice`) 101 | 102 | /** 103 | * Try to open a stream to a reliable endpoint 104 | */ 105 | using stream = await openAsOrThrow(circuit.getOrThrow(), "http://example.com/") 106 | 107 | console.log(`Circuit #${uuid} speed test opend`) 108 | 109 | /** 110 | * Reliability test 111 | */ 112 | for (let i = 0; i < 3; i++) { 113 | /** 114 | * Speed test 115 | */ 116 | await fetch("http://example.com/", { stream: stream.inner, signal: AbortSignal.timeout(1000), preventAbort: true, preventCancel: true, preventClose: true }).then(r => r.text()) 117 | } 118 | 119 | console.log(`Circuit #${uuid} speed test done`) 120 | 121 | return circuit.unwrapOrThrow() 122 | } catch (e: unknown) { 123 | console.error(`Circuit #${uuid} thrown`, { e }) 124 | throw e 125 | } 126 | }), { max: 9 }) 127 | 128 | console.log(`Circuit #${uuid} ready`) 129 | 130 | return createCircuitEntry(pool, index, circuit) 131 | } catch (e: unknown) { 132 | console.error(`Circuit ${uuid} errored`, { e }) 133 | 134 | if (start < update) 135 | continue 136 | throw e 137 | } 138 | } 139 | 140 | throw new Error("Aborted", { cause: signal.reason }) 141 | }) 142 | 143 | const onStarted = () => { 144 | update = Date.now() 145 | 146 | for (const entry of pool.errEntries) 147 | pool.restart(entry.index) 148 | 149 | return 150 | } 151 | 152 | const stack = new Stack() 153 | 154 | tors.pool.events.on("started", onStarted, { passive: true }) 155 | stack.push(new Deferred(() => tors.pool.events.off("started", onStarted))) 156 | 157 | return new Disposer(SizedPool.start(pool, size), () => stack[Symbol.dispose]()) 158 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/relay_early/cell.ts: -------------------------------------------------------------------------------- 1 | import { AesWasm } from "@hazae41/aes.wasm"; 2 | import { Opaque, Readable, Writable } from "@hazae41/binary"; 3 | import { Bytes } from "@hazae41/bytes"; 4 | import { Cursor } from "@hazae41/cursor"; 5 | import { Cell, } from "mods/tor/binary/cells/cell.js"; 6 | import { SecretCircuit } from "mods/tor/circuit.js"; 7 | import { SecretTorStreamDuplex } from "mods/tor/stream.js"; 8 | import { ExpectedCircuitError, ExpectedStreamError, InvalidRelayCommandError, UnexpectedStreamError, UnknownStreamError, UnrecognisedRelayCellError } from "../../errors.js"; 9 | 10 | export interface RelayEarlyCellable { 11 | readonly rcommand: number, 12 | readonly early: true 13 | readonly stream: boolean 14 | } 15 | 16 | export namespace RelayEarlyCellable { 17 | 18 | export interface Streamful { 19 | readonly rcommand: number, 20 | readonly early: true 21 | readonly stream: true 22 | } 23 | 24 | export interface Streamless { 25 | readonly rcommand: number, 26 | readonly early: true 27 | readonly stream: false 28 | } 29 | 30 | } 31 | 32 | export type RelayEarlyCell = 33 | | RelayEarlyCell.Streamful 34 | | RelayEarlyCell.Streamless 35 | 36 | export namespace RelayEarlyCell { 37 | 38 | export const HEAD_LEN = 1 + 2 + 2 + 4 + 2 39 | export const DATA_LEN = Cell.PAYLOAD_LEN - HEAD_LEN 40 | 41 | export const command = 9 42 | 43 | export class Raw { 44 | 45 | constructor( 46 | readonly circuit: SecretCircuit, 47 | readonly stream: number, 48 | readonly rcommand: number, 49 | readonly fragment: T 50 | ) { } 51 | 52 | unpackOrThrow() { 53 | if (this.stream === 0) 54 | return new Streamless(this.circuit, undefined, this.rcommand, this.fragment) 55 | 56 | const stream = this.circuit.streams.get(this.stream) 57 | 58 | if (stream == null) 59 | throw new UnknownStreamError() 60 | 61 | return new Streamful(this.circuit, stream, this.rcommand, this.fragment) 62 | } 63 | 64 | cellOrThrow() { 65 | const cursor = new Cursor(new Uint8Array(Cell.PAYLOAD_LEN)) 66 | 67 | cursor.writeUint8OrThrow(this.rcommand) 68 | cursor.writeUint16OrThrow(0) 69 | cursor.writeUint16OrThrow(this.stream) 70 | 71 | const digestOffset = cursor.offset 72 | 73 | cursor.writeUint32OrThrow(0) 74 | 75 | const size = this.fragment.sizeOrThrow() 76 | cursor.writeUint16OrThrow(size) 77 | this.fragment.writeOrThrow(cursor) 78 | 79 | cursor.fillOrThrow(0, Math.min(cursor.remaining, 4)) 80 | cursor.writeOrThrow(Bytes.random(cursor.remaining)) 81 | 82 | const exit = this.circuit.targets[this.circuit.targets.length - 1] 83 | 84 | exit.forward_digest.updateOrThrow(cursor.bytes) 85 | 86 | using digestSlice = exit.forward_digest.finalizeOrThrow() 87 | 88 | cursor.offset = digestOffset 89 | cursor.writeOrThrow(digestSlice.bytes.subarray(0, 4)) 90 | 91 | using memory = new AesWasm.Memory(cursor.bytes) 92 | 93 | for (let i = this.circuit.targets.length - 1; i >= 0; i--) 94 | this.circuit.targets[i].forward_key.apply_keystream(memory) 95 | 96 | const fragment = new Opaque(new Uint8Array(memory.bytes)) 97 | 98 | return new Cell.Circuitful(this.circuit, RelayEarlyCell.command, fragment) 99 | } 100 | 101 | static uncellOrThrow(cell: Cell) { 102 | if (cell instanceof Cell.Circuitless) 103 | throw new ExpectedCircuitError() 104 | 105 | using memory = new AesWasm.Memory(cell.fragment.bytes) 106 | 107 | for (const target of cell.circuit.targets) { 108 | target.backward_key.apply_keystream(memory) 109 | 110 | const cursor = new Cursor(memory.bytes) 111 | 112 | const rcommand = cursor.readUint8OrThrow() 113 | const recognised = cursor.readUint16OrThrow() 114 | 115 | if (recognised !== 0) 116 | continue 117 | 118 | const stream = cursor.readUint16OrThrow() 119 | 120 | const offset = cursor.offset 121 | const digest4 = cursor.getAndCopyOrThrow(4) 122 | 123 | cursor.writeUint32OrThrow(0) 124 | 125 | using hasher = target.backward_digest.cloneOrThrow() 126 | using digest = hasher.updateOrThrow(cursor.bytes).finalizeOrThrow() 127 | 128 | if (!Bytes.equals2(digest4, digest.bytes.subarray(0, 4))) { 129 | cursor.offset = offset 130 | cursor.writeOrThrow(digest4) 131 | continue 132 | } 133 | 134 | target.backward_digest.updateOrThrow(cursor.bytes) 135 | 136 | const length = cursor.readUint16OrThrow() 137 | const bytes = cursor.readAndCopyOrThrow(length) 138 | const data = new Opaque(bytes) 139 | 140 | return new Raw(cell.circuit, stream, rcommand, data) 141 | } 142 | 143 | throw new UnrecognisedRelayCellError() 144 | } 145 | 146 | } 147 | 148 | export class Streamful { 149 | readonly #raw: Raw 150 | 151 | constructor( 152 | readonly circuit: SecretCircuit, 153 | readonly stream: SecretTorStreamDuplex, 154 | readonly rcommand: number, 155 | readonly fragment: T 156 | ) { 157 | this.#raw = new Raw(circuit, stream.id, rcommand, fragment) 158 | } 159 | 160 | static from(circuit: SecretCircuit, stream: SecretTorStreamDuplex, fragment: T) { 161 | return new Streamful(circuit, stream, fragment.rcommand, fragment) 162 | } 163 | 164 | cellOrThrow() { 165 | return this.#raw.cellOrThrow() 166 | } 167 | 168 | static intoOrThrow(cell: RelayEarlyCell, readable: RelayEarlyCellable.Streamful & Readable) { 169 | if (cell.rcommand !== readable.rcommand) 170 | throw new InvalidRelayCommandError() 171 | if (cell.stream == null) 172 | throw new ExpectedStreamError() 173 | 174 | const fragment = cell.fragment.readIntoOrThrow(readable) 175 | 176 | return new Streamful(cell.circuit, cell.stream, readable.rcommand, fragment) 177 | } 178 | 179 | } 180 | 181 | export class Streamless { 182 | readonly #raw: Raw 183 | 184 | constructor( 185 | readonly circuit: SecretCircuit, 186 | readonly stream: undefined, 187 | readonly rcommand: number, 188 | readonly fragment: T 189 | ) { 190 | this.#raw = new Raw(circuit, 0, rcommand, fragment) 191 | } 192 | 193 | static from(circuit: SecretCircuit, stream: undefined, fragment: T) { 194 | return new Streamless(circuit, stream, fragment.rcommand, fragment) 195 | } 196 | 197 | cellOrThrow() { 198 | return this.#raw.cellOrThrow() 199 | } 200 | 201 | static intoOrThrow(cell: RelayEarlyCell, readable: RelayEarlyCellable.Streamless & Readable) { 202 | if (cell.rcommand !== readable.rcommand) 203 | throw new InvalidRelayCommandError() 204 | if (cell.stream != null) 205 | throw new UnexpectedStreamError() 206 | 207 | const fragment = cell.fragment.readIntoOrThrow(readable) 208 | 209 | return new Streamless(cell.circuit, cell.stream, readable.rcommand, fragment) 210 | } 211 | 212 | } 213 | } -------------------------------------------------------------------------------- /src/mods/tor/certs/certs.ts: -------------------------------------------------------------------------------- 1 | import { Writable } from "@hazae41/binary"; 2 | import { Bytes } from "@hazae41/bytes"; 3 | import { Ed25519 } from "@hazae41/ed25519"; 4 | import { RsaPublicKey, RsaWasm } from "@hazae41/rsa.wasm"; 5 | import { X509 } from "@hazae41/x509"; 6 | import { CrossCert, Ed25519Cert, RsaCert, UnknownCertExtensionError } from "../index.js"; 7 | 8 | export type CertError = 9 | | DuplicatedCertError 10 | | UnknownCertError 11 | | ExpectedCertError 12 | | ExpiredCertError 13 | | PrematureCertError 14 | | InvalidSignatureError 15 | | UnknownCertExtensionError 16 | | InvalidCertError 17 | 18 | export class DuplicatedCertError extends Error { 19 | readonly #class = DuplicatedCertError 20 | readonly name = this.#class.name 21 | 22 | constructor() { 23 | super(`Duplicated certificate`) 24 | } 25 | 26 | } 27 | 28 | export class UnknownCertError extends Error { 29 | readonly #class = UnknownCertError 30 | readonly name = this.#class.name 31 | 32 | constructor() { 33 | super(`Unknown certificate`) 34 | } 35 | 36 | } 37 | 38 | export class ExpectedCertError extends Error { 39 | readonly #class = ExpectedCertError 40 | readonly name = this.#class.name 41 | 42 | constructor() { 43 | super(`Expected a certificate`) 44 | } 45 | 46 | } 47 | 48 | export class ExpiredCertError extends Error { 49 | readonly #class = ExpiredCertError 50 | readonly name = this.#class.name 51 | 52 | constructor() { 53 | super(`Expired certificate`) 54 | } 55 | 56 | } 57 | 58 | export class PrematureCertError extends Error { 59 | readonly #class = PrematureCertError 60 | readonly name = this.#class.name 61 | 62 | constructor() { 63 | super(`Premature certificate`) 64 | } 65 | 66 | } 67 | 68 | export class InvalidSignatureError extends Error { 69 | readonly #class = InvalidSignatureError 70 | readonly name = this.#class.name 71 | 72 | constructor() { 73 | super(`Invalid certificate signature`) 74 | } 75 | 76 | } 77 | 78 | export class InvalidCertError extends Error { 79 | readonly #class = InvalidCertError 80 | readonly name = this.#class.name 81 | 82 | constructor() { 83 | super(`Invalid certificate`) 84 | } 85 | 86 | } 87 | 88 | export interface Certs { 89 | readonly rsa_self: RsaCert, 90 | readonly rsa_to_tls?: RsaCert, 91 | readonly rsa_to_auth?: RsaCert, 92 | readonly rsa_to_ed: CrossCert, 93 | readonly ed_to_sign: Ed25519Cert, 94 | readonly sign_to_tls: Ed25519Cert, 95 | readonly sign_to_auth?: Ed25519Cert, 96 | } 97 | 98 | export namespace Certs { 99 | 100 | export async function verifyOrThrow(pcerts: Partial, tlsCerts?: X509.Certificate[]): Promise { 101 | const { rsa_self, rsa_to_ed, ed_to_sign, sign_to_tls } = pcerts 102 | 103 | if (tlsCerts == null) 104 | throw new ExpectedCertError() 105 | 106 | if (rsa_self == null) 107 | throw new ExpectedCertError() 108 | if (rsa_to_ed == null) 109 | throw new ExpectedCertError() 110 | if (ed_to_sign == null) 111 | throw new ExpectedCertError() 112 | if (sign_to_tls == null) 113 | throw new ExpectedCertError() 114 | 115 | const certs = { rsa_self, rsa_to_ed, ed_to_sign, sign_to_tls } 116 | 117 | const result = await Promise.all([ 118 | verifyRsaSelfOrThrow(certs), 119 | verifyRsaToEdOrThrow(certs), 120 | verifyEdToSigningOrThrow(certs), 121 | verifySigningToTlsOrThrow(certs, tlsCerts), 122 | ]).then(all => all.every(x => x === true)) 123 | 124 | if (result !== true) 125 | throw new Error(`Could not verify certs`) 126 | 127 | return certs 128 | } 129 | 130 | async function verifyRsaSelfOrThrow(certs: Certs): Promise { 131 | if (certs.rsa_self.verifyOrThrow() !== true) 132 | throw new Error(`Could not verify ID_SELF cert`) 133 | 134 | const length = certs.rsa_self.x509.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey.bytes.length 135 | 136 | /** 137 | * Only accept 1024-bits (128-bytes) public keys 138 | */ 139 | if (length !== 12 + 128) 140 | throw new InvalidCertError() 141 | 142 | const signed = X509.writeToBytesOrThrow(certs.rsa_self.x509.tbsCertificate) 143 | const publicKey = X509.writeToBytesOrThrow(certs.rsa_self.x509.tbsCertificate.subjectPublicKeyInfo) 144 | 145 | const signatureAlgorithm = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } } 146 | const signature = certs.rsa_self.x509.signatureValue.bytes 147 | 148 | const key = await crypto.subtle.importKey("spki", publicKey, signatureAlgorithm, true, ["verify"]); 149 | const verified = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, signed) 150 | 151 | if (verified !== true) 152 | throw new InvalidSignatureError() 153 | 154 | /** 155 | * We don't verify the RSA identity on Snowflake / Meek 156 | */ 157 | 158 | return true 159 | } 160 | 161 | async function verifyRsaToEdOrThrow(certs: Certs): Promise { 162 | if (certs.rsa_to_ed.verifyOrThrow() !== true) 163 | throw new Error(`Could not verify ID_TO_ED cert`) 164 | 165 | const publicKeyBytes = X509.writeToBytesOrThrow(certs.rsa_self.x509.tbsCertificate.subjectPublicKeyInfo) 166 | 167 | using publicKeyMemory = new RsaWasm.Memory(publicKeyBytes) 168 | using publicKeyPointer = RsaPublicKey.from_public_key_der(publicKeyMemory) 169 | 170 | const prefix = Bytes.fromUtf8("Tor TLS RSA/Ed25519 cross-certificate") 171 | const prefixed = Bytes.concat([prefix, certs.rsa_to_ed.payload]) 172 | const hashed = new Uint8Array(await crypto.subtle.digest("SHA-256", prefixed)) 173 | 174 | using hashedMemory = new RsaWasm.Memory(hashed) 175 | using signatureMemory = new RsaWasm.Memory(certs.rsa_to_ed.signature) 176 | 177 | const verified = publicKeyPointer.verify_pkcs1v15_unprefixed(hashedMemory, signatureMemory) 178 | 179 | if (verified !== true) 180 | throw new InvalidSignatureError() 181 | 182 | /** 183 | * We don't verify the Ed25519 identity on Snowflake / Meek 184 | */ 185 | 186 | return true 187 | } 188 | 189 | async function verifyEdToSigningOrThrow(certs: Certs): Promise { 190 | if (await certs.ed_to_sign.verifyOrThrow() !== true) 191 | throw new Error(`Could not verify ED_TO_SIGN cert`) 192 | 193 | using identity = await Ed25519.get().getOrThrow().VerifyingKey.importOrThrow(certs.rsa_to_ed.key) 194 | using signature = Ed25519.get().getOrThrow().Signature.importOrThrow(certs.ed_to_sign.signature) 195 | 196 | const verified = await identity.verifyOrThrow(certs.ed_to_sign.payload, signature) 197 | 198 | if (verified !== true) 199 | throw new InvalidSignatureError() 200 | 201 | return true 202 | } 203 | 204 | async function verifySigningToTlsOrThrow(certs: Certs, tlsCerts: X509.Certificate[]): Promise { 205 | if (await certs.sign_to_tls.verifyOrThrow() !== true) 206 | throw new Error(`Could not verify SIGNING_TO_TLS cert`) 207 | 208 | using identity = await Ed25519.get().getOrThrow().VerifyingKey.importOrThrow(certs.ed_to_sign.certKey) 209 | using signature = Ed25519.get().getOrThrow().Signature.importOrThrow(certs.sign_to_tls.signature) 210 | 211 | const verified = await identity.verifyOrThrow(certs.sign_to_tls.payload, signature) 212 | 213 | if (verified !== true) 214 | throw new InvalidSignatureError() 215 | 216 | const tls = Writable.writeToBytesOrThrow(tlsCerts[0].toDER()) 217 | const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", tls)) 218 | 219 | if (Bytes.equals(hash, certs.sign_to_tls.certKey) !== true) 220 | throw new InvalidCertError() 221 | 222 | return true 223 | } 224 | 225 | } -------------------------------------------------------------------------------- /src/mods/tor/binary/cells/direct/relay/cell.ts: -------------------------------------------------------------------------------- 1 | import { AesWasm } from "@hazae41/aes.wasm"; 2 | import { Opaque, Readable, Writable } from "@hazae41/binary"; 3 | import { Bytes, type Uint8Array } from "@hazae41/bytes"; 4 | import { Cursor } from "@hazae41/cursor"; 5 | import { Cell, } from "mods/tor/binary/cells/cell.js"; 6 | import { SecretCircuit } from "mods/tor/circuit.js"; 7 | import { SecretTorStreamDuplex } from "mods/tor/stream.js"; 8 | import { ExpectedCircuitError, ExpectedStreamError, InvalidRelayCommandError, UnexpectedStreamError, UnrecognisedRelayCellError } from "../../errors.js"; 9 | import { RelayDataCell } from "../../relayed/relay_data/cell.js"; 10 | 11 | export interface RelayCellable { 12 | readonly rcommand: number, 13 | readonly early: false, 14 | readonly stream: boolean 15 | } 16 | 17 | export namespace RelayCellable { 18 | 19 | export interface Streamful { 20 | readonly rcommand: number, 21 | readonly early: false, 22 | readonly stream: true 23 | } 24 | 25 | export interface Streamless { 26 | readonly rcommand: number, 27 | readonly early: false, 28 | readonly stream: false 29 | } 30 | 31 | } 32 | 33 | export type RelayCell = 34 | | RelayCell.Streamful 35 | | RelayCell.Streamless 36 | 37 | export namespace RelayCell { 38 | 39 | export const HEAD_LEN = 1 + 2 + 2 + 4 + 2 40 | export const DATA_LEN = Cell.PAYLOAD_LEN - HEAD_LEN 41 | 42 | export const command = 3 43 | 44 | export class Raw { 45 | 46 | constructor( 47 | readonly circuit: SecretCircuit, 48 | readonly stream: number, 49 | readonly rcommand: number, 50 | readonly fragment: T, 51 | readonly digest?: Uint8Array<20> 52 | ) { } 53 | 54 | unpackOrNull() { 55 | if (this.stream === 0) 56 | return new Streamless(this.circuit, undefined, this.rcommand, this.fragment, this.digest) 57 | 58 | const stream = this.circuit.streams.get(this.stream) 59 | 60 | if (stream == null) 61 | return 62 | 63 | return new Streamful(this.circuit, stream, this.rcommand, this.fragment, this.digest) 64 | } 65 | 66 | cellOrThrow() { 67 | const cursor = new Cursor(new Uint8Array(Cell.PAYLOAD_LEN)) 68 | 69 | cursor.writeUint8OrThrow(this.rcommand) 70 | cursor.writeUint16OrThrow(0) 71 | cursor.writeUint16OrThrow(this.stream) 72 | 73 | const digestOffset = cursor.offset 74 | 75 | cursor.writeUint32OrThrow(0) 76 | 77 | const size = this.fragment.sizeOrThrow() 78 | cursor.writeUint16OrThrow(size) 79 | this.fragment.writeOrThrow(cursor) 80 | 81 | cursor.fillOrThrow(0, Math.min(cursor.remaining, 4)) 82 | cursor.writeOrThrow(Bytes.random(cursor.remaining)) 83 | 84 | const exit = this.circuit.targets[this.circuit.targets.length - 1] 85 | 86 | exit.forward_digest.updateOrThrow(cursor.bytes) 87 | 88 | using digest = exit.forward_digest.finalizeOrThrow() 89 | const digest20 = digest.bytes.slice() as Uint8Array<20> 90 | 91 | if (this.rcommand === RelayDataCell.rcommand) { 92 | if (exit.package % 100 === 1) 93 | exit.digests.push(digest20) 94 | exit.package-- 95 | } 96 | 97 | cursor.offset = digestOffset 98 | cursor.writeOrThrow(digest20.subarray(0, 4)) 99 | 100 | using memory = new AesWasm.Memory(cursor.bytes) 101 | 102 | for (let i = this.circuit.targets.length - 1; i >= 0; i--) 103 | this.circuit.targets[i].forward_key.apply_keystream(memory) 104 | 105 | const fragment = new Opaque(new Uint8Array(memory.bytes)) 106 | 107 | return new Cell.Circuitful(this.circuit, RelayCell.command, fragment) 108 | } 109 | 110 | static uncellOrThrow(cell: Cell) { 111 | if (cell instanceof Cell.Circuitless) 112 | throw new ExpectedCircuitError() 113 | 114 | using memory = new AesWasm.Memory(cell.fragment.bytes) 115 | 116 | for (const target of cell.circuit.targets) { 117 | target.backward_key.apply_keystream(memory) 118 | 119 | const cursor = new Cursor(memory.bytes) 120 | 121 | const rcommand = cursor.readUint8OrThrow() 122 | const recognised = cursor.readUint16OrThrow() 123 | 124 | if (recognised !== 0) 125 | continue 126 | 127 | const stream = cursor.readUint16OrThrow() 128 | 129 | const offset = cursor.offset 130 | const digest4 = cursor.getAndCopyOrThrow(4) 131 | 132 | cursor.writeUint32OrThrow(0) 133 | 134 | using hasher = target.backward_digest.cloneOrThrow() 135 | using digest = hasher.updateOrThrow(cursor.bytes).finalizeOrThrow() 136 | const digest20 = digest.bytes.slice() as Uint8Array<20> 137 | 138 | if (!Bytes.equals2(digest4, digest.bytes.subarray(0, 4))) { 139 | cursor.offset = offset 140 | cursor.writeOrThrow(digest4) 141 | continue 142 | } 143 | 144 | target.backward_digest.updateOrThrow(cursor.bytes) 145 | 146 | const length = cursor.readUint16OrThrow() 147 | const bytes = cursor.readAndCopyOrThrow(length) 148 | const data = new Opaque(bytes) 149 | 150 | return new Raw(cell.circuit, stream, rcommand, data, digest20) 151 | } 152 | 153 | throw new UnrecognisedRelayCellError() 154 | } 155 | 156 | } 157 | 158 | export class Streamful { 159 | readonly #raw: Raw 160 | 161 | constructor( 162 | readonly circuit: SecretCircuit, 163 | readonly stream: SecretTorStreamDuplex, 164 | readonly rcommand: number, 165 | readonly fragment: T, 166 | readonly digest?: Uint8Array<20> 167 | ) { 168 | this.#raw = new Raw(circuit, stream.id, rcommand, fragment) 169 | } 170 | 171 | static from(circuit: SecretCircuit, stream: SecretTorStreamDuplex, fragment: T) { 172 | return new Streamful(circuit, stream, fragment.rcommand, fragment) 173 | } 174 | 175 | cellOrThrow() { 176 | return this.#raw.cellOrThrow() 177 | } 178 | 179 | static intoOrThrow(cell: RelayCell, readable: RelayCellable.Streamful & Readable) { 180 | if (cell.rcommand !== readable.rcommand) 181 | throw new InvalidRelayCommandError() 182 | if (cell.stream == null) 183 | throw new ExpectedStreamError() 184 | 185 | const fragment = cell.fragment.readIntoOrThrow(readable) 186 | 187 | return new Streamful(cell.circuit, cell.stream, readable.rcommand, fragment, cell.digest) 188 | } 189 | 190 | } 191 | 192 | export class Streamless { 193 | readonly #raw: Raw 194 | 195 | constructor( 196 | readonly circuit: SecretCircuit, 197 | readonly stream: undefined, 198 | readonly rcommand: number, 199 | readonly fragment: T, 200 | readonly digest?: Uint8Array<20> 201 | ) { 202 | this.#raw = new Raw(circuit, 0, rcommand, fragment) 203 | } 204 | 205 | static from(circuit: SecretCircuit, stream: undefined, fragment: T) { 206 | return new Streamless(circuit, stream, fragment.rcommand, fragment) 207 | } 208 | 209 | cellOrThrow(): Cell.Circuitful { 210 | return this.#raw.cellOrThrow() 211 | } 212 | 213 | static intoOrThrow(cell: RelayCell, readable: RelayCellable.Streamless & Readable) { 214 | if (cell.rcommand !== readable.rcommand) 215 | throw new InvalidRelayCommandError() 216 | if (cell.stream != null) 217 | throw new UnexpectedStreamError() 218 | 219 | const fragment = cell.fragment.readIntoOrThrow(readable) 220 | 221 | return new Streamless(cell.circuit, cell.stream, readable.rcommand, fragment, cell.digest) 222 | } 223 | 224 | } 225 | } -------------------------------------------------------------------------------- /test/website/pages/socket.tsx: -------------------------------------------------------------------------------- 1 | import { Consensus, Echalote } from "@hazae41/echalote"; 2 | import { Ed25519 } from "@hazae41/ed25519"; 3 | import { Ed25519Wasm } from "@hazae41/ed25519.wasm"; 4 | import { Sha1 } from "@hazae41/sha1"; 5 | import { Sha1Wasm } from "@hazae41/sha1.wasm"; 6 | import { X25519 } from "@hazae41/x25519"; 7 | import { X25519Wasm } from "@hazae41/x25519.wasm"; 8 | import { createCircuitPool } from "libs/circuits"; 9 | import { createFlecheWebSocketPool } from "libs/sockets"; 10 | import { createTorPool } from "libs/tors"; 11 | import { DependencyList, useCallback, useEffect, useMemo, useState } from "react"; 12 | 13 | const eth_call = { "jsonrpc": "2.0", "id": 21, "method": "eth_call", "params": [{ "to": "0x1f98415757620b543a52e61c46b32eb19261f984", "data": "0x1749e1e30000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000c2e074ec69a0dfb2997ba6c7d2e1e00000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000240178b8bfd4b63be5dc653a81f793311d472893a9fba1cf21a22bde8747ebf6af4a89cb910000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c2e074ec69a0dfb2997ba6c7d2e1e00000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000240178b8bfdac646d3c984f08aff3937648f4f95365e531e218e91a2bff2d6d798c30eb50000000000000000000000000000000000000000000000000000000000000000000000000000000000122eb74f9d0f1a5ed587f43d120c1c2bbdb9360b00000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000243b3b57ded4b63be5dc653a81f793311d472893a9fba1cf21a22bde8747ebf6af4a89cb9100000000000000000000000000000000000000000000000000000000000000000000000000000000169e633a2d1e6c10dd91238ba11c4a708dfef37c00000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000450d25bcd000000000000000000000000000000000000000000000000000000000000000000000000000000001f98415757620b543a52e61c46b32eb19261f98400000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000040f28c97d000000000000000000000000000000000000000000000000000000000000000000000000000000001f98415757620b543a52e61c46b32eb19261f98400000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000244d2301cc00000000000000000000000005cb48dd2b5911b90d464fc61d5febc2ce374fd40000000000000000000000000000000000000000000000000000000000000000000000000000000065770b5283117639760bea3f867b69b3697a91dd000000000000000000000000000000000000000000000000000000000002d2a80000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002470a0823100000000000000000000000005cb48dd2b5911b90d464fc61d5febc2ce374fd400000000000000000000000000000000000000000000000000000000" }, "0x10b7963"] } 14 | 15 | // const irn_subscribe = { "jsonrpc": "2.0", "method": "irn_subscribe", "params": { topic: "01d4c9f7a4d83ac4e162d518c3e430cd58f392e831f099b4f81c85f367c3aa09" }, "id": "1692551207426770945" } 16 | 17 | async function superfetch(socket: WebSocket) { 18 | const start = Date.now() 19 | 20 | socket.send(JSON.stringify({ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 67 })) 21 | 22 | const event = await new Promise((ok, err) => { 23 | socket.addEventListener("message", ok) 24 | socket.addEventListener("error", err) 25 | }) 26 | 27 | console.log(event.data, Date.now() - start) 28 | 29 | // socket.close() 30 | 31 | // await circuit.tryDestroy().then(r => r.unwrap()) 32 | } 33 | 34 | function useAsyncMemo(factory: () => Promise, deps: DependencyList) { 35 | const [state, setState] = useState() 36 | 37 | useEffect(() => { 38 | factory().then(setState) 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, deps) 41 | 42 | return state 43 | } 44 | 45 | export default function Page() { 46 | 47 | const tors = useAsyncMemo(async () => { 48 | // const ed25519 = Ed25519.fromNoble(noble_ed25519.ed25519) 49 | // const x25519 = X25519.fromNoble(noble_ed25519.x25519) 50 | // const sha1 = Sha1.fromNoble(noble_sha1.sha1) 51 | 52 | await Promise.all([Ed25519Wasm.initBundled(), X25519Wasm.initBundled(), Sha1Wasm.initBundled()]) 53 | 54 | Ed25519.set(await Ed25519.fromNativeOrWasm(Ed25519Wasm)) 55 | X25519.set(await X25519.fromNativeOrWasm(X25519Wasm)) 56 | Sha1.set(Sha1.fromWasm(Sha1Wasm)) 57 | 58 | Echalote.Console.debugging = true 59 | // Cadenas.Console.debugging = true 60 | 61 | return createTorPool(1) 62 | }, []) 63 | 64 | const consensus = useAsyncMemo(async () => { 65 | if (!tors) return 66 | 67 | const tor = await tors.get().pool.getCryptoRandomOrThrow() 68 | using circuit = await tor.createOrThrow(AbortSignal.timeout(5000)) 69 | 70 | return await Consensus.fetchOrThrow(circuit) 71 | }, [tors]) 72 | 73 | const circuits = useMemo(() => { 74 | if (!tors || !consensus) return 75 | 76 | return createCircuitPool(tors.get(), consensus, 9) 77 | }, [tors, consensus]) 78 | 79 | const sockets = useMemo(() => { 80 | if (!circuits) return 81 | 82 | const url = new URL("wss://ethereum.publicnode.com") 83 | 84 | return createFlecheWebSocketPool(circuits.get(), url, 3) 85 | }, [circuits]) 86 | 87 | const onClick = useCallback(async () => { 88 | if (!sockets) 89 | return 90 | 91 | try { 92 | const start = Date.now() 93 | 94 | using socket = await sockets.get().pool.takeCryptoRandomOrThrow() 95 | 96 | await superfetch(socket.inner) 97 | 98 | console.log("superfetch", Date.now() - start) 99 | } catch (e: unknown) { 100 | console.error("onClick", { e }) 101 | } 102 | }, [sockets]) 103 | 104 | const [_, setCounter] = useState(0) 105 | 106 | useEffect(() => { 107 | if (!circuits) 108 | return 109 | if (!sockets) 110 | return 111 | 112 | const onCreatedOrDeleted = () => setCounter(c => c + 1) 113 | 114 | circuits.get().pool.events.on("created", onCreatedOrDeleted, { passive: true }) 115 | circuits.get().pool.events.on("deleted", onCreatedOrDeleted, { passive: true }) 116 | 117 | sockets.get().pool.events.on("created", onCreatedOrDeleted, { passive: true }) 118 | sockets.get().pool.events.on("deleted", onCreatedOrDeleted, { passive: true }) 119 | 120 | return () => { 121 | circuits.get().pool.events.off("created", onCreatedOrDeleted) 122 | circuits.get().pool.events.off("deleted", onCreatedOrDeleted) 123 | 124 | sockets.get().pool.events.off("created", onCreatedOrDeleted) 125 | sockets.get().pool.events.off("deleted", onCreatedOrDeleted) 126 | } 127 | }, [circuits, sockets]) 128 | 129 | return <> 130 | 133 | {circuits 134 | ?
135 | Circuit pool size: {circuits.get().pool.size} / {circuits.get().size} 136 |
137 | :
138 | Loading... 139 |
} 140 | {sockets 141 | ?
142 | Socket pool size: {sockets.get().pool.size} / {sockets.get().size} 143 |
144 | :
145 | Loading... 146 |
} 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/mods/tor/stream.ts: -------------------------------------------------------------------------------- 1 | import { Opaque, Writable } from "@hazae41/binary"; 2 | import { FullDuplex } from "@hazae41/cascade"; 3 | import { Cursor } from "@hazae41/cursor"; 4 | import { CloseEvents, ErrorEvents, SuperEventTarget } from "@hazae41/plume"; 5 | import { Console } from "mods/console/index.js"; 6 | import { RelayCell } from "mods/tor/binary/cells/direct/relay/cell.js"; 7 | import { RelayDataCell } from "mods/tor/binary/cells/relayed/relay_data/cell.js"; 8 | import { RelayEndCell } from "mods/tor/binary/cells/relayed/relay_end/cell.js"; 9 | import { SecretCircuit } from "mods/tor/circuit.js"; 10 | import { RelayConnectedCell } from "./binary/cells/relayed/relay_connected/cell.js"; 11 | import { RelayEndReason, RelayEndReasonOther } from "./binary/cells/relayed/relay_end/reason.js"; 12 | import { RelaySendmeStreamCell } from "./binary/cells/relayed/relay_sendme/cell.js"; 13 | 14 | export class TorStreamDuplex { 15 | 16 | readonly #secret: SecretTorStreamDuplex 17 | 18 | constructor(secret: SecretTorStreamDuplex) { 19 | this.#secret = secret 20 | } 21 | 22 | [Symbol.dispose]() { 23 | this.close() 24 | } 25 | 26 | get id() { 27 | return this.#secret.id 28 | } 29 | 30 | get type() { 31 | return this.#secret.type 32 | } 33 | 34 | get inner() { 35 | return this.#secret.inner 36 | } 37 | 38 | get outer() { 39 | return this.#secret.outer 40 | } 41 | 42 | error(reason?: unknown) { 43 | this.#secret.error(reason) 44 | } 45 | 46 | close() { 47 | this.#secret.close() 48 | } 49 | 50 | } 51 | 52 | export class RelayEndedError extends Error { 53 | readonly #class = RelayEndedError 54 | readonly name = this.#class.name 55 | 56 | constructor( 57 | readonly reason: RelayEndReason 58 | ) { 59 | super(`Relay ended`, { cause: reason }) 60 | } 61 | 62 | } 63 | 64 | export type TorStreamEvents = 65 | & CloseEvents 66 | & ErrorEvents 67 | & { connected: () => void } 68 | 69 | export type SecretTorStreamDuplexType = 70 | | "external" 71 | | "directory" 72 | 73 | export class SecretTorStreamDuplex { 74 | readonly #class = SecretTorStreamDuplex 75 | 76 | readonly duplex: FullDuplex 77 | 78 | readonly events = new SuperEventTarget() 79 | 80 | delivery = 500 81 | package = 500 82 | 83 | #onClean: () => void 84 | 85 | constructor( 86 | readonly type: SecretTorStreamDuplexType, 87 | readonly id: number, 88 | readonly circuit: SecretCircuit 89 | ) { 90 | this.duplex = new FullDuplex({ 91 | output: { 92 | write: c => this.#onOutputWrite(c), 93 | }, 94 | error: e => this.#onDuplexError(e), 95 | close: () => this.#onDuplexClose() 96 | }) 97 | 98 | const onCircuitClose = this.#onCircuitClose.bind(this) 99 | const onCircuitError = this.#onCircuitError.bind(this) 100 | 101 | const onRelayConnectedCell = this.#onRelayConnectedCell.bind(this) 102 | const onRelayDataCell = this.#onRelayDataCell.bind(this) 103 | const onRelayEndCell = this.#onRelayEndCell.bind(this) 104 | 105 | this.circuit.events.on("close", onCircuitClose, { passive: true }) 106 | this.circuit.events.on("error", onCircuitError, { passive: true }) 107 | 108 | this.circuit.events.on("RELAY_CONNECTED", onRelayConnectedCell, { passive: true }) 109 | this.circuit.events.on("RELAY_DATA", onRelayDataCell, { passive: true }) 110 | this.circuit.events.on("RELAY_END", onRelayEndCell, { passive: true }) 111 | 112 | this.#onClean = () => { 113 | this.circuit.events.off("close", onCircuitClose) 114 | this.circuit.events.off("error", onCircuitError) 115 | 116 | this.circuit.events.off("RELAY_CONNECTED", onRelayConnectedCell) 117 | this.circuit.events.off("RELAY_DATA", onRelayDataCell) 118 | this.circuit.events.off("RELAY_END", onRelayEndCell) 119 | 120 | this.circuit.streams.delete(this.id) 121 | 122 | this.#onClean = () => { } 123 | } 124 | } 125 | 126 | [Symbol.dispose]() { 127 | this.close() 128 | } 129 | 130 | get inner() { 131 | return this.duplex.inner 132 | } 133 | 134 | get outer() { 135 | return this.duplex.outer 136 | } 137 | 138 | get input() { 139 | return this.duplex.input 140 | } 141 | 142 | get output() { 143 | return this.duplex.output 144 | } 145 | 146 | get closed() { 147 | return this.duplex.closed 148 | } 149 | 150 | close() { 151 | this.duplex.close() 152 | } 153 | 154 | error(reason?: unknown) { 155 | this.duplex.error(reason) 156 | } 157 | 158 | async #onDuplexClose() { 159 | if (!this.circuit.closed) { 160 | const relay_end_cell = new RelayEndCell(new RelayEndReasonOther(RelayEndCell.reasons.REASON_DONE)) 161 | const relay_cell = RelayCell.Streamful.from(this.circuit, this, relay_end_cell) 162 | this.circuit.tor.output.enqueue(relay_cell.cellOrThrow()) 163 | 164 | this.package-- 165 | } 166 | 167 | await this.events.emit("close") 168 | 169 | this.#onClean() 170 | } 171 | 172 | async #onDuplexError(reason?: unknown) { 173 | if (!this.circuit.closed) { 174 | const relay_end_cell = new RelayEndCell(new RelayEndReasonOther(RelayEndCell.reasons.REASON_MISC)) 175 | const relay_cell = RelayCell.Streamful.from(this.circuit, this, relay_end_cell) 176 | this.circuit.tor.output.enqueue(relay_cell.cellOrThrow()) 177 | 178 | this.package-- 179 | } 180 | 181 | await this.events.emit("error", reason) 182 | 183 | this.#onClean() 184 | } 185 | 186 | async #onCircuitClose() { 187 | Console.debug(`${this.#class.name}.onCircuitClose`) 188 | 189 | if (this.duplex.closing) 190 | return 191 | 192 | this.duplex.close() 193 | } 194 | 195 | async #onCircuitError(reason?: unknown) { 196 | Console.debug(`${this.#class.name}.onCircuitError`, { reason }) 197 | 198 | if (this.duplex.closing) 199 | return 200 | 201 | this.duplex.error(reason) 202 | } 203 | 204 | async #onRelayConnectedCell(cell: RelayCell.Streamful) { 205 | if (cell.stream !== this) 206 | return 207 | 208 | if (this.type === "directory") { 209 | await this.events.emit("connected") 210 | return 211 | } 212 | 213 | if (this.type === "external") { 214 | const cell2 = RelayCell.Streamful.intoOrThrow(cell, RelayConnectedCell) 215 | 216 | Console.debug(`${this.#class.name}.onRelayConnectedCell`, cell2) 217 | 218 | await this.events.emit("connected") 219 | return 220 | } 221 | } 222 | 223 | async #onRelayDataCell(cell: RelayCell.Streamful>) { 224 | if (cell.stream !== this) 225 | return 226 | 227 | Console.debug(`${this.#class.name}.onRelayDataCell`, cell) 228 | 229 | this.delivery-- 230 | 231 | if (this.delivery === 450) { 232 | this.delivery = 500 233 | 234 | const sendme = new RelaySendmeStreamCell() 235 | const sendme_cell = RelayCell.Streamful.from(this.circuit, this, sendme) 236 | this.circuit.tor.output.enqueue(sendme_cell.cellOrThrow()) 237 | } 238 | 239 | this.input.enqueue(cell.fragment.fragment) 240 | } 241 | 242 | async #onRelayEndCell(cell: RelayCell.Streamful) { 243 | if (cell.stream !== this) 244 | return 245 | 246 | Console.debug(`${this.#class.name}.onRelayEndCell`, cell) 247 | 248 | if (this.duplex.closing) 249 | return 250 | 251 | if (cell.fragment.reason.id === RelayEndCell.reasons.REASON_DONE) 252 | this.duplex.close() 253 | else 254 | this.duplex.error(new RelayEndedError(cell.fragment.reason)) 255 | } 256 | 257 | async #onOutputWrite(writable: Writable) { 258 | if (writable.sizeOrThrow() > RelayCell.DATA_LEN) 259 | return await this.#onWriteChunked(writable) 260 | 261 | return await this.#onWriteDirect(writable) 262 | } 263 | 264 | async #onWriteDirect(writable: Writable) { 265 | const relay_data_cell = new RelayDataCell(writable) 266 | const relay_cell = RelayCell.Streamful.from(this.circuit, this, relay_data_cell) 267 | 268 | this.circuit.tor.output.enqueue(relay_cell.cellOrThrow()) 269 | 270 | this.package-- 271 | } 272 | 273 | async #onWriteChunked(writable: Writable) { 274 | const bytes = Writable.writeToBytesOrThrow(writable) 275 | const cursor = new Cursor(bytes) 276 | 277 | for (const chunk of cursor.splitOrThrow(RelayCell.DATA_LEN)) 278 | await this.#onWriteDirect(new Opaque(chunk)) 279 | 280 | return 281 | } 282 | 283 | } -------------------------------------------------------------------------------- /src/mods/tor/circuit.ts: -------------------------------------------------------------------------------- 1 | import { Aes128Ctr128BEKey, AesWasm } from "@hazae41/aes.wasm"; 2 | import { Base64 } from "@hazae41/base64"; 3 | import { Opaque } from "@hazae41/binary"; 4 | import { Bitset } from "@hazae41/bitset"; 5 | import { Bytes } from "@hazae41/bytes"; 6 | import { Future } from "@hazae41/future"; 7 | import { Option } from "@hazae41/option"; 8 | import { CloseEvents, ErrorEvents, Plume, SuperEventTarget } from "@hazae41/plume"; 9 | import { Sha1 } from "@hazae41/sha1"; 10 | import { X25519 } from "@hazae41/x25519"; 11 | import { Console } from "mods/console/index.js"; 12 | import { Ntor } from "mods/tor/algorithms/ntor/index.js"; 13 | import { DestroyCell } from "mods/tor/binary/cells/direct/destroy/cell.js"; 14 | import { RelayBeginCell } from "mods/tor/binary/cells/relayed/relay_begin/cell.js"; 15 | import { RelayDataCell } from "mods/tor/binary/cells/relayed/relay_data/cell.js"; 16 | import { RelayEndCell } from "mods/tor/binary/cells/relayed/relay_end/cell.js"; 17 | import { RelayExtend2Cell } from "mods/tor/binary/cells/relayed/relay_extend2/cell.js"; 18 | import { RelayExtend2Link, RelayExtend2LinkIPv4, RelayExtend2LinkIPv6, RelayExtend2LinkLegacyID, RelayExtend2LinkModernID } from "mods/tor/binary/cells/relayed/relay_extend2/link.js"; 19 | import { RelayExtended2Cell } from "mods/tor/binary/cells/relayed/relay_extended2/cell.js"; 20 | import { RelayTruncateCell } from "mods/tor/binary/cells/relayed/relay_truncate/cell.js"; 21 | import { RelayTruncatedCell } from "mods/tor/binary/cells/relayed/relay_truncated/cell.js"; 22 | import { SecretTorClientDuplex } from "mods/tor/client.js"; 23 | import { SecretTorStreamDuplex, TorStreamDuplex } from "mods/tor/stream.js"; 24 | import { Target } from "mods/tor/target.js"; 25 | import { InvalidNtorAuthError, NtorResult } from "./algorithms/ntor/ntor.js"; 26 | import { Cell } from "./binary/cells/cell.js"; 27 | import { RelayCell } from "./binary/cells/direct/relay/cell.js"; 28 | import { RelayEarlyCell } from "./binary/cells/direct/relay_early/cell.js"; 29 | import { RelayBeginDirCell } from "./binary/cells/relayed/relay_begin_dir/cell.js"; 30 | import { Consensus } from "./consensus/consensus.js"; 31 | import { HASH_LEN } from "./constants.js"; 32 | 33 | export const IPv6 = { 34 | always: 3, 35 | preferred: 2, 36 | avoided: 1, 37 | never: 0 38 | } as const 39 | 40 | export interface CircuitOpenParams { 41 | /** 42 | * Wait RELAY_CONNECTED 43 | */ 44 | readonly wait?: boolean 45 | 46 | /** 47 | * IPv6 preference 48 | */ 49 | readonly ipv6?: keyof typeof IPv6 50 | } 51 | 52 | export class UnknownProtocolError extends Error { 53 | readonly #class = UnknownProtocolError 54 | readonly name = this.#class.name 55 | 56 | constructor( 57 | readonly protocol: string 58 | ) { 59 | super(`Unknown protocol "${protocol}"`) 60 | } 61 | 62 | } 63 | 64 | export class DestroyedError extends Error { 65 | readonly #class = DestroyedError 66 | readonly name = this.#class.name 67 | 68 | constructor( 69 | readonly reason: number 70 | ) { 71 | super(`Circuit destroyed`, { cause: reason }) 72 | } 73 | 74 | } 75 | 76 | export class ExtendError extends Error { 77 | readonly #class = ExtendError 78 | readonly name = this.#class.name 79 | 80 | constructor(options: ErrorOptions) { 81 | super(`Could not extend`, options) 82 | } 83 | 84 | static from(cause: unknown) { 85 | return new ExtendError({ cause }) 86 | } 87 | 88 | } 89 | 90 | export class OpenError extends Error { 91 | readonly #class = OpenError 92 | readonly name = this.#class.name 93 | 94 | constructor(options: ErrorOptions) { 95 | super(`Could not open`, options) 96 | } 97 | 98 | static from(cause: unknown) { 99 | return new OpenError({ cause }) 100 | } 101 | 102 | } 103 | 104 | export class TruncateError extends Error { 105 | readonly #class = TruncateError 106 | readonly name = this.#class.name 107 | 108 | constructor(options: ErrorOptions) { 109 | super(`Could not truncate`, options) 110 | } 111 | 112 | static from(cause: unknown) { 113 | return new TruncateError({ cause }) 114 | } 115 | 116 | } 117 | 118 | export class Circuit { 119 | 120 | readonly events = new SuperEventTarget() 121 | 122 | readonly #secret: SecretCircuit 123 | 124 | constructor(secret: SecretCircuit) { 125 | this.#secret = secret 126 | 127 | const onClose = this.#onClose.bind(this) 128 | this.#secret.events.on("close", onClose) 129 | 130 | const onError = this.#onError.bind(this) 131 | this.#secret.events.on("error", onError) 132 | } 133 | 134 | [Symbol.dispose]() { 135 | this.#secret[Symbol.dispose]() 136 | } 137 | 138 | async [Symbol.asyncDispose]() { 139 | this.#secret[Symbol.asyncDispose]() 140 | } 141 | 142 | get id() { 143 | return this.#secret.id 144 | } 145 | 146 | get closed() { 147 | return Boolean(this.#secret.closed) 148 | } 149 | 150 | async #onClose() { 151 | return await this.events.emit("close", [undefined]) 152 | } 153 | 154 | async #onError(reason?: unknown) { 155 | return await this.events.emit("error", [reason]) 156 | } 157 | 158 | async extendOrThrow(microdesc: Consensus.Microdesc, signal = new AbortController().signal) { 159 | return await this.#secret.extendOrThrow(microdesc, signal) 160 | } 161 | 162 | async openOrThrow(hostname: string, port: number, params?: CircuitOpenParams, signal = new AbortController().signal) { 163 | return await this.#secret.openOrThrow(hostname, port, params, signal) 164 | } 165 | 166 | async openDirOrThrow(params?: CircuitOpenParams, signal = new AbortController().signal) { 167 | return await this.#secret.openDirOrThrow(params, signal) 168 | } 169 | 170 | async close() { 171 | return await this.#secret.close() 172 | } 173 | 174 | } 175 | 176 | export type SecretCircuitEvents = CloseEvents & ErrorEvents & { 177 | /** 178 | * Streamless 179 | */ 180 | "RELAY_EXTENDED2": (cell: RelayCell.Streamless>) => void 181 | "RELAY_TRUNCATED": (cell: RelayCell.Streamless) => void 182 | 183 | /** 184 | * Streamful 185 | */ 186 | "RELAY_CONNECTED": (cell: RelayCell.Streamful) => void 187 | "RELAY_DATA": (cell: RelayCell.Streamful>) => void 188 | "RELAY_END": (cell: RelayCell.Streamful) => void 189 | } 190 | 191 | export class SecretCircuit { 192 | readonly #class = SecretCircuit 193 | 194 | readonly events = new SuperEventTarget() 195 | 196 | readonly targets = new Array() 197 | readonly streams = new Map() 198 | 199 | #streamId = 1 200 | 201 | #closed?: { reason?: unknown } 202 | 203 | #onClean: () => void 204 | 205 | constructor( 206 | readonly id: number, 207 | readonly tor: SecretTorClientDuplex 208 | ) { 209 | const onClose = this.#onTorClose.bind(this) 210 | const onError = this.#onTorError.bind(this) 211 | 212 | const onDestroyCell = this.#onDestroyCell.bind(this) 213 | 214 | const onRelayExtended2Cell = this.#onRelayExtended2Cell.bind(this) 215 | const onRelayTruncatedCell = this.#onRelayTruncatedCell.bind(this) 216 | 217 | const onRelayConnectedCell = this.#onRelayConnectedCell.bind(this) 218 | const onRelayDataCell = this.#onRelayDataCell.bind(this) 219 | const onRelayEndCell = this.#onRelayEndCell.bind(this) 220 | 221 | this.tor.events.on("close", onClose, { passive: true }) 222 | this.tor.events.on("error", onError, { passive: true }) 223 | 224 | this.tor.events.on("DESTROY", onDestroyCell, { passive: true }) 225 | 226 | this.tor.events.on("RELAY_EXTENDED2", onRelayExtended2Cell, { passive: true }) 227 | this.tor.events.on("RELAY_TRUNCATED", onRelayTruncatedCell, { passive: true }) 228 | 229 | this.tor.events.on("RELAY_CONNECTED", onRelayConnectedCell, { passive: true }) 230 | this.tor.events.on("RELAY_DATA", onRelayDataCell, { passive: true }) 231 | this.tor.events.on("RELAY_END", onRelayEndCell, { passive: true }) 232 | 233 | this.#onClean = () => { 234 | for (const stream of this.streams.values()) 235 | stream[Symbol.dispose]() 236 | 237 | for (const target of this.targets) 238 | target[Symbol.dispose]() 239 | 240 | this.tor.events.off("close", onClose) 241 | this.tor.events.off("error", onError) 242 | 243 | this.tor.events.off("DESTROY", onDestroyCell) 244 | 245 | this.tor.events.off("RELAY_EXTENDED2", onRelayExtended2Cell) 246 | this.tor.events.off("RELAY_TRUNCATED", onRelayTruncatedCell) 247 | 248 | this.tor.events.off("RELAY_CONNECTED", onRelayConnectedCell) 249 | this.tor.events.off("RELAY_DATA", onRelayDataCell) 250 | this.tor.events.off("RELAY_END", onRelayEndCell) 251 | 252 | this.tor.circuits.inner.delete(this.id) 253 | 254 | this.#onClean = () => { } 255 | } 256 | 257 | } 258 | 259 | [Symbol.dispose]() { 260 | this.close().catch(console.error) 261 | } 262 | 263 | async [Symbol.asyncDispose]() { 264 | await this.close() 265 | } 266 | 267 | get closed() { 268 | return this.#closed 269 | } 270 | 271 | #onCloseOrError(reason?: unknown) { 272 | if (this.#closed) 273 | return 274 | this.#closed = { reason } 275 | this.#onClean() 276 | } 277 | 278 | async close(reason: number = DestroyCell.reasons.NONE) { 279 | const error = new DestroyedError(reason) 280 | 281 | // TODO: send destroy cell 282 | 283 | this.#onCloseOrError(error) 284 | 285 | if (reason === DestroyCell.reasons.NONE) 286 | await this.events.emit("close", [error]) 287 | else 288 | await this.events.emit("error", [error]) 289 | } 290 | 291 | async #onTorClose() { 292 | Console.debug(`${this.#class.name}.onTorClose`) 293 | 294 | this.#onCloseOrError() 295 | 296 | await this.events.emit("close", [undefined]) 297 | } 298 | 299 | async #onTorError(reason?: unknown) { 300 | Console.debug(`${this.#class.name}.onReadError`, { reason }) 301 | 302 | await this.events.emit("error", [reason]) 303 | 304 | this.#onCloseOrError(reason) 305 | } 306 | 307 | async #onDestroyCell(cell: Cell.Circuitful) { 308 | if (cell.circuit !== this) 309 | return 310 | 311 | Console.debug(`${this.#class.name}.onDestroyCell`, cell) 312 | 313 | const error = new DestroyedError(cell.fragment.reason) 314 | 315 | this.#onCloseOrError(error) 316 | 317 | if (cell.fragment.reason === DestroyCell.reasons.NONE) 318 | await this.events.emit("close", [error]) 319 | else 320 | await this.events.emit("error", [error]) 321 | } 322 | 323 | async #onRelayExtended2Cell(cell: RelayCell.Streamless>) { 324 | if (cell.circuit !== this) 325 | return 326 | 327 | Console.debug(`${this.#class.name}.onRelayExtended2Cell`, cell) 328 | 329 | await this.events.emit("RELAY_EXTENDED2", cell) 330 | } 331 | 332 | async #onRelayTruncatedCell(cell: RelayCell.Streamless) { 333 | if (cell.circuit !== this) 334 | return 335 | 336 | Console.debug(`${this.#class.name}.onRelayTruncatedCell`, cell) 337 | 338 | const error = new DestroyedError(cell.fragment.reason) 339 | 340 | this.#onCloseOrError(error) 341 | 342 | if (cell.fragment.reason === RelayTruncateCell.reasons.NONE) 343 | await this.events.emit("close", [error]) 344 | else 345 | await this.events.emit("error", [error]) 346 | 347 | await this.events.emit("RELAY_TRUNCATED", cell) 348 | } 349 | 350 | async #onRelayConnectedCell(cell: RelayCell.Streamful) { 351 | if (cell.circuit !== this) 352 | return 353 | 354 | Console.debug(`${this.#class.name}.onRelayConnectedCell`, cell) 355 | 356 | await this.events.emit("RELAY_CONNECTED", cell) 357 | } 358 | 359 | async #onRelayDataCell(cell: RelayCell.Streamful>) { 360 | if (cell.circuit !== this) 361 | return 362 | 363 | Console.debug(`${this.#class.name}.onRelayDataCell`, cell) 364 | 365 | await this.events.emit("RELAY_DATA", cell) 366 | } 367 | 368 | async #onRelayEndCell(cell: RelayCell.Streamful) { 369 | if (cell.circuit !== this) 370 | return 371 | 372 | Console.debug(`${this.#class.name}.onRelayEndCell`, cell) 373 | 374 | this.streams.delete(cell.stream.id) 375 | 376 | await this.events.emit("RELAY_END", cell) 377 | } 378 | 379 | async extendOrThrow(microdesc: Consensus.Microdesc, signal = new AbortController().signal) { 380 | if (this.closed != null) 381 | throw this.closed.reason 382 | 383 | using relayid_rsa_x = Base64.get().getOrThrow().decodeUnpaddedOrThrow(microdesc.identity) 384 | const relayid_rsa = Bytes.castOrThrow(relayid_rsa_x.bytes.slice(), HASH_LEN) 385 | 386 | using ntor_key_x = Base64.get().getOrThrow().decodeUnpaddedOrThrow(microdesc.ntorOnionKey) 387 | const ntor_key = Bytes.castOrThrow(ntor_key_x.bytes.slice(), 32) 388 | 389 | const relayid_ed = Option.wrap(microdesc.idEd25519).mapSync(x => { 390 | using memory = Base64.get().getOrThrow().decodeUnpaddedOrThrow(x) 391 | return memory.bytes.slice() 392 | }).getOrNull() 393 | 394 | const links = new Array() 395 | 396 | links.push(new RelayExtend2LinkIPv4(microdesc.hostname, Number(microdesc.orport))) 397 | 398 | if (microdesc.ipv6 != null) 399 | links.push(RelayExtend2LinkIPv6.from(microdesc.ipv6)) 400 | 401 | links.push(new RelayExtend2LinkLegacyID(relayid_rsa)) 402 | 403 | if (relayid_ed != null) 404 | links.push(new RelayExtend2LinkModernID(relayid_ed)) 405 | 406 | using wasm_secret_x = await X25519.get().getOrThrow().PrivateKey.randomOrThrow() 407 | using wasm_public_x = wasm_secret_x.getPublicKeyOrThrow() 408 | 409 | using public_x_memory = await wasm_public_x.exportOrThrow() 410 | 411 | const public_x = Bytes.castOrThrow(public_x_memory.bytes.slice(), 32) 412 | const public_b = ntor_key 413 | 414 | const ntor_request = new Ntor.NtorRequest(public_x, relayid_rsa, public_b) 415 | const relay_extend2 = new RelayExtend2Cell(RelayExtend2Cell.types.NTOR, links, ntor_request) 416 | this.tor.output.enqueue(RelayEarlyCell.Streamless.from(this, undefined, relay_extend2).cellOrThrow()) 417 | 418 | const msg_extended2 = await Plume.waitWithCloseAndErrorOrThrow(this.events, "RELAY_EXTENDED2", (future: Future>>, e) => { 419 | future.resolve(e) 420 | }, signal) 421 | 422 | const response = msg_extended2.fragment.fragment.readIntoOrThrow(Ntor.NtorResponse) 423 | 424 | const { public_y } = response 425 | 426 | using wasm_public_y = await X25519.get().getOrThrow().PublicKey.importOrThrow(public_y) 427 | using wasm_public_b = await X25519.get().getOrThrow().PublicKey.importOrThrow(public_b) 428 | 429 | using wasm_shared_xy = await wasm_secret_x.computeOrThrow(wasm_public_y) 430 | using wasm_shared_xb = await wasm_secret_x.computeOrThrow(wasm_public_b) 431 | 432 | using shared_xy_memory = wasm_shared_xy.exportOrThrow() 433 | using shared_xb_memory = wasm_shared_xb.exportOrThrow() 434 | 435 | const shared_xy = Bytes.castOrThrow(shared_xy_memory.bytes.slice(), 32) 436 | const shared_xb = Bytes.castOrThrow(shared_xb_memory.bytes.slice(), 32) 437 | 438 | const result = await NtorResult.finalizeOrThrow(shared_xy, shared_xb, relayid_rsa, public_b, public_x, public_y) 439 | 440 | if (!Bytes.equals(response.auth, result.auth)) 441 | throw new InvalidNtorAuthError() 442 | 443 | const forward_digest = Sha1.get().getOrThrow().Hasher.createOrThrow() 444 | const backward_digest = Sha1.get().getOrThrow().Hasher.createOrThrow() 445 | 446 | forward_digest.updateOrThrow(result.forwardDigest) 447 | backward_digest.updateOrThrow(result.backwardDigest) 448 | 449 | using forwardKeyMemory = new AesWasm.Memory(result.forwardKey) 450 | using forwardIvMemory = new AesWasm.Memory(new Uint8Array(16)) 451 | 452 | using backwardKeyMemory = new AesWasm.Memory(result.backwardKey) 453 | using backwardIvMemory = new AesWasm.Memory(new Uint8Array(16)) 454 | 455 | const forwardKey = new Aes128Ctr128BEKey(forwardKeyMemory, forwardIvMemory) 456 | const backwardKey = new Aes128Ctr128BEKey(backwardKeyMemory, backwardIvMemory) 457 | 458 | const target = new Target(relayid_rsa, this, forward_digest, backward_digest, forwardKey, backwardKey) 459 | 460 | this.targets.push(target) 461 | } 462 | 463 | async truncateOrThrow(reason: number = RelayTruncateCell.reasons.NONE, signal = new AbortController().signal) { 464 | if (this.closed != null) 465 | throw this.closed.reason 466 | 467 | const relay_truncate = new RelayTruncateCell(reason) 468 | const relay_truncate_cell = RelayCell.Streamless.from(this, undefined, relay_truncate) 469 | this.tor.output.enqueue(relay_truncate_cell.cellOrThrow()) 470 | 471 | await Plume.waitWithCloseAndErrorOrThrow(this.events, "RELAY_TRUNCATED", (future: Future>, e) => { 472 | future.resolve(e) 473 | }, signal) 474 | } 475 | 476 | async openDirOrThrow(params: CircuitOpenParams = {}, signal = new AbortController().signal) { 477 | if (this.closed != null) 478 | throw this.closed.reason 479 | 480 | const stream = new SecretTorStreamDuplex("directory", this.#streamId++, this) 481 | 482 | this.streams.set(stream.id, stream) 483 | 484 | const begin = new RelayBeginDirCell() 485 | const begin_cell = RelayCell.Streamful.from(this, stream, begin) 486 | this.tor.output.enqueue(begin_cell.cellOrThrow()) 487 | 488 | if (!params.wait) 489 | return new TorStreamDuplex(stream) 490 | 491 | await Plume.waitWithCloseAndErrorOrThrow(stream.events, "connected", (future: Future) => { 492 | future.resolve() 493 | }, signal) 494 | 495 | return new TorStreamDuplex(stream) 496 | } 497 | 498 | async openOrThrow(hostname: string, port: number, params: CircuitOpenParams = {}, signal = new AbortController().signal) { 499 | if (this.closed != null) 500 | throw this.closed.reason 501 | 502 | const { ipv6 = "preferred" } = params 503 | 504 | const stream = new SecretTorStreamDuplex("external", this.#streamId++, this) 505 | 506 | this.streams.set(stream.id, stream) 507 | 508 | const flags = new Bitset(0, 32) 509 | .setLE(RelayBeginCell.flags.IPV6_OK, IPv6[ipv6] !== IPv6.never) 510 | .setLE(RelayBeginCell.flags.IPV4_NOT_OK, IPv6[ipv6] === IPv6.always) 511 | .setLE(RelayBeginCell.flags.IPV6_PREFER, IPv6[ipv6] > IPv6.avoided) 512 | .unsign() 513 | .value 514 | 515 | const begin = RelayBeginCell.create(`${hostname}:${port}`, flags) 516 | const begin_cell = RelayCell.Streamful.from(this, stream, begin) 517 | this.tor.output.enqueue(begin_cell.cellOrThrow()) 518 | 519 | if (!params.wait) 520 | return new TorStreamDuplex(stream) 521 | 522 | await Plume.waitWithCloseAndErrorOrThrow(stream.events, "connected", (future: Future) => { 523 | future.resolve() 524 | }, signal) 525 | 526 | return new TorStreamDuplex(stream) 527 | } 528 | 529 | } --------------------------------------------------------------------------------