Files
yaklang-chrome-extension/scripts/verify-aesrsa-transaction.mjs
T

225 lines
9.8 KiB
JavaScript

import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { chromium } from 'playwright-core'
import { resolveChromiumPath } from './resolve-chromium.mjs'
const root = resolve(import.meta.dirname, '..')
const extensionPath = resolve(root, process.env.EXTENSION_PATH || '.output/chrome-mv3-enterprise')
const targetURL = process.env.AESRSA_TARGET || 'http://127.0.0.1:82/'
const manifest = JSON.parse(await readFile(resolve(extensionPath, 'manifest.json'), 'utf8'))
const executablePath = await resolveChromiumPath()
const userDataDir = await mkdtemp(join(tmpdir(), 'yakit-aesrsa-'))
let context
async function extensionRequest(page, action, payload = {}) {
return page.evaluate(async ({ requestAction, requestPayload }) => {
const response = await chrome.runtime.sendMessage({ action: requestAction, payload: requestPayload })
if (!response?.ok) throw new Error(response?.error?.message || response?.error || requestAction)
return response.data
}, { requestAction: action, requestPayload: payload })
}
async function waitFor(page, action, payload, predicate, timeoutMs = 15_000) {
const deadline = Date.now() + timeoutMs
let value
while (Date.now() < deadline) {
value = await extensionRequest(page, action, payload)
if (predicate(value)) return value
await page.waitForTimeout(150)
}
throw new Error(`Timed out waiting for ${action}: ${JSON.stringify(value)}`)
}
try {
context = await chromium.launchPersistentContext(userDataDir, {
executablePath,
headless: true,
viewport: { width: 1280, height: 760 },
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
'--no-first-run',
'--no-default-browser-check',
],
})
let serviceWorker = context.serviceWorkers()[0]
if (!serviceWorker) serviceWorker = await context.waitForEvent('serviceworker', { timeout: 15_000 })
const extensionId = new URL(serviceWorker.url()).host
if (manifest.permissions?.includes('userScripts')) {
const extensionsPage = await context.newPage()
await extensionsPage.goto(`chrome://extensions/?id=${extensionId}`)
const toggle = extensionsPage.locator('#allow-user-scripts cr-toggle')
await toggle.waitFor({ state: 'visible', timeout: 10_000 })
if (!await toggle.evaluate((element) => Boolean(element.checked))) await toggle.click()
await extensionsPage.close()
}
const targetPage = await context.newPage()
targetPage.on('dialog', (dialog) => void dialog.dismiss())
let browserRequestCount = 0
targetPage.on('request', (request) => {
if (new URL(request.url()).pathname === '/encrypt/aesrsa.php') browserRequestCount += 1
})
await targetPage.goto(targetURL)
const controlPage = await context.newPage()
await controlPage.goto(`chrome-extension://${extensionId}/options.html`)
const tabId = await controlPage.evaluate(async (url) => {
const tabs = await chrome.tabs.query({})
return tabs.find((tab) => tab.url === url)?.id
}, targetPage.url())
if (!tabId) throw new Error('Could not resolve the AES+RSA target tab')
await extensionRequest(controlPage, 'recording.start', {
tabId, frameId: 0, captureValues: true, maxEntries: 120, maxValueBytes: 8_192,
})
await targetPage.locator('#username').fill('admin')
await targetPage.locator('#password').fill('wrong-password')
await targetPage.getByRole('button', { name: '登录', exact: true }).click()
await targetPage.getByRole('button', { name: 'AES+Rsa加密', exact: true }).click()
await targetPage.waitForTimeout(500)
if (browserRequestCount !== 1) throw new Error(`Initial recording expected one real request, received ${browserRequestCount}`)
const snapshot = await extensionRequest(controlPage, 'recording.get', { tabId, frameId: 0, limit: 120 })
const candidate = snapshot.profileCandidates?.find((item) => (
item.status === 'capture-required'
&& item.sources?.length === 3
&& new URL(item.request?.url, targetURL).pathname === '/encrypt/aesrsa.php'
))
if (!candidate) throw new Error(`AES+RSA request-level candidate was not inferred: ${JSON.stringify(snapshot)}`)
const matcherEvent = snapshot.events?.find((event) => event.id === candidate.capturePlan?.matcherEventId)
if (!matcherEvent?.crypto?.adapterId || !matcherEvent.wrapperHandleId) {
throw new Error(`AES+RSA candidate has no deep-capture matcher: ${JSON.stringify(candidate)}`)
}
await extensionRequest(controlPage, 'deep.capture.start', {
tabId,
frameId: 0,
matcher: {
kind: 'crypto',
adapterId: matcherEvent.crypto.adapterId,
operation: matcherEvent.crypto.operation,
wrapperHandleId: matcherEvent.wrapperHandleId,
scriptUrl: matcherEvent.scriptUrl,
frameHints: candidate.capturePlan.frameHints,
},
})
await targetPage.locator('#username').fill('admin')
await targetPage.locator('#password').fill('wrong-password')
await targetPage.getByRole('button', { name: '登录', exact: true }).click()
let replayClickFailure
const replayClick = targetPage.getByRole('button', { name: 'AES+Rsa加密', exact: true })
.click({ noWaitAfter: true, timeout: 20_000 })
.catch((reason) => { replayClickFailure = reason })
const paused = await waitFor(controlPage, 'deep.capture.status', { tabId, frameId: 0 }, (value) => (
value?.state === 'paused' && value.pause?.collecting !== true
), 20_000)
const automatic = paused.pause?.automaticCapture
const frame = paused.pause?.frames?.find((item) => item.id === automatic?.frameId)
if (automatic?.state !== 'ready' || automatic.strategy !== 'request-transaction'
|| frame?.functionName !== 'sendDataAesRsa') {
throw new Error(`Deep capture did not select sendDataAesRsa as a request transaction: ${JSON.stringify(paused)}`)
}
const callable = await extensionRequest(controlPage, 'callable.create', {
tabId,
frameId: 0,
source: 'deep-capture',
strategy: 'request-transaction',
callFrameId: frame.id,
name: 'sendDataAesRsa 请求事务',
transaction: {
request: {
method: candidate.request.method,
url: candidate.request.url,
expectedDestinations: candidate.sources.map((source) => source.destination).filter(Boolean),
},
inputMode: 'auto',
boundaries: ['fetch', 'xhr', 'beacon', 'form'],
},
})
if (callable.kind !== 'request-transaction' || callable.inputSlots?.[0]?.name !== 'body') {
throw new Error(`Captured callable is not a request transaction: ${JSON.stringify(callable)}`)
}
await replayClick
if (replayClickFailure) throw replayClickFailure
await targetPage.waitForTimeout(300)
if (browserRequestCount !== 1) {
throw new Error(`Deep-capture replay leaked a real request; observed ${browserRequestCount}`)
}
let execution
try {
execution = await extensionRequest(controlPage, 'callable.execute', {
tabId,
frameId: 0,
callableId: callable.id,
args: [{ username: 'admin', password: '123456' }],
})
} catch (reason) {
const diagnostics = await controlPage.evaluate(async ({ targetTabId, callableId }) => {
const tab = await chrome.tabs.get(targetTabId).catch(() => undefined)
const [injection] = await chrome.scripting.executeScript({
target: { tabId: targetTabId, frameIds: [0] },
world: 'MAIN',
func: async (registryKey, protocolVersion, requestedCallableId) => {
const controller = globalThis[registryKey]
let directExecution
try {
directExecution = {
ok: true,
value: await controller?.command('callable.execute', {
callableId: requestedCallableId,
args: [{ username: 'admin', password: '123456' }],
}),
}
} catch (error) {
directExecution = {
ok: false,
error: error instanceof Error ? `${error.name}: ${error.message}\n${error.stack || ''}` : String(error),
}
}
return {
href: location.href,
controllerVersion: controller?.version,
expectedVersion: protocolVersion,
callables: typeof controller?.command === 'function' ? controller.command('callable.list', {}) : [],
directExecution,
}
},
args: ['__YAKIT_PAGE_RECORDER_V8__', 8, callableId],
}).catch(() => [])
return { tabUrl: tab?.url, page: injection?.result }
}, { targetTabId: tabId, callableId: callable.id })
const selectedFrame = {
functionName: frame.functionName,
functionInspection: frame.functionInspection,
scopes: frame.scopes,
}
throw new Error(`Callable execution failed: ${reason instanceof Error ? reason.message : String(reason)}; browserRequests=${browserRequestCount}; frame=${JSON.stringify(selectedFrame)}; diagnostics=${JSON.stringify(diagnostics)}`)
}
const envelope = execution.value
for (const field of ['encryptedData', 'encryptedKey', 'encryptedIv']) {
if (typeof envelope?.[field] !== 'string' || !envelope[field]) {
throw new Error(`Transaction output is missing ${field}: ${JSON.stringify(execution)}`)
}
}
if (browserRequestCount !== 1) {
throw new Error(`Transaction execution leaked a real browser request; observed ${browserRequestCount}`)
}
const response = await fetch(new URL('/encrypt/aesrsa.php', targetURL), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(envelope),
})
const result = await response.json()
if (!result.success) throw new Error(`Target server rejected the transaction envelope: ${JSON.stringify(result)}`)
process.stdout.write('AES+RSA request transaction verified: no browser request leaked and the target server accepted the captured envelope.\n')
} finally {
await context?.close().catch(() => undefined)
await rm(userDataDir, { recursive: true, force: true })
}