more refactor

This commit is contained in:
LiBr
2026-04-10 20:34:14 +08:00
parent 8d31144741
commit 72123cc922
47 changed files with 868 additions and 365 deletions

3
.gitmodules vendored
View File

@@ -4,3 +4,6 @@
[submodule "wasm/libcurl-wasm"] [submodule "wasm/libcurl-wasm"]
path = wasm/libcurl-wasm path = wasm/libcurl-wasm
url = https://github.com/lbr77/libcurl.js url = https://github.com/lbr77/libcurl.js
[submodule "wasm/openssl-wasm"]
path = wasm/openssl-wasm
url = https://github.com/jedisct1/openssl-wasm

View File

@@ -1,30 +1,13 @@
# sideload.js # Sideload.js
This repository is organized as a Bun workspace with four top-level areas: A pure frontend signing infrastructure.
- `frontend/`: browser signing UI
- `backend/`: Cloudflare Workers Wisp proxy demo
- `dependencies/webmuxd/`: the publishable `webmuxd` package consumed by the frontend
- `wasm/openssl/`: the Rust/WASM OpenSSL bridge whose build artifacts are copied into `dependencies/webmuxd/lib/openssl-wasm`
## Install ## Install
```bash
bun install
```
## Validate <!-- TODO: deploy to cf flow -->
Deploy to Cloudflare worker.
```bash ## Technology
bun run build
bun run lint
bun run test
cd frontend && bun run build
cd backend && bun run check
```
## Workspace Notes 1. when signing
- `frontend/src/main.ts` must consume `webmuxd` package exports instead of duplicating usbmux/lockdown/AFC/InstProxy logic.
- Changes to device communication, pairing, or TLS behavior belong in `dependencies/webmuxd/src/` first.
- `wasm/openssl/pkg` is treated as a build artifact source for the package copy step.

29
README.zh.md Normal file
View File

@@ -0,0 +1,29 @@
# Sideload.js
纯前端ipa签名工具
## 安装/部署
<!-- TODO: deploy to cf flow -->
Deploy to Cloudflare worker.
本地部署With Dockerfile
## 功能
1. apple客户端模拟(by unicorn over wasm)
2. Team证书管理获取删除注册
3. zsign wasmipa解包签名
4. usbmuxd over webusb + lockdownd SSL service握手
## 感谢
1. libimobiledevice
2. https://github.com/hack-different/webmuxd
3. zsign
4. openssl-wasm

View File

@@ -41,10 +41,11 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@lbr77/anisette-js": "0.1.3", "@lbr77/anisette-js": "0.1.3",
"@lbr77/zsign-wasm-resigner-wrapper": "^0.1.5", "@lbr77/zsign-wasm-resigner-wrapper": "workspace:*",
"altsign.js": "^0.1.2", "altsign.js": "^0.1.2",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"libcurl.js": "workspace:*",
"node-forge": "^1.3.3", "node-forge": "^1.3.3",
"webmuxd": "workspace:*", "webmuxd": "workspace:*",
}, },
@@ -55,6 +56,25 @@
"vite": "^7.3.1", "vite": "^7.3.1",
}, },
}, },
"wasm/libcurl-wasm": {
"name": "libcurl.js",
"version": "0.7.4",
},
"wasm/openssl": {
"name": "@lbr77/openssl-wasm",
"version": "0.1.0",
},
"wasm/zsign-wasm": {
"name": "@lbr77/zsign-wasm-resigner-wrapper",
"version": "0.1.0",
"dependencies": {
"jszip": "^3.10.1",
},
"devDependencies": {
"@types/node": "^25.5.2",
"typescript": "~5.9.3",
},
},
}, },
"packages": { "packages": {
"@babel/code-frame": ["@babel/code-frame@7.16.0", "", { "dependencies": { "@babel/highlight": "^7.16.0" } }, "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA=="], "@babel/code-frame": ["@babel/code-frame@7.16.0", "", { "dependencies": { "@babel/highlight": "^7.16.0" } }, "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA=="],
@@ -295,7 +315,9 @@
"@lbr77/anisette-js": ["@lbr77/anisette-js@0.1.3", "", { "dependencies": { "@types/node": "^25.3.2" } }, "sha512-QzZQB/XzMV68ykaj6yU2aPGmDrhE0wJEmSfMJu0VQ5zhfhrv7G2zrHJGUum226jZ//lYJeMSrzmDaI/NgFVIgg=="], "@lbr77/anisette-js": ["@lbr77/anisette-js@0.1.3", "", { "dependencies": { "@types/node": "^25.3.2" } }, "sha512-QzZQB/XzMV68ykaj6yU2aPGmDrhE0wJEmSfMJu0VQ5zhfhrv7G2zrHJGUum226jZ//lYJeMSrzmDaI/NgFVIgg=="],
"@lbr77/zsign-wasm-resigner-wrapper": ["@lbr77/zsign-wasm-resigner-wrapper@0.1.5", "", { "dependencies": { "@types/node": "^25.3.2", "jszip": "^3.10.1" } }, "sha512-oVmRgkrSccDXpS9rlBKc0LWQzqcEFiTQod5ZaKIcyIp3rtwROgc1Tqwrr/7oLVQNdq8jI79lbnCoa1J34AaUqw=="], "@lbr77/openssl-wasm": ["@lbr77/openssl-wasm@workspace:wasm/openssl"],
"@lbr77/zsign-wasm-resigner-wrapper": ["@lbr77/zsign-wasm-resigner-wrapper@workspace:wasm/zsign-wasm"],
"@mercuryworkshop/wisp-js": ["@mercuryworkshop/wisp-js@0.4.1", "", { "dependencies": { "bufferutil": "^4.0.9", "commander": "^14.0.2", "ipaddr.js": "^2.3.0", "ws": "^8.18.3" }, "bin": { "wisp-js-server": "src/bin/server_cli.mjs" } }, "sha512-104LwiXiuhti/e32gmv0Da0u0yuLFDHX8JawCzleTPWJ5t5qTX4EYi4E7/ucbjBPN9wwVPWHE5g5yGqzl/NzQA=="], "@mercuryworkshop/wisp-js": ["@mercuryworkshop/wisp-js@0.4.1", "", { "dependencies": { "bufferutil": "^4.0.9", "commander": "^14.0.2", "ipaddr.js": "^2.3.0", "ws": "^8.18.3" }, "bin": { "wisp-js-server": "src/bin/server_cli.mjs" } }, "sha512-104LwiXiuhti/e32gmv0Da0u0yuLFDHX8JawCzleTPWJ5t5qTX4EYi4E7/ucbjBPN9wwVPWHE5g5yGqzl/NzQA=="],
@@ -923,6 +945,8 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"libcurl.js": ["libcurl.js@workspace:wasm/libcurl-wasm"],
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
@@ -1411,6 +1435,8 @@
"acorn-globals/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], "acorn-globals/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="],
"altsign.js/@lbr77/zsign-wasm-resigner-wrapper": ["@lbr77/zsign-wasm-resigner-wrapper@0.1.5", "", { "dependencies": { "@types/node": "^25.3.2", "jszip": "^3.10.1" } }, "sha512-oVmRgkrSccDXpS9rlBKc0LWQzqcEFiTQod5ZaKIcyIp3rtwROgc1Tqwrr/7oLVQNdq8jI79lbnCoa1J34AaUqw=="],
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"anymatch/picomatch": ["picomatch@2.3.0", "", {}, "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw=="], "anymatch/picomatch": ["picomatch@2.3.0", "", {}, "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw=="],

View File

@@ -25,7 +25,8 @@
"LICENSE" "LICENSE"
], ],
"scripts": { "scripts": {
"build": "tsc && bun run copy:openssl-wasm", "build": "bun run build:openssl-wasm && tsc && bun run copy:openssl-wasm",
"build:openssl-wasm": "cd ../../wasm/openssl && bun run build",
"copy:openssl-wasm": "bun scripts/copy-openssl-wasm.mjs", "copy:openssl-wasm": "bun scripts/copy-openssl-wasm.mjs",
"prepare": "bun run build", "prepare": "bun run build",
"prepublishOnly": "bun run test && bun run lint", "prepublishOnly": "bun run test && bun run lint",

View File

@@ -1,22 +1,19 @@
import { mkdir, copyFile } from "node:fs/promises" import { cp, mkdir, rm } from "node:fs/promises"
import { dirname, resolve } from "node:path" import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url" import { fileURLToPath } from "node:url"
const scriptDir = dirname(fileURLToPath(import.meta.url)) const scriptDir = dirname(fileURLToPath(import.meta.url))
const packageDir = resolve(scriptDir, "..") const packageDir = resolve(scriptDir, "..")
const workspaceRootDir = resolve(packageDir, "../..") const workspaceRootDir = resolve(packageDir, "../..")
const sourceDir = resolve(workspaceRootDir, "wasm/openssl/pkg") const sourceDir = resolve(workspaceRootDir, "wasm/openssl")
const targetDir = resolve(packageDir, "lib/openssl-wasm") const targetDir = resolve(packageDir, "lib/openssl-wasm")
const directoriesToCopy = ["dist", "binary"]
const filesToCopy = [ await rm(targetDir, { recursive: true, force: true })
"openssl_wasm.js",
"openssl_wasm.d.ts",
"openssl_wasm_bg.wasm",
"openssl_wasm_bg.wasm.d.ts",
]
await mkdir(targetDir, { recursive: true }) await mkdir(targetDir, { recursive: true })
for (const fileName of filesToCopy) { for (const directoryName of directoriesToCopy) {
await copyFile(resolve(sourceDir, fileName), resolve(targetDir, fileName)) await cp(resolve(sourceDir, directoryName), resolve(targetDir, directoryName), {
recursive: true,
})
} }

19
dependencies/webmuxd/src/browser.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
export { CONSOLE_LOGGER, NULL_LOGGER, type Logger } from "./logger"
export { WebUsbTransport, type WebUsbTransportOptions } from "./core/webusb-transport"
export {
DirectUsbMuxClient,
LOCKDOWN_PORT,
installIpaViaInstProxy,
sanitizeIpaFileName,
createHostId,
createSystemBuid,
encodeStoredPairRecord,
decodeStoredPairRecord,
type PairRecord,
type StoredPairRecordPayload,
type WebUsbTransportInstance,
} from "./core/imobiledevice-client"
export {
createOpenSslWasmTlsFactory,
generatePairRecordWithOpenSslWasm,
} from "./core/openssl-wasm-browser"

View File

@@ -0,0 +1,112 @@
import type { TlsConnection, TlsConnectionFactory } from "./imobiledevice-client"
import type {
OpenSslWasmConnectionRequest,
OpenSslWasmPairRecordRequest,
} from "./openssl-wasm"
interface OpenSslWasmBrowserModule {
default(input?: unknown): Promise<unknown>
OpensslClient: new (
serverName: string,
caCertificatePem: string,
certificatePem: string,
privateKeyPem: string,
) => TlsConnection
libimobiledevice_generate_pair_record(
devicePublicKey: Uint8Array,
hostId: string,
systemBuid: string,
): string
}
const OPENSSL_WASM_MODULE_URL = new URL(
"../openssl-wasm/dist/index.mjs",
import.meta.url,
)
let opensslWasmModule: OpenSslWasmBrowserModule | null = null
let opensslWasmModulePromise: Promise<OpenSslWasmBrowserModule> | null = null
let opensslWasmInitPromise: Promise<void> | null = null
const toOpenSslWasmModule = (moduleValue: unknown): OpenSslWasmBrowserModule => {
if (!moduleValue || typeof moduleValue !== "object") {
throw new Error("OpenSSL wasm module did not return an object")
}
const candidate = moduleValue as Record<string, unknown>
if (typeof candidate.default !== "function") {
throw new Error("OpenSSL wasm module is missing its default initializer")
}
if (typeof candidate.OpensslClient !== "function") {
throw new Error("OpenSSL wasm module is missing OpensslClient")
}
if (typeof candidate.libimobiledevice_generate_pair_record !== "function") {
throw new Error("OpenSSL wasm module is missing pair record generation")
}
return candidate as unknown as OpenSslWasmBrowserModule
}
const loadOpenSslWasmModule = async (): Promise<OpenSslWasmBrowserModule> => {
if (!opensslWasmModulePromise) {
opensslWasmModulePromise = import(
/* @vite-ignore */
OPENSSL_WASM_MODULE_URL.href
).then((moduleValue) => {
const loadedModule = toOpenSslWasmModule(moduleValue)
opensslWasmModule = loadedModule
return loadedModule
})
}
return await opensslWasmModulePromise
}
const requireOpenSslWasmModule = (): OpenSslWasmBrowserModule => {
if (!opensslWasmModule) {
throw new Error("OpenSSL wasm is not ready. Call ensureOpenSslWasmReady() first.")
}
return opensslWasmModule
}
export const ensureOpenSslWasmReady = async (): Promise<void> => {
if (!opensslWasmInitPromise) {
opensslWasmInitPromise = loadOpenSslWasmModule().then(async (moduleValue) => {
await moduleValue.default()
})
}
await opensslWasmInitPromise
}
export const createOpenSslWasmConnection = (
request: OpenSslWasmConnectionRequest,
): TlsConnection => {
const moduleValue = requireOpenSslWasmModule()
return new moduleValue.OpensslClient(
request.serverName,
request.caCertificatePem,
request.certificatePem,
request.privateKeyPem,
)
}
export const createOpenSslWasmTlsFactory = (): TlsConnectionFactory => {
return {
ensureReady: ensureOpenSslWasmReady,
createConnection: createOpenSslWasmConnection,
}
}
export const generatePairRecordWithOpenSslWasm = async (
request: OpenSslWasmPairRecordRequest,
): Promise<string> => {
await ensureOpenSslWasmReady()
const moduleValue = requireOpenSslWasmModule()
return moduleValue.libimobiledevice_generate_pair_record(
new Uint8Array(request.devicePublicKey),
request.hostId,
request.systemBuid,
)
}

View File

@@ -28,7 +28,7 @@ interface OpenSslWasmModule {
): string ): string
} }
const OPENSSL_WASM_MODULE_SPECIFIER = "../openssl-wasm/openssl_wasm.js" const OPENSSL_WASM_MODULE_SPECIFIER = "../openssl-wasm/dist/index.mjs"
/** /**
* Keep native `import()` intact in the CommonJS build so bundlers can defer the * Keep native `import()` intact in the CommonJS build so bundlers can defer the

View File

@@ -1,4 +1,4 @@
import { CONSOLE_LOGGER, Logger, NULL_LOGGER } from "../webmuxd" import { CONSOLE_LOGGER, Logger, NULL_LOGGER } from "../logger"
import { DataHandler, DisconnectHandler, UsbMuxTransport } from "./transport" import { DataHandler, DisconnectHandler, UsbMuxTransport } from "./transport"
const USBMUX_CLASS = 255 const USBMUX_CLASS = 255

38
dependencies/webmuxd/src/logger.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
type LogLevel = "debug" | "info" | "warn" | "error"
export interface Logger {
log(level: LogLevel, message: string): void
}
export const NULL_LOGGER = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
log: (level: LogLevel, message: string): void => {
return
},
}
export const CONSOLE_LOGGER = {
log: (level: LogLevel, message: string): void => {
switch (level) {
case "info":
// eslint-disable-next-line no-console
console.log(message)
break
case "warn":
// eslint-disable-next-line no-console
console.warn(message)
break
case "error":
// eslint-disable-next-line no-console
console.error(message)
break
case "debug":
// eslint-disable-next-line no-console
console.debug(message)
break
default:
// eslint-disable-next-line no-console
console.error(`Unknown log level ${level}: ${message}`)
}
},
}

View File

@@ -0,0 +1,2 @@
export { default } from "../../../../../wasm/openssl/dist/index.mjs"
export * from "../../../../../wasm/openssl/dist/index.mjs"

View File

@@ -1,47 +1,11 @@
import { NULL_LOGGER, type Logger } from "./logger"
const USBMUX_USB_FILTER = [{ vendorId: 0x5ac, productId: 0x12a8 }]; const USBMUX_USB_FILTER = [{ vendorId: 0x5ac, productId: 0x12a8 }];
const USBMUX_CLASS = 255; const USBMUX_CLASS = 255;
const USBMUX_SUBCLASS = 254; const USBMUX_SUBCLASS = 254;
const USBMUX_PROTOCOL = 2; const USBMUX_PROTOCOL = 2;
type LogLevel = "debug" | "info" | "warn" | "error" export { CONSOLE_LOGGER, NULL_LOGGER, type Logger } from "./logger"
export interface Logger {
log(level: LogLevel, message: string): void
}
export const NULL_LOGGER = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
log: (level: LogLevel, message: string): void => {
return
}
}
export const CONSOLE_LOGGER = {
log: (level: LogLevel, message: string): void => {
switch (level) {
case "info":
// eslint-disable-next-line no-console
console.log(message)
break
case "warn":
// eslint-disable-next-line no-console
console.warn(message)
break
case "error":
// eslint-disable-next-line no-console
console.error(message)
break
case "debug":
// eslint-disable-next-line no-console
console.debug(message)
break
default:
// eslint-disable-next-line no-console
console.error(`Unknown log level ${level}: ${message}`)
}
}
}
export default class MobileDevice { export default class MobileDevice {
static logger: Logger = NULL_LOGGER static logger: Logger = NULL_LOGGER

View File

@@ -9,5 +9,11 @@
"types": ["node", "w3c-web-usb"] "types": ["node", "w3c-web-usb"]
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"] "exclude": [
"node_modules",
"**/__tests__/*",
"src/browser.ts",
"src/core/openssl-wasm-browser.ts",
"src/openssl-wasm/**/*"
]
} }

View File

@@ -11,10 +11,11 @@
}, },
"dependencies": { "dependencies": {
"@lbr77/anisette-js": "0.1.3", "@lbr77/anisette-js": "0.1.3",
"@lbr77/zsign-wasm-resigner-wrapper": "^0.1.5", "@lbr77/zsign-wasm-resigner-wrapper": "workspace:*",
"altsign.js": "^0.1.2", "altsign.js": "^0.1.2",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"libcurl.js": "workspace:*",
"node-forge": "^1.3.3", "node-forge": "^1.3.3",
"webmuxd": "workspace:*" "webmuxd": "workspace:*"
}, },

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,9 +1,11 @@
import type { HttpClient } from "@lbr77/anisette-js" import type { HttpClient } from "@lbr77/anisette-js"
import { initLibcurl, libcurl } from "./anisette-libcurl-init" import { initLibcurl } from "./anisette-libcurl-init"
import { requireLibcurl } from "./wasm/libcurl"
export class LibcurlHttpClient implements HttpClient { export class LibcurlHttpClient implements HttpClient {
async get(url: string, headers: Record<string, string>): Promise<Uint8Array> { async get(url: string, headers: Record<string, string>): Promise<Uint8Array> {
await initLibcurl() await initLibcurl()
const libcurl = requireLibcurl()
const response = await libcurl.fetch(url, { const response = await libcurl.fetch(url, {
method: "GET", method: "GET",
@@ -21,6 +23,7 @@ export class LibcurlHttpClient implements HttpClient {
async post(url: string, body: string, headers: Record<string, string>): Promise<Uint8Array> { async post(url: string, body: string, headers: Record<string, string>): Promise<Uint8Array> {
await initLibcurl() await initLibcurl()
const libcurl = requireLibcurl()
const response = await libcurl.fetch(url, { const response = await libcurl.fetch(url, {
method: "POST", method: "POST",

View File

@@ -1,5 +1,4 @@
// @ts-ignore import { loadLibcurl, libcurl } from "./wasm/libcurl"
import { libcurl } from "../public/anisette/libcurl_full.mjs"
let initialized = false let initialized = false
let initPromise: Promise<void> | null = null let initPromise: Promise<void> | null = null
@@ -13,10 +12,11 @@ export async function initLibcurl(): Promise<void> {
} }
initPromise = (async () => { initPromise = (async () => {
const loadedLibcurl = await loadLibcurl()
const wsProto = location.protocol === "https:" ? "wss:" : "ws:" const wsProto = location.protocol === "https:" ? "wss:" : "ws:"
const wsUrl = `${wsProto}//${location.host}/wisp/` const wsUrl = `${wsProto}//${location.host}/wisp/`
libcurl.set_websocket(wsUrl) loadedLibcurl.set_websocket(wsUrl)
await libcurl.load_wasm() await loadedLibcurl.load_wasm()
initialized = true initialized = true
})() })()

View File

@@ -1,13 +1,14 @@
import { strFromU8, unzipSync } from "fflate" import { strFromU8, unzipSync } from "fflate"
import { import type {
type AnisetteData, AnisetteData,
type AppleAPI, AppleAPI,
type AppID, AppID,
type Certificate, Certificate,
type Device, Device,
type Team, Team,
} from "altsign.js" } from "altsign.js"
import { initLibcurl, libcurl } from "./anisette-libcurl-init" import { initLibcurl } from "./anisette-libcurl-init"
import { requireLibcurl } from "./wasm/libcurl"
const SIGNING_IDENTITY_STORAGE_KEY = "webmuxd:signing-identities" const SIGNING_IDENTITY_STORAGE_KEY = "webmuxd:signing-identities"
const PRIMARY_APP_INFO_PLIST_RE = /^Payload\/[^/]+\.app\/Info\.plist$/ const PRIMARY_APP_INFO_PLIST_RE = /^Payload\/[^/]+\.app\/Info\.plist$/
@@ -129,6 +130,7 @@ async function getAppleApi(): Promise<AppleAPI> {
} }
const { AppleAPI, Fetch } = await loadAltsignModule() const { AppleAPI, Fetch } = await loadAltsignModule()
const appleFetch = new Fetch(initLibcurl, async (url, options) => { const appleFetch = new Fetch(initLibcurl, async (url, options) => {
const libcurl = requireLibcurl()
const response = await libcurl.fetch(url, { const response = await libcurl.fetch(url, {
method: options.method, method: options.method,
headers: options.headers, headers: options.headers,

View File

@@ -1,9 +0,0 @@
export function setupCounter(element: HTMLButtonElement) {
let counter = 0
const setCounter = (count: number) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

View File

@@ -1,121 +1,35 @@
import "./style.css" import "./style.css"
import * as webmuxdModule from "webmuxd"
import { import {
initAnisette, DirectUsbMuxClient,
getAnisetteData, LOCKDOWN_PORT,
provisionAnisette, WebUsbTransport,
type AnisetteData, createHostId,
} from "./anisette-service" createOpenSslWasmTlsFactory,
import { createSystemBuid,
loginAppleDeveloperAccount, decodeStoredPairRecord,
refreshAppleDeveloperContext, encodeStoredPairRecord,
signIpaWithAppleContext, generatePairRecordWithOpenSslWasm,
type AppleDeveloperContext, installIpaViaInstProxy,
} from "./apple-signing" sanitizeIpaFileName,
type PairRecord,
type StoredPairRecordPayload,
} from "webmuxd"
import type { AnisetteData } from "./anisette-service"
import type { AppleDeveloperContext } from "./apple-signing"
interface WebUsbTransportInstance { type WasmPairRecordPayload = Pick<
readonly isOpen: boolean PairRecord,
open(): Promise<void> | "hostId"
close(): Promise<void> | "systemBuid"
send(data: ArrayBuffer): Promise<void> | "hostCertificatePem"
setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void | "hostPrivateKeyPem"
setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void | "rootCertificatePem"
} | "rootPrivateKeyPem"
| "deviceCertificatePem"
>
interface WebUsbTransportCtor { type AnisetteServiceModule = typeof import("./anisette-service")
supported(): boolean type AppleSigningModule = typeof import("./apple-signing")
requestAppleDevice(): Promise<WebUsbTransportInstance>
}
interface PairRecord {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: Uint8Array
escrowBag?: Uint8Array
}
interface StoredPairRecordPayload {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: string
escrowBag: string | null
}
interface StartSessionResult {
sessionId: string
enableSessionSsl: boolean
}
interface DirectUsbMuxClient {
readonly isHandshakeComplete: boolean
readonly isLockdownConnected: boolean
readonly isSessionStarted: boolean
readonly isSessionSslEnabled: boolean
readonly isTlsActive: boolean
readonly isPaired: boolean
loadPairRecord(record: PairRecord | null): void
openAndHandshake(): Promise<void>
connectLockdown(port?: number): Promise<void>
getOrFetchDeviceUdid(): Promise<string>
getOrFetchDeviceName(): Promise<string | null>
pairDevice(hostId: string, systemBuid: string): Promise<PairRecord>
startSession(hostId: string, systemBuid: string): Promise<StartSessionResult>
close(): Promise<void>
}
interface DirectUsbMuxClientCtor {
new (
transport: WebUsbTransportInstance,
options?: {
log?: (message: string) => void
onStateChange?: () => void
lockdownLabel?: string
tlsFactory?: {
ensureReady?: () => Promise<void>
createConnection(request: {
serverName: string
caCertificatePem: string
certificatePem: string
privateKeyPem: string
}): {
is_handshaking(): boolean
write_plaintext(data: Uint8Array): void
feed_tls(data: Uint8Array): void
take_tls_out(): Uint8Array
take_plain_out(): Uint8Array
free(): void
}
}
pairRecordFactory?: {
createPairRecord(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}): Promise<PairRecord>
}
},
): DirectUsbMuxClient
}
interface WasmPairRecordPayload {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
}
interface PairedDeviceInfo { interface PairedDeviceInfo {
udid: string udid: string
@@ -162,8 +76,6 @@ interface StoredAccountSessionPayload {
type AppPage = "login" | "sign" type AppPage = "login" | "sign"
const LOCKDOWN_PORT = 62078
const HOST_ID_STORAGE_KEY = "webmuxd:host-id" const HOST_ID_STORAGE_KEY = "webmuxd:host-id"
const SYSTEM_BUID_STORAGE_KEY = "webmuxd:system-buid" const SYSTEM_BUID_STORAGE_KEY = "webmuxd:system-buid"
const PAIR_RECORDS_STORAGE_KEY = "webmuxd:pair-records-by-udid" const PAIR_RECORDS_STORAGE_KEY = "webmuxd:pair-records-by-udid"
@@ -177,67 +89,24 @@ const SELECTED_DEVICE_UDID_STORAGE_KEY = "webmuxd:selected-device-udid"
const LOGIN_PAGE_HASH = "#/login" const LOGIN_PAGE_HASH = "#/login"
const SIGN_PAGE_HASH = "#/sign" const SIGN_PAGE_HASH = "#/sign"
const webmuxdModuleValue = webmuxdModule as unknown as Record<string, unknown> let anisetteServicePromise: Promise<AnisetteServiceModule> | null = null
let appleSigningModulePromise: Promise<AppleSigningModule> | null = null
const WebUsbTransport = resolveWebmuxdExport<WebUsbTransportCtor>( const loadAnisetteService = async (): Promise<AnisetteServiceModule> => {
webmuxdModuleValue, if (!anisetteServicePromise) {
"WebUsbTransport", anisetteServicePromise = import("./anisette-service")
)
const WebmuxdDirectUsbMuxClient = resolveWebmuxdExport<DirectUsbMuxClientCtor>(
webmuxdModuleValue,
"DirectUsbMuxClient",
)
const webmuxdInstallIpaViaInstProxy = resolveWebmuxdExport<
(
client: DirectUsbMuxClient,
ipaData: Uint8Array,
fileName: string,
onLog?: (message: string) => void,
) => Promise<void>
>(webmuxdModuleValue, "installIpaViaInstProxy")
const webmuxdSanitizeIpaFileName = resolveWebmuxdExport<(fileName: string) => string>(
webmuxdModuleValue,
"sanitizeIpaFileName",
)
const webmuxdCreateHostId = resolveWebmuxdExport<() => string>(
webmuxdModuleValue,
"createHostId",
)
const webmuxdCreateSystemBuid = resolveWebmuxdExport<() => string>(
webmuxdModuleValue,
"createSystemBuid",
)
const webmuxdEncodeStoredPairRecord = resolveWebmuxdExport<
(record: PairRecord) => StoredPairRecordPayload
>(webmuxdModuleValue, "encodeStoredPairRecord")
const webmuxdDecodeStoredPairRecord = resolveWebmuxdExport<
(payload: StoredPairRecordPayload) => PairRecord | null
>(webmuxdModuleValue, "decodeStoredPairRecord")
const webmuxdCreateOpenSslWasmTlsFactory = resolveWebmuxdExport<
() => {
ensureReady?(): Promise<void>
createConnection(request: {
serverName: string
caCertificatePem: string
certificatePem: string
privateKeyPem: string
}): {
is_handshaking(): boolean
write_plaintext(data: Uint8Array): void
feed_tls(data: Uint8Array): void
take_tls_out(): Uint8Array
take_plain_out(): Uint8Array
free(): void
}
} }
>(webmuxdModuleValue, "createOpenSslWasmTlsFactory")
const webmuxdGeneratePairRecordWithOpenSslWasm = resolveWebmuxdExport< return await anisetteServicePromise
(request: { }
devicePublicKey: Uint8Array
hostId: string const loadAppleSigningModule = async (): Promise<AppleSigningModule> => {
systemBuid: string if (!appleSigningModulePromise) {
}) => Promise<string> appleSigningModulePromise = import("./apple-signing")
>(webmuxdModuleValue, "generatePairRecordWithOpenSslWasm") }
return await appleSigningModulePromise
}
const app = document.querySelector<HTMLDivElement>("#app") const app = document.querySelector<HTMLDivElement>("#app")
if (!app) { if (!app) {
@@ -586,11 +455,11 @@ const ensureClientSelected = async (): Promise<DirectUsbMuxClient> => {
} }
const transport = await WebUsbTransport.requestAppleDevice() const transport = await WebUsbTransport.requestAppleDevice()
directClient = new WebmuxdDirectUsbMuxClient(transport, { directClient = new DirectUsbMuxClient(transport, {
log: addLog, log: addLog,
onStateChange: refreshUi, onStateChange: refreshUi,
lockdownLabel: "webmuxd.frontend", lockdownLabel: "webmuxd.frontend",
tlsFactory: webmuxdCreateOpenSslWasmTlsFactory(), tlsFactory: createOpenSslWasmTlsFactory(),
pairRecordFactory: { pairRecordFactory: {
createPairRecord: async (request) => { createPairRecord: async (request) => {
return await createPairRecord(request.devicePublicKey, request.hostId, request.systemBuid) return await createPairRecord(request.devicePublicKey, request.hostId, request.systemBuid)
@@ -681,19 +550,20 @@ const ensureAnisetteData = async (): Promise<AnisetteData> => {
return anisetteData return anisetteData
} }
const anisette = await initAnisette() const anisetteService = await loadAnisetteService()
const anisette = await anisetteService.initAnisette()
const alreadyProvisioned = anisette.isProvisioned const alreadyProvisioned = anisette.isProvisioned
anisetteProvisioned = alreadyProvisioned anisetteProvisioned = alreadyProvisioned
if (alreadyProvisioned) { if (alreadyProvisioned) {
addLog("login: anisette already provisioned") addLog("login: anisette already provisioned")
} else { } else {
addLog("login: preparing anisette environment...") addLog("login: preparing anisette environment...")
await provisionAnisette() await anisetteService.provisionAnisette()
anisetteProvisioned = true anisetteProvisioned = true
addLog("login: anisette provisioned") addLog("login: anisette provisioned")
} }
anisetteData = await getAnisetteData() anisetteData = await anisetteService.getAnisetteData()
addLog(`login: anisette ready (${shortToken(anisetteData.machineID)})`) addLog(`login: anisette ready (${shortToken(anisetteData.machineID)})`)
refreshUi() refreshUi()
return anisetteData return anisetteData
@@ -701,7 +571,8 @@ const ensureAnisetteData = async (): Promise<AnisetteData> => {
const syncAnisetteProvisionedStatus = async (): Promise<void> => { const syncAnisetteProvisionedStatus = async (): Promise<void> => {
try { try {
const anisette = await initAnisette() const anisetteService = await loadAnisetteService()
const anisette = await anisetteService.initAnisette()
anisetteProvisioned = anisette.isProvisioned anisetteProvisioned = anisette.isProvisioned
refreshUi() refreshUi()
} catch (error) { } catch (error) {
@@ -727,7 +598,8 @@ const loginAndSignFlow = async (): Promise<void> => {
const anisette = await ensureAnisetteData() const anisette = await ensureAnisetteData()
addLog("login: authenticating Apple account...") addLog("login: authenticating Apple account...")
const context = await loginAppleDeveloperAccount({ const appleSigning = await loadAppleSigningModule()
const context = await appleSigning.loginAppleDeveloperAccount({
anisetteData: anisette, anisetteData: anisette,
credentials: { appleId, password }, credentials: { appleId, password },
onLog: addLog, onLog: addLog,
@@ -736,7 +608,7 @@ const loginAndSignFlow = async (): Promise<void> => {
}, },
}) })
loginContext = await refreshAppleDeveloperContext(context, addLog) loginContext = await appleSigning.refreshAppleDeveloperContext(context, addLog)
accountContextMap.set(accountKey(loginContext.appleId, loginContext.team.identifier), loginContext) accountContextMap.set(accountKey(loginContext.appleId, loginContext.team.identifier), loginContext)
persistAccountSummary(loginContext) persistAccountSummary(loginContext)
if (rememberSessionInput.checked) { if (rememberSessionInput.checked) {
@@ -781,7 +653,8 @@ const signSelectedIpa = async (): Promise<File> => {
}, },
} }
const refreshed = await refreshAppleDeveloperContext(loginContext, addLog) const appleSigning = await loadAppleSigningModule()
const refreshed = await appleSigning.refreshAppleDeveloperContext(loginContext, addLog)
loginContext = refreshed loginContext = refreshed
accountContextMap.set(accountKey(refreshed.appleId, refreshed.team.identifier), refreshed) accountContextMap.set(accountKey(refreshed.appleId, refreshed.team.identifier), refreshed)
persistAccountSummary(refreshed) persistAccountSummary(refreshed)
@@ -790,7 +663,7 @@ const signSelectedIpa = async (): Promise<File> => {
} }
addLog("sign: preparing ipa...") addLog("sign: preparing ipa...")
const result = await signIpaWithAppleContext({ const result = await appleSigning.signIpaWithAppleContext({
ipaFile: selectedIpaFile, ipaFile: selectedIpaFile,
context: refreshed, context: refreshed,
deviceUdid: targetUdid, deviceUdid: targetUdid,
@@ -868,8 +741,8 @@ const installFlow = async (): Promise<void> => {
addLog("install: uploading and installing...") addLog("install: uploading and installing...")
const bytes = new Uint8Array(await upload.arrayBuffer()) const bytes = new Uint8Array(await upload.arrayBuffer())
const safeName = webmuxdSanitizeIpaFileName(upload.name) const safeName = sanitizeIpaFileName(upload.name)
await webmuxdInstallIpaViaInstProxy(client, bytes, safeName, addLog) await installIpaViaInstProxy(client, bytes, safeName, addLog)
addLog("install: complete") addLog("install: complete")
setInstallProgress(100, "complete") setInstallProgress(100, "complete")
} catch (error) { } catch (error) {
@@ -1405,7 +1278,7 @@ function getOrCreateHostId(): string {
if (existing && existing.trim().length > 0) { if (existing && existing.trim().length > 0) {
return existing return existing
} }
const created = webmuxdCreateHostId() const created = createHostId()
saveText(HOST_ID_STORAGE_KEY, created) saveText(HOST_ID_STORAGE_KEY, created)
return created return created
} }
@@ -1415,7 +1288,7 @@ function getOrCreateSystemBuid(): string {
if (existing && existing.trim().length > 0) { if (existing && existing.trim().length > 0) {
return existing return existing
} }
const created = webmuxdCreateSystemBuid() const created = createSystemBuid()
saveText(SYSTEM_BUID_STORAGE_KEY, created) saveText(SYSTEM_BUID_STORAGE_KEY, created)
return created return created
} }
@@ -1446,7 +1319,7 @@ function savePairRecordForUdid(udid: string, record: PairRecord): void {
return return
} }
const map = readPairRecordMap() const map = readPairRecordMap()
map[normalizedUdid] = webmuxdEncodeStoredPairRecord(record) map[normalizedUdid] = encodeStoredPairRecord(record)
writePairRecordMap(map) writePairRecordMap(map)
} }
@@ -1457,7 +1330,7 @@ function loadLegacyPairRecord(): PairRecord | null {
} }
try { try {
const parsed = JSON.parse(text) as StoredPairRecordPayload const parsed = JSON.parse(text) as StoredPairRecordPayload
return webmuxdDecodeStoredPairRecord(parsed) return decodeStoredPairRecord(parsed)
} catch { } catch {
return null return null
} }
@@ -1473,7 +1346,7 @@ function loadPairRecordForUdid(udid: string): PairRecord | null {
const fromMap = map[normalizedUdid] const fromMap = map[normalizedUdid]
if (fromMap) { if (fromMap) {
try { try {
return webmuxdDecodeStoredPairRecord(fromMap) return decodeStoredPairRecord(fromMap)
} catch { } catch {
return null return null
} }
@@ -1492,7 +1365,7 @@ async function createPairRecord(
hostId: string, hostId: string,
systemBuid: string, systemBuid: string,
): Promise<PairRecord> { ): Promise<PairRecord> {
const payloadText = await webmuxdGeneratePairRecordWithOpenSslWasm({ const payloadText = await generatePairRecordWithOpenSslWasm({
devicePublicKey: devicePublicKeyBytes, devicePublicKey: devicePublicKeyBytes,
hostId, hostId,
systemBuid, systemBuid,
@@ -1763,20 +1636,3 @@ function formatError(error: unknown): string {
} }
return String(error) return String(error)
} }
function resolveWebmuxdExport<T>(moduleValue: Record<string, unknown>, key: string): T {
const direct = moduleValue[key]
if (direct !== undefined) {
return direct as T
}
const defaultValue = moduleValue.default
if (defaultValue && typeof defaultValue === "object") {
const fromDefault = (defaultValue as Record<string, unknown>)[key]
if (fromDefault !== undefined) {
return fromDefault as T
}
}
throw new Error(`webmuxd export ${key} is unavailable`)
}

View File

@@ -1,2 +0,0 @@
declare module "node-forge"

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

6
frontend/src/wasm/libcurl-entry.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export const libcurl: {
fetch(input: string | URL, init?: Record<string, unknown>): Promise<Response>
load_wasm(url?: string): Promise<void>
set_websocket(url: string): void
readonly ready?: boolean
}

View File

@@ -0,0 +1 @@
export { libcurl } from "libcurl.js/bundled"

View File

@@ -0,0 +1,53 @@
export interface LibcurlApi {
fetch(input: string | URL, init?: Record<string, unknown>): Promise<Response>
load_wasm(url?: string): Promise<void>
set_websocket(url: string): void
readonly ready?: boolean
}
let libcurlModulePromise: Promise<LibcurlApi> | null = null
export let libcurl: LibcurlApi
const toLibcurl = (moduleValue: unknown): LibcurlApi => {
if (!moduleValue || typeof moduleValue !== "object") {
throw new Error("libcurl module did not return an object")
}
const candidate = moduleValue as Record<string, unknown>
if (!candidate.libcurl || typeof candidate.libcurl !== "object") {
throw new Error("libcurl module is missing the libcurl export")
}
const loadedLibcurl = candidate.libcurl as Partial<LibcurlApi>
if (typeof loadedLibcurl.fetch !== "function") {
throw new Error("libcurl export is missing fetch")
}
if (typeof loadedLibcurl.load_wasm !== "function") {
throw new Error("libcurl export is missing load_wasm")
}
if (typeof loadedLibcurl.set_websocket !== "function") {
throw new Error("libcurl export is missing set_websocket")
}
return loadedLibcurl as LibcurlApi
}
export const loadLibcurl = async (): Promise<LibcurlApi> => {
if (!libcurlModulePromise) {
libcurlModulePromise = import("./libcurl-entry.js").then((moduleValue) => {
const loadedLibcurl = toLibcurl(moduleValue)
libcurl = loadedLibcurl
return loadedLibcurl
})
}
return await libcurlModulePromise
}
export const requireLibcurl = (): LibcurlApi => {
if (!libcurl) {
throw new Error("libcurl is not ready. Call initLibcurl() first.")
}
return libcurl
}

View File

@@ -0,0 +1,75 @@
let opensslWasmModule = null
let opensslWasmModulePromise = null
let opensslWasmInitPromise = null
const loadOpenSslWasmModule = async () => {
if (!opensslWasmModulePromise) {
opensslWasmModulePromise = import("../../../wasm/openssl/dist/index.mjs").then(
(moduleValue) => {
if (!moduleValue || typeof moduleValue !== "object") {
throw new Error("OpenSSL wasm module did not return an object")
}
const candidate = moduleValue
if (typeof candidate.default !== "function") {
throw new Error("OpenSSL wasm module is missing its default initializer")
}
if (typeof candidate.OpensslClient !== "function") {
throw new Error("OpenSSL wasm module is missing OpensslClient")
}
if (typeof candidate.libimobiledevice_generate_pair_record !== "function") {
throw new Error("OpenSSL wasm module is missing pair record generation")
}
opensslWasmModule = candidate
return candidate
},
)
}
return await opensslWasmModulePromise
}
const requireOpenSslWasmModule = () => {
if (!opensslWasmModule) {
throw new Error("OpenSSL wasm is not ready. Call ensureOpenSslWasmReady() first.")
}
return opensslWasmModule
}
export const ensureOpenSslWasmReady = async () => {
if (!opensslWasmInitPromise) {
opensslWasmInitPromise = loadOpenSslWasmModule().then(async (moduleValue) => {
await moduleValue.default()
})
}
await opensslWasmInitPromise
}
export const createOpenSslWasmConnection = (request) => {
const moduleValue = requireOpenSslWasmModule()
return new moduleValue.OpensslClient(
request.serverName,
request.caCertificatePem,
request.certificatePem,
request.privateKeyPem,
)
}
export const createOpenSslWasmTlsFactory = () => {
return {
ensureReady: ensureOpenSslWasmReady,
createConnection: createOpenSslWasmConnection,
}
}
export const generatePairRecordWithOpenSslWasm = async (request) => {
await ensureOpenSslWasmReady()
const moduleValue = requireOpenSslWasmModule()
return moduleValue.libimobiledevice_generate_pair_record(
new Uint8Array(request.devicePublicKey),
request.hostId,
request.systemBuid,
)
}

120
frontend/src/webmuxd-browser.d.ts vendored Normal file
View File

@@ -0,0 +1,120 @@
declare module "webmuxd" {
export interface WebUsbTransportInstance {
readonly isOpen: boolean
open(): Promise<void>
close(): Promise<void>
send(data: ArrayBuffer): Promise<void>
setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void
setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void
}
export interface PairRecord {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: Uint8Array
escrowBag?: Uint8Array
}
export interface StoredPairRecordPayload {
hostId: string
systemBuid: string
hostCertificatePem: string
hostPrivateKeyPem: string
rootCertificatePem: string
rootPrivateKeyPem: string
deviceCertificatePem: string
devicePublicKey: string
escrowBag: string | null
}
export interface TlsConnection {
is_handshaking(): boolean
write_plaintext(data: Uint8Array): void
feed_tls(data: Uint8Array): void
take_tls_out(): Uint8Array
take_plain_out(): Uint8Array
free(): void
}
export interface TlsConnectionFactory {
ensureReady?(): Promise<void>
createConnection(request: {
serverName: string
caCertificatePem: string
certificatePem: string
privateKeyPem: string
}): TlsConnection
}
export class WebUsbTransport implements WebUsbTransportInstance {
constructor(device: unknown, options?: { logger?: unknown; transferSize?: number })
readonly isOpen: boolean
static supported(): boolean
static requestAppleDevice(logger?: unknown): Promise<WebUsbTransport>
open(): Promise<void>
close(): Promise<void>
send(data: ArrayBuffer): Promise<void>
setDataHandler(handler: ((data: ArrayBuffer) => void) | null): void
setDisconnectHandler(handler: ((reason?: unknown) => void) | null): void
}
export class DirectUsbMuxClient {
constructor(
transport: WebUsbTransportInstance,
options?: {
log?: (message: string) => void
onStateChange?: () => void
lockdownLabel?: string
tlsFactory?: TlsConnectionFactory
pairRecordFactory?: {
createPairRecord(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}): Promise<PairRecord>
}
},
)
readonly isHandshakeComplete: boolean
readonly isLockdownConnected: boolean
readonly isSessionStarted: boolean
readonly isSessionSslEnabled: boolean
readonly isTlsActive: boolean
readonly isPaired: boolean
loadPairRecord(record: PairRecord | null): void
openAndHandshake(): Promise<void>
connectLockdown(port?: number): Promise<void>
getOrFetchDeviceUdid(): Promise<string>
getOrFetchDeviceName(): Promise<string | null>
pairDevice(hostId: string, systemBuid: string): Promise<PairRecord>
startSession(hostId: string, systemBuid: string): Promise<{
sessionId: string
enableSessionSsl: boolean
}>
close(): Promise<void>
}
export const LOCKDOWN_PORT: number
export function installIpaViaInstProxy(
client: DirectUsbMuxClient,
ipaData: Uint8Array,
fileName: string,
onLog?: (message: string) => void,
): Promise<void>
export function sanitizeIpaFileName(fileName: string): string
export function createHostId(): string
export function createSystemBuid(): string
export function encodeStoredPairRecord(record: PairRecord): StoredPairRecordPayload
export function decodeStoredPairRecord(parsed: StoredPairRecordPayload): PairRecord | null
export function createOpenSslWasmTlsFactory(): TlsConnectionFactory
export function generatePairRecordWithOpenSslWasm(request: {
devicePublicKey: Uint8Array
hostId: string
systemBuid: string
}): Promise<string>
}

View File

@@ -0,0 +1,15 @@
export { WebUsbTransport } from "../../dependencies/webmuxd/src/core/webusb-transport.ts"
export {
DirectUsbMuxClient,
LOCKDOWN_PORT,
installIpaViaInstProxy,
sanitizeIpaFileName,
createHostId,
createSystemBuid,
encodeStoredPairRecord,
decodeStoredPairRecord,
} from "../../dependencies/webmuxd/src/core/imobiledevice-client.ts"
export {
createOpenSslWasmTlsFactory,
generatePairRecordWithOpenSslWasm,
} from "./wasm/openssl-webmuxd.js"

View File

@@ -1,4 +0,0 @@
declare module "webmuxd" {
const webmuxd: Record<string, unknown>
export default webmuxd
}

View File

@@ -18,16 +18,22 @@ export default defineConfig({
}, },
resolve: { resolve: {
alias: { alias: {
webmuxd: resolve(repoRootDir, "dependencies/webmuxd/lib/webmuxd.js"), webmuxd: resolve(frontendDir, "src/webmuxd-browser.js"),
}, },
preserveSymlinks: true,
}, },
optimizeDeps: { optimizeDeps: {
include: ["@lbr77/anisette-js/browser"], exclude: [
"altsign.js",
"@lbr77/anisette-js",
"@lbr77/anisette-js/browser",
"@lbr77/zsign-wasm-resigner-wrapper",
"libcurl.js",
"libcurl.js/bundled",
],
}, },
build: { build: {
commonjsOptions: { commonjsOptions: {
include: [/node_modules/, /\/lib\/webmuxd\.js/, /\/lib\/core\/.*\.js/], include: [/node_modules/],
}, },
}, },
}) })

View File

@@ -5,13 +5,20 @@
"workspaces": [ "workspaces": [
"frontend", "frontend",
"backend", "backend",
"dependencies/webmuxd" "dependencies/webmuxd",
"wasm/openssl",
"wasm/libcurl-wasm",
"wasm/zsign-wasm"
], ],
"scripts": { "scripts": {
"build": "cd dependencies/webmuxd && bun run build", "build": "cd dependencies/webmuxd && bun run build",
"build:webmuxd": "cd dependencies/webmuxd && bun run build", "build:webmuxd": "cd dependencies/webmuxd && bun run build",
"build:frontend": "cd frontend && bun run build", "build:frontend": "cd frontend && bun run build",
"build:backend": "cd backend && bun run check", "build:backend": "cd backend && bun run check",
"build:wasm": "bun run build:wasm:openssl && bun run build:wasm:libcurl && bun run build:wasm:zsign",
"build:wasm:openssl": "bash scripts/build-wasm-openssl.sh",
"build:wasm:libcurl": "bash scripts/build-wasm-libcurl.sh",
"build:wasm:zsign": "bash scripts/build-wasm-zsign.sh",
"lint": "cd dependencies/webmuxd && bun run lint", "lint": "cd dependencies/webmuxd && bun run lint",
"test": "cd dependencies/webmuxd && bun run test" "test": "cd dependencies/webmuxd && bun run test"
} }

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wasm-common.sh
source "$SCRIPT_DIR/wasm-common.sh"
activate_emscripten
if [[ "$(uname -s)" != "Linux" ]]; then
echo "libcurl-wasm upstream only supports Linux builds." >&2
echo "Run this script on Linux, or use orb to enter a Linux environment first." >&2
exit 1
fi
cd "$REPO_ROOT/wasm/libcurl-wasm"
bun run build

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wasm-common.sh
source "$SCRIPT_DIR/wasm-common.sh"
OPENSSL_PRECOMPILED="$(resolve_openssl_precompiled_dir || true)"
if [[ -z "${OPENSSL_PRECOMPILED}" ]]; then
echo "OpenSSL precompiled directory not found." >&2
echo "Set OPENSSL_PRECOMPILED_DIR or OPENSSL_ROOT to a directory containing include/ and lib/." >&2
exit 1
fi
cd "$REPO_ROOT/wasm/openssl"
OPENSSL_ROOT="$OPENSSL_PRECOMPILED" bun run build

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wasm-common.sh
source "$SCRIPT_DIR/wasm-common.sh"
activate_emscripten
OPENSSL_PRECOMPILED="$(resolve_openssl_precompiled_dir || true)"
if [[ -z "${OPENSSL_PRECOMPILED}" ]]; then
echo "OpenSSL precompiled directory not found." >&2
echo "Set OPENSSL_PRECOMPILED_DIR or OPENSSL_WASM to a directory containing include/ and lib/." >&2
exit 1
fi
cd "$REPO_ROOT/wasm/zsign-wasm"
OPENSSL_WASM="$OPENSSL_PRECOMPILED" bun run build

43
scripts/wasm-common.sh Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
resolve_openssl_precompiled_dir() {
local candidate
for candidate in \
"${OPENSSL_WASM:-}" \
"${OPENSSL_ROOT:-}" \
"${OPENSSL_PRECOMPILED_DIR:-}" \
"$REPO_ROOT/openssl-wasm/precompiled" \
"$REPO_ROOT/wasm/openssl/precompiled" \
"$REPO_ROOT/wasm/vendor/openssl-wasm/precompiled" \
"$REPO_ROOT/wasm/openssl-wasm/precompiled"
do
if [[ -n "$candidate" && -d "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
activate_emscripten() {
if [[ -n "${EMSDK_ENV:-}" ]]; then
if [[ ! -f "${EMSDK_ENV}" ]]; then
echo "EMSDK_ENV does not exist: ${EMSDK_ENV}" >&2
return 1
fi
# shellcheck disable=SC1090
. "${EMSDK_ENV}"
fi
if ! command -v emcc >/dev/null 2>&1 || ! command -v em++ >/dev/null 2>&1; then
echo "Emscripten is not available. Install it or set EMSDK_ENV to emsdk_env.sh." >&2
return 1
fi
}

View File

@@ -1 +1,3 @@
/target/ /target/
/binary/
/dist/

View File

@@ -16,9 +16,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.56" version = "1.2.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -53,15 +53,15 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.91" version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -79,9 +79,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "openssl" name = "openssl"
@@ -150,9 +150,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.44" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -237,9 +237,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.114" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -250,9 +250,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.114" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -260,9 +260,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.114" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -273,18 +273,18 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.114" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.91" version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View File

@@ -5,18 +5,19 @@ edition = "2021"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
path = "rust-src/lib.rs"
[build-dependencies] [build-dependencies]
cc = "1" cc = "1"
[dependencies] [dependencies]
js-sys = "0.3" js-sys = "0.3.94"
openssl = "0.10.75" openssl = "0.10.75"
openssl-sys = { path = "vendor/openssl-sys" } openssl-sys = { path = "vendor/openssl-sys" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
wasm-bindgen = "0.2.100" wasm-bindgen = "0.2.117"
web-sys = { version = "0.3", features = ["Window", "Crypto"] } web-sys = { version = "0.3.94", features = ["Window", "Crypto"] }
[patch.crates-io] [patch.crates-io]
openssl-sys = { path = "vendor/openssl-sys" } openssl-sys = { path = "vendor/openssl-sys" }

11
wasm/openssl/build.mjs Normal file
View File

@@ -0,0 +1,11 @@
import { cp, mkdir, rm } from "node:fs/promises"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
const packageDir = dirname(fileURLToPath(import.meta.url))
const sourceDir = resolve(packageDir, "src")
const distDir = resolve(packageDir, "dist")
await rm(distDir, { recursive: true, force: true })
await mkdir(distDir, { recursive: true })
await cp(sourceDir, distDir, { recursive: true })

View File

@@ -1,6 +1,6 @@
fn main() { fn main() {
cc::Build::new() cc::Build::new()
.file("src/c_shim/vsnprintf_shim.c") .file("rust-src/c_shim/vsnprintf_shim.c")
.flag_if_supported("-std=c99") .flag_if_supported("-std=c99")
.compile("vsnprintf_shim"); .compile("vsnprintf_shim");
} }

View File

@@ -4,8 +4,42 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT_DIR" cd "$ROOT_DIR"
OPENSSL_ROOT="/Users/libr/Desktop/Life/browser-apple/openssl-wasm/precompiled" OPENSSL_ROOT="${OPENSSL_ROOT:-${OPENSSL_PRECOMPILED_DIR:-$ROOT_DIR/precompiled}}"
LLVM_BIN="/opt/homebrew/opt/llvm/bin" LLVM_BIN="${LLVM_BIN:-}"
if [[ ! -d "$OPENSSL_ROOT" ]]; then
echo "OpenSSL precompiled directory not found: $OPENSSL_ROOT" >&2
echo "Set OPENSSL_ROOT or OPENSSL_PRECOMPILED_DIR to a directory containing include/ and lib/." >&2
exit 1
fi
if [[ -z "$LLVM_BIN" ]] && command -v brew >/dev/null 2>&1; then
BREW_LLVM_PREFIX="$(brew --prefix llvm 2>/dev/null || true)"
if [[ -n "$BREW_LLVM_PREFIX" && -x "$BREW_LLVM_PREFIX/bin/clang" ]]; then
LLVM_BIN="$BREW_LLVM_PREFIX/bin"
fi
fi
if [[ -n "$LLVM_BIN" ]]; then
CLANG_BIN="$LLVM_BIN/clang"
LLVM_AR_BIN="$LLVM_BIN/llvm-ar"
LLVM_RANLIB_BIN="$LLVM_BIN/llvm-ranlib"
else
CLANG_BIN="${CLANG_BIN:-$(command -v clang || true)}"
LLVM_AR_BIN="${LLVM_AR_BIN:-$(command -v llvm-ar || true)}"
LLVM_RANLIB_BIN="${LLVM_RANLIB_BIN:-$(command -v llvm-ranlib || true)}"
fi
if [[ -z "$CLANG_BIN" || -z "$LLVM_AR_BIN" || -z "$LLVM_RANLIB_BIN" ]]; then
echo "LLVM tools for wasm32-unknown-unknown are not available." >&2
echo "Set LLVM_BIN or CLANG_BIN/LLVM_AR_BIN/LLVM_RANLIB_BIN." >&2
exit 1
fi
if ! command -v wasm-bindgen >/dev/null 2>&1; then
echo "Missing wasm-bindgen CLI. Install it with: cargo install wasm-bindgen-cli" >&2
exit 1
fi
export OPENSSL_NO_VENDOR=1 export OPENSSL_NO_VENDOR=1
export OPENSSL_STATIC=1 export OPENSSL_STATIC=1
@@ -14,22 +48,18 @@ export OPENSSL_LIB_DIR="$OPENSSL_ROOT/lib"
export OPENSSL_INCLUDE_DIR="$OPENSSL_ROOT/include" export OPENSSL_INCLUDE_DIR="$OPENSSL_ROOT/include"
export OPENSSL_LIBS="ssl:crypto" export OPENSSL_LIBS="ssl:crypto"
export CC_wasm32_unknown_unknown="$LLVM_BIN/clang --target=wasm32-unknown-unknown" export CC_wasm32_unknown_unknown="$CLANG_BIN --target=wasm32-unknown-unknown"
export AR_wasm32_unknown_unknown="$LLVM_BIN/llvm-ar" export AR_wasm32_unknown_unknown="$LLVM_AR_BIN"
export RANLIB_wasm32_unknown_unknown="$LLVM_BIN/llvm-ranlib" export RANLIB_wasm32_unknown_unknown="$LLVM_RANLIB_BIN"
echo "[1/2] Building wasm32-unknown-unknown with openssl-rs..." echo "[1/2] Building wasm32-unknown-unknown with openssl-rs..."
cargo build --release --target wasm32-unknown-unknown cargo build --release --target wasm32-unknown-unknown
if command -v wasm-bindgen >/dev/null 2>&1; then echo "[2/2] Generating JS bindings with wasm-bindgen..."
echo "[2/2] Generating JS bindings with wasm-bindgen..." rm -rf binary
mkdir -p pkg mkdir -p binary
wasm-bindgen \ wasm-bindgen \
target/wasm32-unknown-unknown/release/openssl_wasm.wasm \ target/wasm32-unknown-unknown/release/openssl_wasm.wasm \
--out-dir pkg \ --out-dir binary \
--target web --target web
echo "Done: pkg/ generated." echo "Done: binary/ generated."
else
echo "[2/2] wasm-bindgen CLI not found; skipped JS binding generation."
echo "Install via: cargo install wasm-bindgen-cli"
fi

28
wasm/openssl/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@lbr77/openssl-wasm",
"version": "0.1.0",
"description": "OpenSSL WebAssembly package with stable src/dist/binary layout.",
"type": "module",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"files": [
"dist/**/*",
"binary/**/*",
"README.md"
],
"scripts": {
"build:binary": "bash ./build.sh",
"build:dist": "bun ./build.mjs",
"build": "bun run build:binary && bun run build:dist",
"clean": "rm -rf dist binary",
"prepack": "bun run build"
}
}

View File

@@ -38,6 +38,15 @@ static COUNT_STAT: AtomicUsize = AtomicUsize::new(0);
static COUNT_OPENDIR: AtomicUsize = AtomicUsize::new(0); static COUNT_OPENDIR: AtomicUsize = AtomicUsize::new(0);
static ARC4_FALLBACK_SEED: AtomicUsize = AtomicUsize::new(0x9E37_79B9); static ARC4_FALLBACK_SEED: AtomicUsize = AtomicUsize::new(0x9E37_79B9);
#[no_mangle]
pub static mut errno: c_int = 0;
#[no_mangle]
pub static CLOCK_REALTIME: c_int = 0;
#[no_mangle]
pub static _CLOCK_REALTIME: c_int = 0;
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct PairRecordWasmOut { struct PairRecordWasmOut {

3
wasm/openssl/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export * from "../binary/openssl_wasm.js"
export default function ensureOpenSslWasmModuleReady(input?: unknown): Promise<void>

View File

@@ -0,0 +1,17 @@
import initOpenSslWasm from "../binary/openssl_wasm.js"
export * from "../binary/openssl_wasm.js"
let initPromise = null
/**
* Keep a stable ESM wrapper in `src/` so consumers always import the package
* entry, while the raw wasm-bindgen output stays isolated in `binary/`.
*/
export default async function ensureOpenSslWasmModuleReady(input) {
if (!initPromise) {
initPromise = Promise.resolve(initOpenSslWasm(input)).then(() => undefined)
}
await initPromise
}