Resecta is a free, open-source iPhone app that redacts PDFs on-device and then checks the output. It runs on iOS 26 or later, collects no data, and makes no network requests of its own.
A black box drawn over text can leave the text underneath readable. Resecta is designed to remove the text in the regions you mark, not just cover it, and then checks the redacted PDF before you share it. How redaction has failed in practice, with sources: Redaction by architecture.
App Store: Resecta on the App Store · Website: resecta.app
License: Apache License 2.0
Latest release: 1.3.1 — see the CHANGELOG.
Resecta is a focused PDF redaction tool that operates entirely on your device, for anyone who needs to remove sensitive regions from PDFs before sharing them. Marked regions are overwritten in the page's pixels — every page is rasterized and the file is rebuilt from those rasters — rather than covered with a box; a verification pass, on by default, then re-opens the output and checks it. The app makes no network requests of its own, does not create accounts, and does not collect analytics or telemetry.
For reviewers. The design reasoning and the check behind each claim are in ENGINEERING.md (start at its "Where to start reading"); what the app defends and what it does not is THREAT-MODEL.md; the test tree is described under Testing below.
The core workflow is:
Import → View → Mark → Apply → Verify → Export
- Import a PDF from Files, or JPEG, PNG and HEIC images from Files or Photos, one page each; or open the bundled sample document. A password-protected PDF opens with its password; the redacted copy is not password-protected.
- View pages and navigate the document.
- Mark regions for redaction by drawing rectangles, by marking a whole page from the page bar, or by selecting and applying results from Search (text, pattern, and multi-term matching) or Scan (on-device text detection).
- Apply redaction. Every page is rasterized, marked or not — vector text and images are converted into flat bitmap data, marked regions are overwritten in those pixels, and the rebuilt file is designed to leave the original text layer out. Source metadata (author, editing history and the rest) is stripped; the rebuilt file carries a producer tag replaced with a fixed value, timestamps rewritten to a fixed date and a file identifier derived from the file's own bytes, so the export is not metadata-free — see
PRIVACY.md. - Verify. By default, a multi-layer verification engine then scans the output for residual content using text extraction, OCR, binary string search across multiple encodings, structural analysis, and metadata checks, re-runs each search you applied on the output, checks on the original page that each box covers the whole match it came from, and runs a detection sweep on the output. The results list groups each check by its outcome and counts only passing checks as passed.
- Export via the system share sheet.
- Secure Rasterization — produces image-only output and is the simplest approach for high-sensitivity documents. Verification runs as a 7-layer check.
- Searchable Redaction — preserves non-redacted text for selectability and search, using a fresh monospace font designed to reduce the glyph-positioning side channels identified in academic research. Verification runs as a 12-layer check (the five additional checks cover the preserved-text layer). If any unmarked text on a page shows no ink or almost none — white or near-white on white, invisible or nearly transparent, or under an opaque box drawn into the page — that page is exported as an image only. The stated limits: text covered by a picture in the page, text under a bar that is part of a scanned picture, and text that shows faintly through a box that is not fully opaque can still enter the text layer (
KNOWN_ISSUES.mdKI-18).
Both modes share the same pixel-destruction core. Settings holds the default mode and each document can override it; Paranoid Mode pins Secure Rasterization.
The end-to-end pipeline. The mode shapes the applied output and selects the verification pass:
flowchart LR
A[Import PDF or image] --> B[View]
B --> C[Mark: search, scan, or draw regions]
C --> E{Mode}
E -->|Secure Rasterization| D1[Apply: rasterize / fill / strip document metadata]
E -->|Searchable Redaction| D2[Apply: the same, plus a rebuilt text layer]
D1 --> F[Verify - 7-layer pass]
D2 --> G[Verify - 12-layer pass]
F --> H[Export]
G --> H[Export]
All stages run on-device; the redaction pipeline makes no network calls. Verification is a check, not a substitute for human review of the output.
- No network requests of its own: the source contains no
URLSessionorNWConnectionusage (the check is in How we verify these claims). - No accounts, no analytics, no telemetry, no server-side components.
- On-device detection combines regex patterns and checksum validators, bundled gazetteers and context scoring,
NLTaggernamed-entity recognition for names, and Vision for OCR. - The sample document is bundled in the app — not downloaded.
Users are responsible for verifying that redaction output meets their specific requirements before sharing documents.
Resecta is designed to address specific risks that arise when sharing redacted documents. The list below names what is in scope and what is not; the full model — assets, adversaries, trust boundaries, accepted risks and a dated security-posture table — is THREAT-MODEL.md.
In scope:
- Residual text after redaction. Both modes are designed to rasterize every page and leave the original text layer out of the rebuilt file; Searchable Redaction adds a rebuilt layer holding only unmarked text. The multi-layer verification pass then checks the output for redacted text that remains.
- Document metadata leakage. Author, editing history, tagged structure, and other source metadata fields are stripped from exported documents. The rebuilt file does carry a producer tag — replaced with a fixed value ("Resecta") that identifies neither the operating system version nor the build — the writer's creation/modification timestamps, rewritten to a fixed date, and a file identifier derived from the file's own bytes; it is not metadata-free. See
PRIVACY.md. - Font-positioning side channels (Searchable Redaction). The rebuilt text layer is drawn in a fresh monospace font on a uniform per-line pitch, designed to reduce the glyph-positioning side channels identified in academic research on PDFs that pair a page image with a hidden text layer.
Out of scope:
- Compromised device. Resecta runs on the user's device. If the device is compromised, the threat model assumes the attacker already has access to whatever the user is redacting.
- Exploits of Apple's PDF and image frameworks. Resecta does not sandbox them; a document crafted to exploit them is outside the scope of what this tool addresses.
- Network adversaries. Resecta makes no network requests of its own.
- Human review of output. Users are responsible for visually reviewing redacted documents before sharing. The verification pass is a check, not a substitute for review.
Each claim in the map below is paired with a mechanical check in this repo. The map is the short version; the depth, the design reasoning, and the honest limits of each check are in ENGINEERING.md. Paths in the map that do not start with Scripts/ or Tests/ are inside the engine package's sources or tests.
| Claim | Check |
|---|---|
| Marked regions are destroyed, not covered | Per-region pixel readback after every fill (Pipeline/PageRasterizer.swift) — one wrong pixel fails the render, the page is re-rendered once at the lowest resolution tier, and a second failure fails the export; the classic annotation-over-text attacks are constructed and destroyed in SecurityTests/FakeRedactionTests.swift |
| The exported file is re-opened and re-checked | The 7/12-layer verification pass re-opens the output and scans its text, OCR of its rendered pages, the raw bytes across seven encodings (outside stream data) plus each page's decoded text, its structure and its metadata, re-runs each applied search on the output, and runs the app's structured detectors on the output to report what remains — names and addresses are not swept (Verification/VerificationEngine.swift) |
| Placement survives rotated pages | A rotation × crop-box-origin test matrix positions its regions with a transform written independently of the production code (SecurityTests/RotatedPageCoordinateTests.swift) |
| No network requests of its own | A source grep for networking symbols returns no code references (the sole hit is a comment); the pre-commit hook rejects those symbols on added Swift source lines (Scripts/audit-lint.sh) |
| The app's own copy is linted for overclaiming vocabulary | A banned-vocabulary lint walks the legal string catalog, the app's string literals, the verification checks' result copy and the shipping docs (Tests/ResectaAppTests/LegalPhraseLintTests.swift, Scripts/claims-lint.sh) |
| Bundled detection data is what the key holder signed | An Ed25519 signature over the gazetteer manifest is verified the first time the detectors are needed, then each listed file's size and SHA-256 digest against its manifest entry; a failure withholds the gated loaders and degrades detection, with a banner in the Scan interface (Detection/GazetteerLoader.swift, Detection/Gazetteer/AssetIntegrity.swift) |
App source lives in Sources/ResectaApp/. The redaction engine is an SPM package at Packages/RedactionEngine/, consumed through standard SwiftPM (the package also builds on macOS for host-side tests; there is no macOS app).
-
Clone and install hooks.
git clone https://github.com/Merlin1A/resecta.git cd resecta ./Scripts/install-hooks.shThe pre-commit hook is a symlink to
Scripts/audit-lint.sh, which runs the mechanical checks described inCONTRIBUTING.md; the pre-push hook runs the test schemes the change touches. Never bypass them with--no-verify. -
Generate the Xcode project.
./regenerate.sh
ResectaApp.xcodeprojis regenerated fromproject.ymlvia XcodeGen. Do not editproject.pbxprojby hand. -
Open in Xcode and build.
open ResectaApp.xcodeproj
Select the ResectaApp scheme and an iPhone 17 simulator. Build with
⌘B. The app target uses iOS 26 and the Swift 6 language mode (tools version 6.2); theRedactionEngineSPM package builds in the same mode under strict concurrency checking.
Tests and gates: CONTRIBUTING.md.
The test tree is larger than the source tree: roughly 71,000 lines of Swift source to roughly 122,000 lines of test code, about 1.7×. Counted from the current tree:
- Engine package (
Packages/RedactionEngine/Tests) — 2,288 Swift Testing@Testfunctions across 316 suites: the pipeline and rasterization, the verification layers, the security suites (fake redaction, pixel destruction, rotated-page coordinates, adversarial verification), search, detection, and the corpus measurement harnesses. - App target (
Tests/ResectaAppTests) — 2,156@Testfunctions across 339 suites: the pipeline state machine, cancellation and restart races, view-level predicates, and the honesty guards that keep the docs and UI copy accurate. - UI / end-to-end (
Tests/ResectaAppUITests) — 96 XCUITest methods that drive the built app on a simulator (run from Xcode; the batched runner and CI build them without running them): the first-launch legal gate, search-to-redaction flows, the search re-check on the results screen, and the editor's handling of links inside a document.
Together the suites carry about 12,400 #expect/#require assertions. Beyond ordinary coverage, they pin the things this project cannot afford to regress: the named fake-redaction attacks (text under an opaque annotation must be destroyed at the text-layer, byte, and annotation level), the rotation × geometry placement matrix, fill-readback edge cases, cancellation and restart races, and the app's own copy — overclaiming is treated as a defect class with its own red tests. The reasoning behind that structure is in ENGINEERING.md.
The commands that run both suites locally, and what the batched runner reports, are in CONTRIBUTING.md.
On GitHub Actions. Every pull request runs two required checks: pr-gate (ci.yml), which builds the app, its unit- and UI-test bundles and the engine's test bundle without running them and runs the lint, count and hash checks, and sim-suite (ResectaApp), which runs the app's unit suite on a hosted simulator. Both schemes (app and engine) run weekly and on release tags; the engine suite runs locally on a simulator before any push that touches the engine package, through the pre-push hook or an explicit batched run (the local-suites status on each pushed commit names the schemes it ran). The full gate list is in CONTRIBUTING.md.
- If Resecta is useful to you, starring the repo helps others discover it.
- Read
CONTRIBUTING.mdfor the gates every change passes and the changes that need an agreed plan before they land. - Open bugs and implementation constraints are tracked in
KNOWN_ISSUES.md. - Security issues: please use
SECURITY.md— do not file public issues for vulnerabilities. - Questions and support: support@resecta.app. Bugs: GitHub issues; please don't attach real documents.
Licensed under the Apache License, Version 2.0. See LICENSE for the full text.