---
title: "下載檔加密 — 需求討論稿 (FR-169)"
brand: "Guidant AI · **FR-169** 下載檔加密"
eyebrow: "FR-169 · 需求討論稿 · 2026-10-29（v3 待審）"
h1: "下載檔加密"
subtitle: "下載下來的檔案本身帶密碼；密碼由系統當場產生、畫面只顯示一次；不裝軟體、不寄信；主系統與 Agent 同一套"
lede: "報表、證據包從系統下載到電腦後是**明文**，別人走到這台電腦、雙擊就能看。本案讓每一份下載出去的檔自己帶鎖：**Word、Excel、PDF 用檔案格式內建的開啟密碼**，雙擊會先問密碼，Word、Acrobat、Chrome 都能直接開；**證據包、圖片、log 這類多檔或非文件打成加密 7z**。密碼由系統產生、按下載前在畫面上**只顯示一次**，不存、不寄信，忘了就重新匯出一份。加密邏輯寫成一支**獨立小套件**，主系統與 Agent 各自一行接上，Agent 離線照常可用。決策者已裁：**不做撤銷、不做到期、不做 IRM／DRM**。"
chips: [
  {text: "已定：威脅是「別人坐到我電腦雙擊」、原生密碼不裝軟體、系統產密碼畫面顯示一次、不寄信、獨立小套件、不做撤銷與 IRM、使用者不能選不加密、管理員開關預設開且關閉有警示", kind: ok},
  {text: "待裁：D1～D4、D6～D8（套件名、密碼形態、多檔格式、浮水印、Agent 政策下發、線上看、密碼在哪產）", kind: warn},
  {text: "前作：FR-166／166.1 Agent 主控台與 file.download 權限、FR-133 錯誤追蹤遮罩、FR-145.3 結案報告浮水印", kind: accent},
  {text: "雛形：prototype/index.html（六個畫面）", kind: accent}
]
footer: "FR-169 · 下載檔加密 — 需求討論稿 · 2026-10-29 v3 待審 · 現況來源：api/ 16 支匯出端點、jedi_iam/domain/security_policy.py、core/plugins/remote_agent.py 心跳掛點、jedi-masking 套件形狀 · 取捨全文 docs/analysis/2026-10-29-download-file-encryption-approach.md"
---

## 這案要解決什麼 {#why nav="為什麼"}

::: statgrid
::: {.stat .crit}
[明文]{.v}[16 支匯出端點吐出的檔落到電腦上都沒鎖]{.k}
:::
::: {.stat .crit}
[0 筆]{.v}[下載這件事現在沒有稽核紀錄，外流無法追責]{.k}
:::
::: {.stat .warn}
[2 邊]{.v}[主系統與 Agent 主控台各有下載出口，Agent 可能離線]{.k}
:::
::: {.stat .ok}
[0 套]{.v}[使用者要裝的軟體：Word、Acrobat、Chrome 本來就會問密碼]{.k}
:::
:::

**要擋的只有一件事**：檔案下載到電腦後，別人走到這台電腦雙擊就能開。

**不在這案的**（決策者 2026-10-29 裁示）：檔案離開系統後遠端收回、到期、禁轉傳、追蹤誰轉給誰——那是 IRM／DRM 的領地，要客戶裝 client，決策者原話「先不做撤銷」。螢幕截圖與拍照任何方案都擋不了，不假裝。

> 工研院那種要用解密軟體的我覺得很麻煩，才想說有沒有其他模式。
> [— 決策者原話（2026-10-29）]{.attr}

## 分工概述（30 秒版） {#overview nav="分工概述"}

整案：**一支獨立加密套件 ＋ 一個「密碼只顯示一次」對話框 ＋ 安全政策多一段 ＋ 每次下載寫稽核。主系統與 Agent 同一套。**

| 期 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| **一 先看到效果** | 新套件 `jedi-file-protect`（暫名，D1）：產密碼、PDF／Office／7z 三種加密、浮水印；FE 匯出對話框；先接「稽核計畫匯出」與「結案報告」兩個端點 | 按匯出跳對話框、下載到的 PDF 雙擊要密碼 | DEV 稽核計畫頁按匯出 Word，對話框顯示 16 碼密碼；複製後下載；雙擊檔案 Word 問密碼，貼上才開；按取消不產檔；再按一次匯出密碼不同 |
| **二 全部出口接上** | 主系統其餘 14 支匯出端點；Agent 主控台報告與檔案下載；證據包、診斷包改加密 7z | 任何「匯出／下載」都走同一個對話框 | 翻一遍每個有下載鈕的頁面都跳對話框；拿 7z 用 7-Zip 開要密碼、用 Windows 內建解壓會說不支援（對話框有提醒）；拔 Agent 網路後在主控台下載照常跳對話框 |
| **三 開關、稽核、浮水印** | 安全政策頁多一個「檔案下載保護」開關（預設開）；關閉時寫稽核事件、安全政策頁與系統總覽常駐警示、每筆下載標「未加密」；開關值隨心跳下發 Agent；每次下載寫稽核事件；PDF／Word 蓋下載者浮水印 | 出貨就是全部加密；客戶 IT 真要關有一條正規、有紀錄、被看見的路 | 關掉開關後：匯出不跳對話框直接給檔、安全政策頁與總覽出現紅字「檔案下載保護已關閉」、稽核事件多一筆 SECURITY_POLICY_UPDATED、之後每筆下載事件標「未加密」；開回來全部恢復；Agent 下一次心跳後跟著變；開 PDF 每頁角落有下載者帳號與時間 |

依賴：一先做；二、三可以同時做。**決策者功課**：D1 套件名、D2 密碼形態、D4 浮水印要不要。

## 完整走一次：稽核員下載稽核計畫 {#usecase nav="走一次"}

1. 稽核員在稽核計畫頁按「匯出 Word」。
2. 跳出對話框：「這份檔會加密。密碼只顯示這一次，請先複製再下載。」下方是 `Kq7m-Xw2p-Rt9v-Bn4z`，旁邊「複製」鈕。
3. 按「複製」，再按「下載」。瀏覽器下載 `audit-plan-2026-10-29.docx`。
4. 對話框關閉，密碼從畫面消失。系統寫一筆稽核事件：誰、何時、哪份、已加密。
5. 稽核員把密碼貼進公司的密碼管理器。
6. 下午同事走到這台電腦雙擊那個 docx，Word 跳出密碼框，開不了。
7. 一週後稽核員忘了密碼，回系統再按一次匯出，拿到新檔新密碼。舊檔沒人開得了。

Agent 主控台走同一條，差別是第 4 步的稽核事件先寫在 Agent 本機、連線後跟其他事件一起回主系統。

```{.mermaid cap="圖 1 — 下載一次的時序；密碼不存、不進 log、不出伺服器任何其他通道"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant U as 使用者（瀏覽器）
    participant FE as 前端對話框
    participant BE as 主系統或 Agent
    participant P as jedi-file-protect
    U->>FE: 按「匯出」
    FE->>BE: 問政策（一律／可選／不加密）
    BE-->>FE: 一律加密
    FE->>FE: 在瀏覽器端產 16 碼密碼，顯示一次
    U->>FE: 複製密碼、按「下載」
    FE->>BE: GET /export?format=docx（密碼放 header，不放 URL）
    BE->>BE: 產出原本的 docx
    BE->>P: protect(bytes, 檔名, 密碼, 浮水印資訊)
    P-->>BE: 加密後 bytes、新檔名、是否需 7-Zip
    BE->>BE: 寫稽核事件（不含密碼）
    BE-->>FE: 檔案（Cache-Control: no-store）
    FE-->>U: 瀏覽器下載；對話框關閉，密碼清掉
```

## 已經定案的 {#decided nav="已定案"}

::: {.callout .decided}
**威脅模型：擋「別人坐到我電腦雙擊」，不擋「檔案離開後收回」**

決策者 2026-10-29 裁示先不做撤銷。IRM／DRM／自建 HTML 殼都因此排除。
:::

::: {.callout .decided}
**檔案格式內建的開啟密碼，不裝軟體**

Word、Excel、PDF 的規格本來就定義了加密方式，加密完還是正常的 .docx／.xlsx／.pdf，Word、Acrobat、Chrome、手機閱讀器自己跳密碼框。只有「開啟密碼」算加密；「限制編輯／禁列印」那種權限密碼內容仍是明文、一鍵可拆，不當保護宣稱。
:::

::: {.callout .decided}
**密碼由系統產生、畫面只顯示一次、不存、不寄信**

不要使用者自己想密碼。不存 DB、不進 log、不進錯誤追蹤站；忘了就重新匯出一份新檔新密碼，沒有「重寄」。不寄信的直接好處：Agent 離線照常可用，不必把通知模組搬進 Agent；密碼與檔案天然走不同通路。
:::

::: {.callout .decided}
**使用者永遠不能選「不加密」；管理員有一個開關、預設開、關閉有警示（決策者 2026-10-29 兩輪後定）**

對話框裡只有「複製密碼、下載」或「取消」。安全政策頁是一個開／關，不是三選一，出貨預設開。關閉是明確、有紀錄、被看見的管理員決定：寫稽核事件、安全政策頁與系統總覽常駐紅字「檔案下載保護已關閉」、之後每筆下載事件標「未加密」。為什麼留開關而不是寫死強制：我們自己的 e2e 回歸與教育訓練每天都在下載、客戶若已有 DLP 一落地就加密會變成兩層、匯出的 Excel 要餵別的系統時加密檔開不了——這三種情境要有正規出口，否則使用者會自己繞。反悔條件：上線後客戶普遍一裝就關，改成不可關。
:::

::: {.callout .decided}
**獨立小套件，不放 jedi-file-upload、不塞 jedi-common**

jedi-file-upload 帶整套資料庫與物件儲存相依，Agent 現在不依賴它，且記憶體現產的報表不經過它的簽票層；jedi-common 是 25 支套件的共同底，塞進 pypdf、Office 加密、7z 會讓用不到的套件都多裝。先例是 jedi-masking、jedi-integrity 這種「一件事、零框架相依」的小套件。未來診斷包、資料庫備份下載、授權檔匯出都會用到。
:::

## 待決策 {#decisions nav="待決策"}

::: {.callout .pending}
**D1 套件名稱**

選項：`jedi-file-protect`（出檔前保護，含加密與浮水印）／`jedi-file-crypto`（只講加密）／`jedi-export-guard`。

[**建議**：`jedi-file-protect`。它做的不只加密，浮水印也在裡面；「protect」對到 Office 選單上的「保護文件」，使用者語彙一致。]{.rec}
:::

::: {.callout .pending}
**D2 密碼長什麼樣**

檔案密碼可以離線用顯卡無限次試，所以**不能照登入驗證碼做成六碼數字**（幾分鐘破）。選項：
① 16 碼隨機英數分四段（`Kq7m-Xw2p-Rt9v-Bn4z`），約 95 位元強度；
② 五個隨機單字（`tiger-maple-orbit-canvas-brick`），好唸好抄，約 64 位元，單字表要自備；
③ 20 碼含符號，最強但難抄。

[**建議**：①。分段好對照、純英數不會被 Word 密碼框吃掉符號、強度足夠（每秒十億次試要幾千年）。去掉易混字元 0／O／1／l／I。]{.rec}
:::

::: {.callout .pending}
**D3 多檔與非文件（圖片、log、證據包）用哪種容器**

選項：
① **加密 7z**：檔名清單也加密、AES-256、`py7zr` 純 Python；要裝 7-Zip／Keka 才開得了，Windows 檔案總管與 macOS 內建解壓都不支援；
② AES-256 zip（`pyzipper`）：同樣要裝 7-Zip，且**檔名清單是明文**（「XX公司_滲透報告.pdf」直接外露）；
③ 單張圖片另外轉成一頁加密 PDF，讓 Chrome 雙擊就開。

[**建議**：①，對話框明寫「請用 7-Zip 或 Keka 開啟」。③ 不做：為了少裝一個 7-Zip 多開一條路徑不值得。現有 zip 出口（稽核計畫 zip、前置資料全部下載、Agent 檔案包、診斷包）全改 7z。]{.rec}
:::

::: {.callout .pending}
**D4 浮水印要不要、長什麼樣**

密碼擋被動情境（別人雙擊）；浮水印擋的是「使用者自己把檔傳出去」的追責。選項：
① PDF 與 Word 每頁頁尾小字「下載者 王大明（wang） · 2026-10-29 14:32 · Guidant AI」；
② 斜角大字滿版（銀行對保那種）；
③ 不做，本案只做加密。

[**建議**：①。結案報告已有「草稿」浮水印機制可延伸；滿版斜字會影響可讀性，稽核員會抱怨；Excel 與 7z 不蓋（Excel 沒有頁的概念，蓋了會進資料格）。]{.rec}
:::

::: {.callout .pending}
**D6 開關值怎麼到 Agent**

Agent 主控台也有下載，而且可能離線。選項：
① 開關值掛進心跳回應（照 FR-166 D23 權限下發的模型），Agent 存最後一份，離線沿用；
② Agent 自己一份設定，與主系統無關；
③ Agent 一律加密，不吃開關。

[**建議**：①。一份真相；掛點 `heartbeat_extras` 現成；離線沿用最後一份是 FR-166 已接受的行為。密碼在 Agent 本機瀏覽器端產，稽核事件走 FR-166.1 D6 剛接好的本機 logging 管線，離線先存、連線補送。]{.rec}
:::

::: {.callout .pending}
**D7 「預設線上看、不下載」要不要在這案做**

前面討論提過「多數人只是要看一眼，不需要讓檔案落地」。但要做就是每個匯出頁面加預覽器，範圍是這案的好幾倍。

[**建議**：這案**不做**。只做加密。Agent 主控台已有 HTML 預覽（FR-166），主系統結案報告也已有 PDF 預覽，先維持現狀；以後要做另開 FR。]{.rec}
:::

::: {.callout .pending}
**D8 密碼在哪一端產、怎麼送到後端**

選項：① 瀏覽器端用 `crypto.getRandomValues` 產、放 HTTP header 送到後端加密；② 後端產、回給前端顯示。

[**建議**：①。密碼只在使用者瀏覽器與那一次請求裡出現，後端連「產生」這一步的 log 風險都沒有；放 header 不放 URL（URL 會進 access log、瀏覽器歷史）。**陷阱**：`crypto.getRandomValues` 在 http＋IP 的頁面仍可用（它不像 `randomUUID` 要 secure context），但要實測一次。後端收不到 header 時回 400，不靜默吐明文。]{.rec}
:::

## 現況盤點：沿用什麼、補什麼 {#inventory nav="現況盤點"}

| 元件 | 現況 | 本案動作 |
|---|---|---|
| 匯出端點 | 16 支各自 `send_file`：稽核計畫（docx／pdf／xlsx／zip）、SSP、結案報告、控制項對照、RACI、風險登錄、前置資料 zip、診斷包、AI 呼叫紀錄、Agent 報告與檔案包等 | 各加一行呼叫共用 helper；不改各自的產檔邏輯 |
| 加密能力 | 主系統有 `cryptography`（Fernet 用於密鑰欄位）、`pypdf`、LibreOffice；**沒有** Office 加密、7z | 新套件帶 `pypdf`、`msoffcrypto-tool`、`py7zr` |
| 浮水印 | 結案報告有「草稿」浮水印（docx 範本變數） | 套件提供 PDF 與 docx 蓋頁尾小字；結案報告沿用範本變數 |
| 安全政策 | `system_configs` 群 `RUNTIME_CONFIG`，型別與範圍在 jedi-iam `security_policy.py`；FE 頁四段（兩階段驗證、登入、密碼、權杖） | 加一布林鍵 `FILE_DOWNLOAD_PROTECTION`（預設 true）進 `BOOL_KEYS`；FE 多一個開關＋關閉警示 |
| 心跳下發 | `heartbeat_extras` 已掛弱點庫、工具 schema、LDAP 設定 | 多掛一支帶開關值 |
| 稽核事件 | `audit(logger, name, EventCode, **fields)`；Agent 側 FR-166.1 D6 接同一套 | 新事件碼 `FILE_DOWNLOADED`，欄位：檔名、格式、是否加密（開關關閉時為否）、來源端點 |
| FE 下載 | `useApExportDownload` 等 composable 各自 axios blob 下載；檔名讀 `X-Filename` | 抽一支共用 `useProtectedDownload`：先開對話框產密碼，確認後帶 header 下載；各頁改呼叫它 |
| Agent 主控台 | FR-166.1 D3：下載收 `file.download` 能力點，出貨全鎖 | 只有持有能力點的人會看到對話框；主控台前端接同一支 composable 的 Agent 版 |

## 資料模型草案 {#data nav="資料模型"}

**不新增資料表。** 只有：

- `system_configs`：`RUNTIME_CONFIG` 群多一布林鍵 `FILE_DOWNLOAD_PROTECTION`，預設 true。
- `common/enum/event_code.py`：多一個 `FILE_DOWNLOADED` 事件碼（Agent 側對應一個）。

密碼**不落任何表**。

## 業界怎麼做 {#industry nav="業界做法"}

| 類型 | 代表 | 做法 | 對我們 |
|---|---|---|---|
| 一般 SaaS | Google Drive、M365 | 不加密檔案；靜態加密＋短效連結＋審計 | 底線，我們已有登入守門 |
| 安全傳檔 | Proton Drive、Tresorit | 瀏覽器端對端加密 | 伺服器必須看得到內容才能產報表，不適用 |
| GRC 同業 | Vanta、Drata、AuditBoard | 幾乎不加密匯出檔；靠權限、審計、浮水印、審計員入口線上看 | 我們多做一步 |
| 金融／政府傳檔 | 台灣銀行、健保署 | **Office／PDF 原生密碼＋AES 壓縮檔**，密碼走第二通道 | 本案採用的就是這條，但密碼改成系統產、畫面顯示（銀行用身分證字號是弱做法） |
| 企業 IRM | Purview、Seclore、工研院 | 開檔時向政策伺服器驗證、可撤銷 | 決策者裁不做 |

## 排除的方案 {#rejected nav="排除方案"}

| 方案 | 為什麼不 |
|---|---|
| IRM／DRM（含工研院類、OpenTDF、Purview） | 要客戶裝 client；且擋不住「同一台已登入電腦換人坐」；撤銷不在本案 |
| 自建 HTML 殼（開檔連回伺服器要鑰匙） | 只有做撤銷才有存在理由；不做撤銷就只是比原生密碼多一層自建格式 |
| 使用者自訂密碼 | 決策者不要；會用生日、統編等可預測密碼 |
| 寄信或簡訊送密碼 | Agent 離線寄不出；多一條留紀錄的通路；畫面顯示已足夠 |
| 加密 zip 當唯一手段 | 解開後一份明文留在電腦，下次有人坐過來一樣開；檔名明文 |
| 自製加密格式（.enc） | 沒人開得了，業界無人這樣做 |
| 放 jedi-file-upload 或 jedi-common | 見「已定案」第五條 |
| 「使用者可選」模式、對話框「不加密下載」 | 決策者裁不要；會變成習慣性點掉 |
| 寫死強制、沒有管理員開關 | e2e／教育訓練、客戶既有 DLP、匯出餵別的系統三種情境沒有正規出口，使用者會自己繞；開關預設開＋關閉有警示已壓掉「被關掉」的風險 |

## 不做什麼 {#out nav="不做什麼"}

- 撤銷、到期、追蹤轉傳、禁截圖。
- 使用者自訂密碼、重寄密碼、個人固定匯出密鑰。
- 線上預覽器改造（D7 建議不做）。
- 上傳進系統的檔靜態加密（物件儲存那層，另案）。
- 匯入功能不動：加密檔上傳進系統要先解密，那是另一個需求。

## 先看哪裡 {#next nav="下一步"}

1. 點 `prototype/index.html`：①匯出對話框（含 7z 提醒）②安全政策開關與關閉後的警示 ③Agent 主控台離線時下載。
2. 裁 D1～D4、D6～D8。
3. 拍板後落 design.md、拆卡。預估跨 jedi-packages（新套件）、BE（16 支端點、政策鍵、心跳、事件碼、helper）、FE（composable、對話框、安全政策段、i18n）、evidence-agent（主控台接套件與 composable）四個 repo。
