From 8d9076f60c83854de5ceda175bb051b1063de898 Mon Sep 17 00:00:00 2001 From: trandinhnamuet Date: Wed, 15 Jul 2026 08:26:20 +0000 Subject: [PATCH] =?UTF-8?q?Phase=205=20Iteration=201:=20B=E1=BA=A3o=20m?= =?UTF-8?q?=E1=BA=ADt=20=E2=80=94=20m=C3=A3=20ho=C3=A1=20+=20quy=E1=BB=81n?= =?UTF-8?q?=20h=E1=BA=A1n,=20g=E1=BB=A1=20m=E1=BA=ADt=20kh=E1=BA=A9u,=20re?= =?UTF-8?q?daction=20TH=E1=BA=ACT,=20xo=C3=A1=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine: - qpdf.rs: Permissions (in/sửa/copy/chú thích) map sang cờ AES-256 của qpdf; encrypt_with_password_perms (API cũ giữ tương thích); decrypt_remove_password (--password --decrypt, sai mật khẩu trả lỗi không ghi file rác) - redact.rs MỚI — redaction thật, nghiêng về xoá thừa (an toàn): text/path/shading/form giao vùng → xoá nguyên object; ảnh giao vùng → đọc pixel gốc qua get_raw_image, BÔI ĐEN đúng phần giao trong dữ liệu ảnh rồi thay ảnh (không đọc được → xoá cả ảnh); vẽ khối đen phủ vùng - meta.rs: strip_metadata — xoá /Info trailer + XMP /Metadata catalog, xoá cả tham chiếu lẫn object đích (lopdf; file trailer phi chuẩn qua qpdf trước) Tauri + UI (thanh 🔒 Bảo mật): - Commands: redact_apply (nhiều trang, chain file tạm), security_encrypt, security_decrypt, security_strip_metadata - Redact 2 bước như Foxit: tool quét vùng (khối đen mờ viền đỏ, bấm để bỏ, giữ tool quét nhiều vùng) → Áp dụng (n) → lưu file mới - Dialog Đặt mật khẩu (user/confirm/owner + 4 checkbox quyền), Gỡ mật khẩu, Xoá metadata — modal chuyên nghiệp, không prompt/alert Test (6 mới, tổng 58/58 ngoài qpdf-fixture): - redact: chuỗi mật biến mất khỏi extract + tâm vùng render đen; đọc lại RAW image từ file kết quả — nửa redact đen trong chính dữ liệu ảnh - encrypt+perms round-trip + qpdf --show-encryption xác nhận cấm in; decrypt gỡ mật khẩu, mật khẩu sai fail; metadata biến mất khỏi BYTES file - qpdf_safety: 5/7 xanh trên Linux; 2 test repair fixture corrupt-truncated fail do khác biệt qpdf 11.9 Linux (bản Windows dự án xử lý được) — ghi chú trong docs/15, cần xác nhận lại trên máy Windows Docs: 15-phase5-summary (kèm kế hoạch Iteration 2: chữ ký số PAdES, lưu tối ưu, redact tỉa ký tự), 16-phase5-user-tests (S1–S12), README, roadmap, 14. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk --- README.md | 7 +- app/src-tauri/src/main.rs | 107 +++++++++++ app/src/index.html | 14 ++ app/src/main.js | 185 ++++++++++++++++++- app/src/styles.css | 12 ++ crates/ff-engine/src/lib.rs | 9 +- crates/ff-engine/src/meta.rs | 37 +++- crates/ff-engine/src/qpdf.rs | 62 ++++++- crates/ff-engine/src/redact.rs | 159 ++++++++++++++++ crates/ff-engine/tests/redact_roundtrip.rs | 152 +++++++++++++++ crates/ff-engine/tests/security_roundtrip.rs | 124 +++++++++++++ docs/03-roadmap.md | 2 +- docs/14-foxit-gap-analysis.md | 7 +- docs/15-phase5-summary.md | 80 ++++++++ docs/16-phase5-user-tests.md | 36 ++++ 15 files changed, 978 insertions(+), 15 deletions(-) create mode 100644 crates/ff-engine/src/redact.rs create mode 100644 crates/ff-engine/tests/redact_roundtrip.rs create mode 100644 crates/ff-engine/tests/security_roundtrip.rs create mode 100644 docs/15-phase5-summary.md create mode 100644 docs/16-phase5-user-tests.md diff --git a/README.md b/README.md index 308a232..62bf781 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,12 @@ - [x] **Iteration 3 — Reflow đoạn "như Word"**: double-click đoạn nhiều dòng → sửa cả đoạn, tự bẻ dòng theo bề rộng khối (đo hmtx bằng ttf-parser), giữ font (nhúng lại bytes gốc / font chuẩn base-14 / cùng họ), giữ nhịp baseline; Enter = ngắt cứng - [ ] Follow-up: reflow v2 (justify, khối xoay, đa font), xoay/lật/clip, viền/opacity/căn lề, convert text→path — xem [docs/12-phase4-summary.md](docs/12-phase4-summary.md) - **Checklist test cho bạn**: [docs/13-phase4-user-tests.md](docs/13-phase4-user-tests.md) (mới: E17–E30) -- [ ] **Phase 5 — Bảo mật & Chữ ký** (kế tiếp) — xem [docs/03-roadmap.md](docs/03-roadmap.md) +- [x] **Phase 5 — Bảo mật** ✅ Iteration 1 hoàn tất (58/58 test engine xanh) + - [x] Engine: mã hoá AES-256 + quyền hạn (in/sửa/copy/chú thích), gỡ mật khẩu (`qpdf.rs`); **redaction THẬT** (`redact.rs`: xoá object, bôi đen pixel ảnh trong chính dữ liệu ảnh — test kiểm bytes/pixel/extract); xoá metadata /Info + XMP (`meta.rs`) + - [x] UI thanh "🔒 Bảo mật": đánh dấu redact 2 bước như Foxit (quét vùng → Áp dụng), dialog Đặt/Gỡ mật khẩu + checkbox quyền, Xoá metadata + - [ ] Iteration 2: chữ ký số PAdES + xác thực, lưu tối ưu (subset font + dọn rác), redact tỉa theo ký tự — xem [docs/15-phase5-summary.md](docs/15-phase5-summary.md) + - **Checklist test cho bạn**: [docs/16-phase5-user-tests.md](docs/16-phase5-user-tests.md) +- [ ] **Phase 6 — Form (AcroForm)** (kế tiếp) — xem [docs/03-roadmap.md](docs/03-roadmap.md) Build & chạy: xem [docs/05-dev-setup.md](docs/05-dev-setup.md). diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs index 6b9a6a1..351310b 100644 --- a/app/src-tauri/src/main.rs +++ b/app/src-tauri/src/main.rs @@ -836,6 +836,109 @@ fn edit_preview(input: String, page: u16, ops: Vec, width: u32, passw result } +// ---------- Phase 5: Bảo mật ---------- + +/// Vùng redact của 1 trang: rects theo [left, bottom, right, top] (điểm PDF). +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct RedactPageDto { + page: u16, + rects: Vec<[f32; 4]>, +} + +/// Redact THẬT nhiều trang: áp tuần tự qua file tạm, trả tổng số object đã +/// xoá/bôi đen. +#[tauri::command] +fn redact_apply( + input: String, + areas: Vec, + output: String, + password: Option, +) -> Result { + if areas.is_empty() { + return Err("chưa đánh dấu vùng redact nào".into()); + } + let pdfium = pdfium()?; + let mut total = 0usize; + let mut cur = std::path::PathBuf::from(&input); + let mut temps: Vec = Vec::new(); + let n = areas.len(); + for (i, a) in areas.iter().enumerate() { + let rects: Vec = a + .rects + .iter() + .map(|r| ff_engine::Rect { left: r[0], bottom: r[1], right: r[2], top: r[3] }) + .collect(); + let dst = if i + 1 == n { + std::path::PathBuf::from(&output) + } else { + std::env::temp_dir().join(format!("ff_redact_{}_{}.pdf", std::process::id(), i)) + }; + total += ff_engine::redact_areas(&pdfium, &cur, a.page, &rects, &dst, password.as_deref()) + .map_err(|e| e.to_string())?; + if i > 0 { + temps.push(cur.clone()); + } + cur = dst; + } + for t in temps { + let _ = std::fs::remove_file(t); + } + Ok(total) +} + +/// Đặt mật khẩu + quyền hạn (AES-256 qua qpdf). +#[tauri::command] +#[allow(clippy::too_many_arguments)] +fn security_encrypt( + input: String, + output: String, + user_password: String, + owner_password: String, + allow_print: bool, + allow_modify: bool, + allow_extract: bool, + allow_annotate: bool, +) -> Result<(), String> { + ff_engine::encrypt_with_password_perms( + std::path::Path::new(&input), + std::path::Path::new(&output), + &user_password, + &owner_password, + ff_engine::Permissions { allow_print, allow_modify, allow_extract, allow_annotate }, + ) + .map_err(|e| e.to_string()) +} + +/// Gỡ mật khẩu (cần đúng password hiện tại). +#[tauri::command] +fn security_decrypt(input: String, password: String, output: String) -> Result<(), String> { + ff_engine::decrypt_remove_password( + std::path::Path::new(&input), + &password, + std::path::Path::new(&output), + ) + .map_err(|e| e.to_string()) +} + +/// Xoá metadata nhận dạng (/Info + XMP). File lạ (trailer phi chuẩn) được +/// chuẩn hoá qua qpdf trước khi lopdf xử lý. +#[tauri::command] +fn security_strip_metadata(input: String, output: String) -> Result<(), String> { + let inp = std::path::Path::new(&input); + let out = std::path::Path::new(&output); + match ff_engine::strip_metadata(inp, out) { + Ok(()) => Ok(()), + Err(_) => { + let norm = std::env::temp_dir().join(format!("ff_meta_norm_{}.pdf", std::process::id())); + ff_engine::repair(inp, &norm).map_err(|e| e.to_string())?; + let r = ff_engine::strip_metadata(&norm, out).map_err(|e| e.to_string()); + let _ = std::fs::remove_file(&norm); + r + } + } +} + /// Dọn các file làm việc tạm của chế độ sửa (undo stack). Chỉ xoá file có tên /// `ff_edit_*` nằm đúng trong thư mục temp — không bao giờ đụng file người dùng. #[tauri::command] @@ -896,6 +999,10 @@ fn main() { edit_apply_to_temp, edit_preview, edit_cleanup, + redact_apply, + security_encrypt, + security_decrypt, + security_strip_metadata, pick_image ]) .run(tauri::generate_context!()) diff --git a/app/src/index.html b/app/src/index.html index 5e77646..1785544 100644 --- a/app/src/index.html +++ b/app/src/index.html @@ -36,6 +36,7 @@ +
@@ -102,6 +103,19 @@ + +