feat: implement main process obfuscation and verification

- Add package-main-obfuscation.mjs to handle obfuscation of main process JavaScript files.
- Introduce obfuscatePackageMain function to obfuscate main files and generate a report.
- Implement verifyPackageMainObfuscation function to validate obfuscation reports.
- Create obfuscate-package-main.mjs script for command-line obfuscation execution.
- Add audit-packaged-app.mjs to check for protected plaintext exposure in packaged apps.
- Enhance tests to cover obfuscation and verification processes, including protected markers.
- Ensure obfuscation does not exceed specified growth limits and maintains original functionality.
This commit is contained in:
hy committed 2026-07-20 14:44:32 +08:00
1 parent c5c1d8c651
commit 7fb3e2bc25
8 files changed
+1070 -4

No files matched your search

+4 -1
View File
@@ -42,9 +42,11 @@
"start": "electron-vite preview",
"dev": "node scripts/prepare-better-sqlite3-native.mjs ensure electron && node scripts/prepare-node-pty-native.mjs ensure electron && node scripts/ensure-web-crawler-sidecar.mjs && electron-vite dev",
"dev:no-sidecar": "node scripts/prepare-better-sqlite3-native.mjs ensure electron && node scripts/prepare-node-pty-native.mjs ensure electron && electron-vite dev",
"build": "node scripts/prepare-better-sqlite3-native.mjs ensure node && node scripts/prepare-node-pty-native.mjs ensure node && tsc --noEmit -p tsconfig.node.json --composite false && tsc --noEmit -p tsconfig.web.json --composite false && node scripts/build-web-crawler-sidecar.mjs && node scripts/prepare-better-sqlite3-native.mjs export-electron-resource && electron-vite build && node scripts/verify-runtime-bundles.mjs && node scripts/prepare-node-pty-native.mjs ensure electron && node scripts/prepare-package-stage.mjs",
"build": "node scripts/prepare-better-sqlite3-native.mjs ensure node && node scripts/prepare-node-pty-native.mjs ensure node && tsc --noEmit -p tsconfig.node.json --composite false && tsc --noEmit -p tsconfig.web.json --composite false && node scripts/build-web-crawler-sidecar.mjs && node scripts/prepare-better-sqlite3-native.mjs export-electron-resource && electron-vite build && node scripts/verify-runtime-bundles.mjs && node scripts/prepare-node-pty-native.mjs ensure electron && node scripts/prepare-package-stage.mjs && node scripts/obfuscate-package-main.mjs && node scripts/verify-package-main-obfuscation.mjs && node scripts/audit-package-stage.mjs",
"postinstall": "node scripts/postinstall.mjs",
"package:audit:stage": "node scripts/audit-package-stage.mjs",
"package:obfuscate:main": "node scripts/obfuscate-package-main.mjs",
"package:obfuscation:verify": "node scripts/verify-package-main-obfuscation.mjs",
"package:audit": "node scripts/audit-packaged-app.mjs dist",
"package:audit:summary": "node scripts/print-package-audit-summary.mjs dist/package-audit.json",
"build:unpack": "pnpm run build && electron-builder --dir && pnpm run package:audit",
@@ -141,6 +143,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"javascript-obfuscator": "5.5.0",
"jsdom": "^26.1.0",
"less": "^4.5.1",
"prettier": "^3.7.4",
+561 -3
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -9,6 +9,7 @@ import {
normalizeRelativePath,
scanTextForSecrets
} from './package-content-policy.mjs'
import { assertProtectedMainPlaintextAbsent } from './package-main-obfuscation.mjs'
import { evaluatePackageSizeBudget, loadPackageSizeBudgetConfig } from './package-size-budget.mjs'
const inputDirectory = path.resolve(process.argv[2] || 'dist')
@@ -107,6 +108,9 @@ for (const archivePath of archives) {
if (!/\.(?:js|cjs|mjs|json|html|css)$/u.test(item.relativePath)) continue
const source = extractArchiveFile(archivePath, item.relativePath).toString('utf8')
scanTextForSecrets(source, `${archivePath}:${item.relativePath}`)
if (item.relativePath.startsWith('out/main/')) {
assertProtectedMainPlaintextAbsent(source, `${archivePath}:${item.relativePath}`)
}
}
const appRoot = findApplicationRoot(archivePath)
+18
View File
@@ -0,0 +1,18 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { obfuscatePackageMain } from './package-main-obfuscation.mjs'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const stageDirectory = path.resolve(process.argv[2] || path.join(root, 'release-stage', 'app'))
const reportPath = path.resolve(
process.argv[3] || path.join(root, 'release-stage', 'main-obfuscation-report.json')
)
const report = obfuscatePackageMain({ stageDirectory, reportPath })
process.stdout.write(
`[package] main obfuscation passed: ${String(report.totals.files)} files, ` +
`${(report.totals.originalBytes / 1024 / 1024).toFixed(1)} MiB -> ` +
`${(report.totals.obfuscatedBytes / 1024 / 1024).toFixed(1)} MiB\n`
)
process.stdout.write(`[package] main obfuscation report: ${reportPath}\n`)
+309
View File
@@ -0,0 +1,309 @@
import crypto from 'node:crypto'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
import { spawnSync } from 'node:child_process'
import JavaScriptObfuscator from 'javascript-obfuscator'
const require = createRequire(import.meta.url)
const obfuscatorPackage = require('javascript-obfuscator/package.json')
export const MAIN_OBFUSCATION_SCHEMA_VERSION = 1
export const MAIN_OBFUSCATION_CONFIGURATION_VERSION = 1
export const MAX_MAIN_OBFUSCATION_GROWTH_BYTES = 3 * 1024 * 1024
export const PROTECTED_MAIN_PLAINTEXT_MARKERS = [
{
id: 'brain-system-prompt',
value: '像一个判断稳定的人类执行者一样工作'
},
{
id: 'mouth-system-prompt',
value: '你的职责是基于已经提供的信息,面向用户交付最终结果'
}
]
const JAVASCRIPT_EXTENSIONS = new Set(['.js', '.cjs', '.mjs'])
const normalizeRelativePath = (value) => value.replaceAll(path.sep, '/')
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex')
const deriveSeed = (relativePath) => {
const digest = crypto
.createHash('sha256')
.update(`ncurator-main-obfuscation-v1\0${normalizeRelativePath(relativePath)}`)
.digest()
return Math.max(1, digest.readUInt32BE(0) & 0x7fffffff)
}
export const createMainObfuscationOptions = (relativePath) => ({
compact: true,
target: 'node',
identifierNamesGenerator: 'hexadecimal',
renameGlobals: false,
renameProperties: false,
transformObjectKeys: false,
stringArray: true,
stringArrayThreshold: 1,
stringArrayEncoding: ['rc4'],
stringArrayRotate: true,
stringArrayShuffle: true,
splitStrings: true,
splitStringsChunkLength: 8,
controlFlowFlattening: false,
deadCodeInjection: false,
debugProtection: false,
selfDefending: false,
disableConsoleOutput: false,
numbersToExpressions: false,
unicodeEscapeSequence: false,
sourceMap: false,
seed: deriveSeed(relativePath)
})
export const obfuscateMainJavaScript = (source, relativePath) =>
JavaScriptObfuscator.obfuscate(
source,
createMainObfuscationOptions(relativePath)
).getObfuscatedCode()
const listJavaScriptFiles = (rootDirectory) => {
const result = []
const walk = (directory) => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const absolutePath = path.join(directory, entry.name)
if (entry.isDirectory()) {
walk(absolutePath)
continue
}
if (entry.isFile() && JAVASCRIPT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
result.push(absolutePath)
}
}
}
walk(rootDirectory)
return result.sort((left, right) => left.localeCompare(right))
}
const assertJavaScriptSyntax = (filePath) => {
const result = spawnSync(process.execPath, ['--check', filePath], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
})
if (result.error) throw result.error
if (result.status !== 0) {
throw new Error(
`obfuscated JavaScript syntax check failed for ${filePath}:\n${String(result.stderr || result.stdout).trim()}`
)
}
}
const assertProtectedMarkersPresent = (sources) => {
const missing = PROTECTED_MAIN_PLAINTEXT_MARKERS.filter(
(marker) => !sources.some((source) => source.includes(marker.value))
)
if (missing.length > 0) {
throw new Error(
`main obfuscation protection markers were not found before obfuscation: ${missing
.map((marker) => marker.id)
.join(', ')}`
)
}
}
export const assertProtectedMainPlaintextAbsent = (source, sourceLabel) => {
const exposed = PROTECTED_MAIN_PLAINTEXT_MARKERS.filter((marker) => source.includes(marker.value))
if (exposed.length > 0) {
throw new Error(
`${sourceLabel} exposes protected main-process plaintext: ${exposed
.map((marker) => marker.id)
.join(', ')}`
)
}
}
const writeJsonAtomically = (filePath, value) => {
fs.mkdirSync(path.dirname(filePath), { recursive: true })
const temporaryPath = `${filePath}.tmp-${String(process.pid)}`
try {
fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
fs.renameSync(temporaryPath, filePath)
} finally {
fs.rmSync(temporaryPath, { force: true })
}
}
export const obfuscatePackageMain = ({ stageDirectory, reportPath }) => {
const stageRoot = path.resolve(stageDirectory)
const mainDirectory = path.join(stageRoot, 'out', 'main')
const resolvedReportPath = path.resolve(reportPath)
fs.rmSync(resolvedReportPath, { force: true })
if (!fs.existsSync(mainDirectory) || !fs.statSync(mainDirectory).isDirectory()) {
throw new Error(`staged main-process directory not found: ${mainDirectory}`)
}
const packageJsonPath = path.join(stageRoot, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const files = listJavaScriptFiles(mainDirectory)
if (files.length === 0)
throw new Error(`no staged main-process JavaScript found: ${mainDirectory}`)
const originalSources = files.map((filePath) => fs.readFileSync(filePath, 'utf8'))
assertProtectedMarkersPresent(originalSources)
const pending = []
let originalBytes = 0
let obfuscatedBytes = 0
let growthBytes = 0
try {
for (let index = 0; index < files.length; index += 1) {
const filePath = files[index]
const relativePath = normalizeRelativePath(path.relative(stageRoot, filePath))
const originalSource = originalSources[index]
const obfuscatedSource = obfuscateMainJavaScript(originalSource, relativePath)
if (!obfuscatedSource || obfuscatedSource === originalSource) {
throw new Error(`main-process JavaScript was not transformed: ${relativePath}`)
}
assertProtectedMainPlaintextAbsent(obfuscatedSource, relativePath)
if (
/(?:\/\/[#@]\s*sourceMappingURL\s*=|\/\*[#@]\s*sourceMappingURL\s*=)/u.test(
obfuscatedSource
)
) {
throw new Error(
`obfuscated main-process JavaScript references a source map: ${relativePath}`
)
}
const extension = path.extname(filePath)
const temporaryPath = path.join(
path.dirname(filePath),
`.${path.basename(filePath, extension)}.obfuscating-${String(process.pid)}${extension}`
)
fs.writeFileSync(temporaryPath, obfuscatedSource, 'utf8')
pending.push({
filePath,
temporaryPath,
relativePath,
originalBytes: Buffer.byteLength(originalSource),
obfuscatedBytes: Buffer.byteLength(obfuscatedSource),
originalSha256: sha256(originalSource),
obfuscatedSha256: sha256(obfuscatedSource)
})
assertJavaScriptSyntax(temporaryPath)
}
originalBytes = pending.reduce((sum, item) => sum + item.originalBytes, 0)
obfuscatedBytes = pending.reduce((sum, item) => sum + item.obfuscatedBytes, 0)
growthBytes = obfuscatedBytes - originalBytes
if (growthBytes > MAX_MAIN_OBFUSCATION_GROWTH_BYTES) {
throw new Error(
`main-process obfuscation grew by ${(growthBytes / 1024 / 1024).toFixed(1)} MiB; limit is ${(MAX_MAIN_OBFUSCATION_GROWTH_BYTES / 1024 / 1024).toFixed(1)} MiB`
)
}
for (const item of pending) fs.renameSync(item.temporaryPath, item.filePath)
} finally {
for (const item of pending) fs.rmSync(item.temporaryPath, { force: true })
}
const report = {
schemaVersion: MAIN_OBFUSCATION_SCHEMA_VERSION,
configurationVersion: MAIN_OBFUSCATION_CONFIGURATION_VERSION,
status: 'passed',
generatedAt: new Date().toISOString(),
packageVersion: String(packageJson.version || ''),
tool: {
name: 'javascript-obfuscator',
version: String(obfuscatorPackage.version)
},
protectedMarkers: PROTECTED_MAIN_PLAINTEXT_MARKERS.map((marker) => marker.id),
totals: {
files: pending.length,
originalBytes,
obfuscatedBytes,
growthBytes,
growthPercent: originalBytes > 0 ? (growthBytes / originalBytes) * 100 : 0
},
files: pending.map((item) => ({
path: item.relativePath,
originalBytes: item.originalBytes,
obfuscatedBytes: item.obfuscatedBytes,
originalSha256: item.originalSha256,
obfuscatedSha256: item.obfuscatedSha256
}))
}
writeJsonAtomically(resolvedReportPath, report)
return report
}
export const verifyPackageMainObfuscation = ({ stageDirectory, reportPath }) => {
const stageRoot = path.resolve(stageDirectory)
const mainDirectory = path.join(stageRoot, 'out', 'main')
const resolvedReportPath = path.resolve(reportPath)
if (!fs.existsSync(resolvedReportPath)) {
throw new Error(`main-process obfuscation report not found: ${resolvedReportPath}`)
}
const report = JSON.parse(fs.readFileSync(resolvedReportPath, 'utf8'))
if (
report.schemaVersion !== MAIN_OBFUSCATION_SCHEMA_VERSION ||
report.configurationVersion !== MAIN_OBFUSCATION_CONFIGURATION_VERSION ||
report.status !== 'passed'
) {
throw new Error('main-process obfuscation report has an unsupported or failed status')
}
if (
report.tool?.name !== 'javascript-obfuscator' ||
report.tool?.version !== String(obfuscatorPackage.version) ||
JSON.stringify(report.protectedMarkers) !==
JSON.stringify(PROTECTED_MAIN_PLAINTEXT_MARKERS.map((marker) => marker.id))
) {
throw new Error('main-process obfuscation report does not match the active protection policy')
}
const packageJson = JSON.parse(fs.readFileSync(path.join(stageRoot, 'package.json'), 'utf8'))
if (report.packageVersion !== String(packageJson.version || '')) {
throw new Error('main-process obfuscation report does not match the staged package version')
}
const currentFiles = listJavaScriptFiles(mainDirectory).map((filePath) =>
normalizeRelativePath(path.relative(stageRoot, filePath))
)
const reportedFiles = report.files.map((item) => item.path)
if (JSON.stringify(currentFiles) !== JSON.stringify(reportedFiles)) {
throw new Error('main-process obfuscation report does not match the staged JavaScript files')
}
for (const item of report.files) {
const filePath = path.join(stageRoot, item.path)
const source = fs.readFileSync(filePath, 'utf8')
if (sha256(source) !== item.obfuscatedSha256 || item.originalSha256 === item.obfuscatedSha256) {
throw new Error(`main-process obfuscation hash mismatch: ${item.path}`)
}
assertProtectedMainPlaintextAbsent(source, item.path)
assertJavaScriptSyntax(filePath)
}
const originalBytes = report.files.reduce((sum, item) => sum + Number(item.originalBytes || 0), 0)
const obfuscatedBytes = report.files.reduce(
(sum, item) => sum + Number(item.obfuscatedBytes || 0),
0
)
if (
report.totals.files !== report.files.length ||
report.totals.originalBytes !== originalBytes ||
report.totals.obfuscatedBytes !== obfuscatedBytes ||
report.totals.growthBytes !== obfuscatedBytes - originalBytes
) {
throw new Error('main-process obfuscation report totals are inconsistent')
}
if (report.totals.growthBytes > MAX_MAIN_OBFUSCATION_GROWTH_BYTES) {
throw new Error('main-process obfuscation report exceeds the allowed growth budget')
}
return report
}
@@ -0,0 +1,16 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { verifyPackageMainObfuscation } from './package-main-obfuscation.mjs'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const stageDirectory = path.resolve(process.argv[2] || path.join(root, 'release-stage', 'app'))
const reportPath = path.resolve(
process.argv[3] || path.join(root, 'release-stage', 'main-obfuscation-report.json')
)
const report = verifyPackageMainObfuscation({ stageDirectory, reportPath })
process.stdout.write(
`[package] main obfuscation verified: ${String(report.totals.files)} files, ` +
`growth ${(report.totals.growthBytes / 1024 / 1024).toFixed(1)} MiB\n`
)
@@ -6,6 +6,7 @@ import { promisify } from 'node:util'
import { createPackage } from '@electron/asar'
import { afterEach, describe, expect, it } from 'vitest'
import { normalizeRelativePath } from '../../../../scripts/package-content-policy.mjs'
import { PROTECTED_MAIN_PLAINTEXT_MARKERS } from '../../../../scripts/package-main-obfuscation.mjs'
const execFileAsync = promisify(execFile)
let temporaryDirectory = ''
@@ -66,4 +67,39 @@ describe('package content policy paths', () => {
expect.objectContaining({ root: 'win-unpacked', archiveFileCount: 2 })
])
})
it('rejects a packaged app that exposes protected main-process prompts', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-package-audit-'))
const appSource = path.join(temporaryDirectory, 'app-source')
const packagedApp = path.join(temporaryDirectory, 'package', 'win-unpacked')
const archivePath = path.join(packagedApp, 'resources', 'app.asar')
await mkdir(path.join(appSource, 'out', 'main'), { recursive: true })
await mkdir(path.dirname(archivePath), { recursive: true })
await writeFile(
path.join(appSource, 'out', 'main', 'index.js'),
`const prompt = ${JSON.stringify(PROTECTED_MAIN_PLAINTEXT_MARKERS[0].value)}\n`
)
await writeFile(
path.join(appSource, 'package.json'),
`${JSON.stringify({
name: 'ncurator-audit-fixture',
version: '1.0.0',
main: './out/main/index.js',
private: true,
dependencies: {}
})}\n`
)
await createPackage(appSource, archivePath)
let stderr = ''
try {
await execFileAsync(process.execPath, [
path.resolve('scripts/audit-packaged-app.mjs'),
path.join(temporaryDirectory, 'package')
])
} catch (error) {
stderr = String((error as { stderr?: string }).stderr || error)
}
expect(stderr).toContain('exposes protected main-process plaintext')
})
})
@@ -0,0 +1,122 @@
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import vm from 'node:vm'
import { afterEach, describe, expect, it } from 'vitest'
import {
MAX_MAIN_OBFUSCATION_GROWTH_BYTES,
PROTECTED_MAIN_PLAINTEXT_MARKERS,
obfuscateMainJavaScript,
obfuscatePackageMain,
verifyPackageMainObfuscation
} from '../../../../scripts/package-main-obfuscation.mjs'
let temporaryDirectory = ''
afterEach(async () => {
if (temporaryDirectory) await rm(temporaryDirectory, { recursive: true, force: true })
temporaryDirectory = ''
})
const fixtureSource = `
'use strict'
const path = require('node:path')
const buildPrompt = (input) => [
'<system>',
'像一个判断稳定的人类执行者一样工作',
'你的职责是基于已经提供的信息,面向用户交付最终结果',
JSON.stringify({ input, schema: { type: 'object', required: ['answer'] } }, null, 2),
'</system>',
path.sep
].join('\\n')
globalThis.__ncuratorPromptResult = buildPrompt('中文、emoji 🚀、换行\\n保持一致')
`
const executeFixture = (source: string): string => {
const requiredSpecifiers: string[] = []
const context: Record<string, unknown> = {
require: (specifier: string) => {
requiredSpecifiers.push(specifier)
return { sep: '/' }
}
}
vm.runInNewContext(source, context, { timeout: 10_000 })
return JSON.stringify({
prompt: String(context.__ncuratorPromptResult || ''),
requiredSpecifiers
})
}
describe('package-main-obfuscation main-process release protection', () => {
it('preserves the exact runtime prompt while hiding protected plaintext', () => {
const originalPrompt = executeFixture(fixtureSource)
const obfuscated = obfuscateMainJavaScript(fixtureSource, 'out/main/index.js')
const repeated = obfuscateMainJavaScript(fixtureSource, 'out/main/index.js')
const otherFile = obfuscateMainJavaScript(fixtureSource, 'out/main/worker.js')
const obfuscatedPrompt = executeFixture(obfuscated)
expect(obfuscatedPrompt).toBe(originalPrompt)
expect(obfuscated).not.toBe(fixtureSource)
expect(repeated).toBe(obfuscated)
expect(otherFile).not.toBe(obfuscated)
for (const marker of PROTECTED_MAIN_PLAINTEXT_MARKERS) {
expect(fixtureSource).toContain(marker.value)
expect(obfuscated).not.toContain(marker.value)
}
})
it('writes and verifies a report tied to every staged main-process file', async () => {
temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'ncurator-main-obfuscation-'))
const stageDirectory = path.join(temporaryDirectory, 'app')
const mainDirectory = path.join(stageDirectory, 'out', 'main')
const reportPath = path.join(temporaryDirectory, 'main-obfuscation-report.json')
await mkdir(mainDirectory, { recursive: true })
await writeFile(
path.join(stageDirectory, 'package.json'),
`${JSON.stringify({ name: 'fixture', version: '1.2.3' })}\n`,
'utf8'
)
await writeFile(path.join(mainDirectory, 'index.js'), fixtureSource, 'utf8')
await writeFile(
path.join(mainDirectory, 'worker.js'),
`'use strict'; globalThis.__workerReady = true\n`,
'utf8'
)
const report = obfuscatePackageMain({ stageDirectory, reportPath })
const verified = verifyPackageMainObfuscation({ stageDirectory, reportPath })
const stagedSource = await readFile(path.join(mainDirectory, 'index.js'), 'utf8')
expect(report.status).toBe('passed')
expect(verified.packageVersion).toBe('1.2.3')
expect(verified.files.map((item: { path: string }) => item.path)).toEqual([
'out/main/index.js',
'out/main/worker.js'
])
expect(verified.totals.growthBytes).toBeLessThanOrEqual(MAX_MAIN_OBFUSCATION_GROWTH_BYTES)
for (const marker of PROTECTED_MAIN_PLAINTEXT_MARKERS) {
expect(stagedSource).not.toContain(marker.value)
}
await writeFile(path.join(mainDirectory, 'index.js'), `${stagedSource}\n// tampered\n`, 'utf8')
expect(() => verifyPackageMainObfuscation({ stageDirectory, reportPath })).toThrow(
'hash mismatch'
)
})
it('runs only after dependency collection and keeps packaged-app protection auditing', async () => {
const packageJson = JSON.parse(await readFile(path.resolve('package.json'), 'utf8'))
const buildScript = String(packageJson.scripts.build || '')
const prepareIndex = buildScript.indexOf('prepare-package-stage.mjs')
const obfuscateIndex = buildScript.indexOf('obfuscate-package-main.mjs')
const verifyIndex = buildScript.indexOf('verify-package-main-obfuscation.mjs')
const auditIndex = buildScript.lastIndexOf('audit-package-stage.mjs')
const packagedAudit = await readFile(path.resolve('scripts/audit-packaged-app.mjs'), 'utf8')
expect(prepareIndex).toBeGreaterThanOrEqual(0)
expect(obfuscateIndex).toBeGreaterThan(prepareIndex)
expect(verifyIndex).toBeGreaterThan(obfuscateIndex)
expect(auditIndex).toBeGreaterThan(verifyIndex)
expect(packagedAudit).toContain('assertProtectedMainPlaintextAbsent')
})
})