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

252 lines
9.7 KiB
JavaScript

import {
extensionRequest,
launchBrowserAgentContractHarness,
transformedFetchOptions,
waitFor,
} from './browser-agent-contract-harness.mjs'
const targetURL = process.env.AESSERVER_TARGET || 'http://127.0.0.1:82/'
const keyPath = '/encrypt/server_generate_key.php'
const requestPath = '/encrypt/aesserver.php'
const plaintext = {username: 'admin', password: '123456'}
let harness
function pathOf(url) {
return new URL(url, targetURL).pathname
}
function snapshotSummary(snapshot) {
return {
events: snapshot.events?.map((event) => ({
sequence: event.sequence,
kind: event.kind,
operation: event.operation,
url: event.url ? pathOf(event.url) : undefined,
})),
candidates: snapshot.profileCandidates?.map((candidate) => ({
status: candidate.status,
request: pathOf(candidate.request?.url || ''),
prerequisites: candidate.capturePlan?.transaction?.prerequisites?.map((step) => pathOf(step.url)),
})),
}
}
function dependencySummary(snapshot, candidate) {
const eventIds = new Set(candidate.evidence?.flatMap((item) => item.eventIds || []))
return {
evidence: candidate.evidence?.filter((item) => item.kind === 'response-boundary'),
events: snapshot.events?.filter((event) => eventIds.has(event.id)).map((event) => ({
id: event.id,
sequence: event.sequence,
kind: event.kind,
operation: event.operation,
inputs: event.inputs?.map((item) => item.path),
outputs: event.outputs?.map((item) => item.path),
})),
links: snapshot.links?.filter((link) => eventIds.has(link.fromEventId) || eventIds.has(link.toEventId))
.map((link) => ({kind: link.kind, fromPath: link.fromPath, toPath: link.toPath})),
}
}
async function performLogin(targetPage, password = 'wrong-password') {
await targetPage.locator('#username').fill('admin')
await targetPage.locator('#password').fill(password)
await targetPage.getByRole('button', {name: '登录', exact: true}).click()
return targetPage.getByRole('button', {name: 'AES服务端获取Key', exact: true})
}
try {
harness = await launchBrowserAgentContractHarness({
profilePrefix: 'yakit-aesserver-',
targetURL,
})
const {controlPage, tabId, targetPage} = harness
const observed = {key: 0, terminal: 0}
targetPage.on('request', (request) => {
const path = pathOf(request.url())
if (path === keyPath) observed.key += 1
if (path === requestPath) observed.terminal += 1
})
await extensionRequest(controlPage, 'recording.start', {
tabId,
frameId: 0,
captureValues: true,
maxEntries: 160,
maxValueBytes: 8_192,
})
await (await performLogin(targetPage)).click()
await targetPage.waitForTimeout(600)
if (observed.key !== 1 || observed.terminal !== 1) {
throw new Error(`Initial operation did not produce the expected two-request flow: ${JSON.stringify(observed)}`)
}
const snapshot = await extensionRequest(
controlPage,
'recording.get',
{tabId, frameId: 0, limit: 160},
)
const candidate = snapshot.profileCandidates?.find((item) => (
item.status === 'capture-required'
&& pathOf(item.request?.url || '') === requestPath
&& item.capturePlan?.transaction?.prerequisites?.some((step) => pathOf(step.url) === keyPath)
))
if (!candidate) {
throw new Error(`Recording did not infer the online key dependency: ${JSON.stringify(snapshotSummary(snapshot))}`)
}
const transaction = candidate.capturePlan.transaction
if (transaction.version !== 2
|| transaction.prerequisites.length !== 1
|| transaction.prerequisites[0].boundary !== 'fetch'
|| transaction.prerequisites[0].response.bodyFormat !== 'json'
|| !transaction.prerequisites[0].response.requiredPaths.includes('body.aes_key')
|| !transaction.prerequisites[0].response.requiredPaths.includes('body.aes_iv')
|| transaction.request.boundary !== 'fetch'
|| pathOf(transaction.request.url) !== requestPath) {
throw new Error(`Inferred request transaction is not evidence-complete: ${JSON.stringify({
transaction,
dependency: dependencySummary(snapshot, candidate),
})}`)
}
const matcherEvent = snapshot.events?.find((event) => event.id === candidate.capturePlan.matcherEventId)
if (!matcherEvent?.crypto?.adapterId || !matcherEvent.wrapperHandleId) {
throw new Error(`Online-key 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,
},
})
const replayButton = await performLogin(targetPage)
let replayFailure
const replay = replayButton.click({noWaitAfter: true, timeout: 20_000})
.catch((reason) => { replayFailure = 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 !== 'fetchAndSendDataAes') {
throw new Error(`Deep capture selected an invalid strategy: ${JSON.stringify({automatic, functionName: frame?.functionName})}`)
}
const callable = await extensionRequest(controlPage, 'callable.create', {
tabId,
frameId: 0,
source: 'deep-capture',
strategy: 'request-transaction',
callFrameId: frame.id,
name: '在线取钥请求事务',
candidateId: candidate.id,
})
await replay
if (replayFailure) throw replayFailure
await targetPage.waitForTimeout(300)
if (observed.key !== 2 || observed.terminal !== 1) {
throw new Error(`Deep capture did not preserve the prerequisite/terminal boundary: ${JSON.stringify(observed)}`)
}
const beforeCallable = {...observed}
const callableExecution = await extensionRequest(controlPage, 'callable.execute', {
tabId,
frameId: 0,
callableId: callable.id,
args: [plaintext],
})
if (typeof callableExecution.value?.encryptedData !== 'string') {
throw new Error(`Request transaction did not return the terminal envelope: ${JSON.stringify(callableExecution)}`)
}
if (observed.key !== beforeCallable.key + 1 || observed.terminal !== beforeCallable.terminal) {
throw new Error(`Callable execution leaked or skipped a request: ${JSON.stringify({beforeCallable, observed})}`)
}
const plainPacket = {
method: 'POST',
url: new URL(requestPath, targetURL).toString(),
headers: [{name: 'Content-Type', value: 'application/json'}],
bodyBase64: Buffer.from(JSON.stringify(plaintext)).toString('base64'),
}
const validation = await extensionRequest(controlPage, 'analysis.profile.validate', {
tabId,
frameId: 0,
candidateId: candidate.id,
callableId: callable.id,
inputPaths: ['body'],
name: '在线取钥明文网关',
packet: plainPacket,
comparisonMode: 'structure',
})
if (!validation?.valid || !validation?.saveEligible || !validation.validationDraft?.id) {
throw new Error(`Online-key Profile validation failed: ${JSON.stringify({
valid: validation?.valid,
saveEligible: validation?.saveEligible,
proofLevel: validation?.proofLevel,
})}`)
}
const validationDraft = await extensionRequest(
controlPage,
'analysis.profile.validation.latest',
{tabId, frameId: 0},
)
if (!validationDraft?.profile || validationDraft.id !== validation.validationDraft.id) {
throw new Error('Validated online-key Profile draft was not available for confirmation')
}
const savedProfile = await extensionRequest(
controlPage,
'transform.profile.save',
validationDraft.profile,
)
if (
savedProfile.requestTransaction?.callableId !== callable.id
|| savedProfile.requestTransaction?.transaction?.version !== 2
) {
throw new Error('Saved Profile did not retain its trusted request-transaction binding')
}
const beforeProfile = {...observed}
const execution = await extensionRequest(controlPage, 'transform.execute', {
profileId: savedProfile.id,
direction: 'request',
packet: plainPacket,
})
if (observed.key !== beforeProfile.key + 1 || observed.terminal !== beforeProfile.terminal) {
throw new Error(`Saved Profile leaked or skipped a request: ${JSON.stringify({beforeProfile, observed})}`)
}
const sessionHeader = execution.setHeaders?.find((header) => header.name.toLowerCase() === 'cookie')
if (!sessionHeader?.value.includes('PHPSESSID=')) {
throw new Error(`Saved Profile did not bind the browser session to the outgoing packet: ${JSON.stringify(execution)}`)
}
const wireBody = JSON.parse(Buffer.from(execution.bodyBase64, 'base64').toString('utf8'))
if (typeof wireBody.encryptedData !== 'string' || Object.keys(wireBody).length !== 1) {
throw new Error(`Saved Profile produced an invalid terminal envelope: ${JSON.stringify(wireBody)}`)
}
const response = await fetch(
execution.url,
transformedFetchOptions(execution, plainPacket.headers),
)
const result = await response.json()
if (!result.success) {
throw new Error(`Target server rejected the session-bound Profile output: ${JSON.stringify(result)}`)
}
process.stdout.write(
'Online-key transaction verified: evidence inferred one bounded prerequisite, the browser sent no terminal request during replay, the saved Profile exported its browser session, and the target server accepted the final packet.\n',
)
} finally {
await harness?.close()
}