一起加密貨幣冷錢包事件,表面上跟醫療資訊系統毫無關係。一個是管比特幣私鑰,一個是管病歷與檢驗數據,產業、法規、使用者完全不同。但把事件拆到根因那一層,會看到一組讓人不安的共通結構:一個組態設定在很久以前被寫錯,系統長期正常運作、沒有任何錯誤訊息,直到某天被用某種方式驗證出來,才發現所有建立在它之上的保護,從一開始就不成立。這篇要做的事,是把這個結構搬到醫療資訊系統裡,找出三個同樣可能正在靜默失效、卻沒有人在檢查的地方。

前情提要:一個被關掉的亂數產生器

一個組態檢查邏輯的疏漏,讓硬體亂數產生器長期形同虛設,系統卻在每一次開機、每一次交易裡都維持「正常運作」的外觀,直到有人驗證熵值才發現問題已存在超過五年。

2026年7月30日,加密貨幣冷錢包 Coldcard 被揭露這項根因問題,受影響裝置一度在41分鐘內被轉出逾千枚比特幣,後續擴大到數千個地址、金額達數千萬美元等級。完整的根因拆解、時間線與受影響版本,見姊妹作〈如何避免靜默降級:Coldcard冷錢包事件的3個檢查點〉,本文不重述,只取一件事往下走。

醫療資訊系統與研究資料庫之間的資料流示意,一名資訊人員在監看資料傳輸畫面
資料在系統之間流動時看起來一切正常,真正的風險常常藏在沒有人檢查的那個環節(圖/APPI News)

為什麼醫療資訊躲不掉這題

**兩者共享同一種失效結構:一次做錯、長期無感、爆發時無法回溯。**只要系統裡有任何一個環節依賴亂數產生器的品質,而這個環節本身不會因為亂數變差而報錯,就存在同樣的風險。

一次做錯、長期無感、爆發時無法回溯

這句話拆開來看,是三個獨立的條件,缺一不可才會釀成事故。

第一,一次做錯。組態寫錯、依賴關係接錯、驗證邏輯漏掉一個分支,這種錯誤往往發生在系統建置或整合的某個時間點,是一次性的工程決策,不是持續性的操作疏失。

第二,長期無感。錯誤之所以能存活五年,不是因為沒人維護系統,而是因為系統的其他部分完全不知道要去檢查這件事。亂數產生器輸出的每一個位元組看起來都合法、都能通過現有的格式檢查,程式碼繼續往下跑,交易繼續核發,種子繼續生成。沒有任何一層會報錯,因為「格式正確」跟「品質足夠」是兩件不同的事,而系統只檢查了前者。

第三,爆發時無法回溯。等到問題被發現,已經生成的資料(種子、金鑰、識別碼)本身就是問題所在,而不是某個可以打補丁修好的邏輯錯誤。Coinkite 在官方公告裡講得很直接:更新韌體不會改變或修復一個已經生成的種子(updating the firmware does not change or repair an existing seed)。修好程式碼,救不回已經產生的東西。

醫療資訊系統裡,任何依賴亂數產生器的環節,只要同時滿足這三個條件,就是同一種事故的另一個版本。差別只在於,加密貨幣的資產是可以被追蹤轉出的比特幣,醫療系統的資產是病人的身分與資料。

「韌體更新不能修復已產生的種子」的醫療對應版

把這句話直接翻譯到醫療資訊系統的語境,會變成:事後補上的檢核邏輯,救不回已經發出去的識別碼。

假設一套去識別化(de-identification,把可直接識別病人身分的欄位移除或置換,改用代號處理個人資料的技術)流程,用一個品質不足的亂數產生器產生病人的假名或研究用識別碼,已經跑了三年、產生了數萬筆資料。某天工程團隊發現這個產生器的熵不足、輸出可被預測,於是立刻修好程式碼、換上更好的產生器。這個修正能保護的,只有未來新產生的識別碼。已經發出去、已經流通在研究資料集裡、已經被合作單位存取過的那數萬筆識別碼,不會因為程式碼修好而變得不可預測。它們已經是既定事實,風險已經發生,只是還沒有人去驗證、去利用它。

這就是為什麼這題躲不掉:醫療資訊系統裡有太多地方依賴亂數產生器的品質,而且大多數地方,一旦品質不足,系統不會用任何方式提醒你。

檢查點一:去識別化的假名與研究 subject ID

如果產生假名或研究識別碼的亂數產生器可被預測,去識別化實質上形同虛設,但整條資料交付流程會完全正常跑完,不會有任何一層報錯。

去識別化的核心邏輯,是把「這個人是誰」跟「這筆資料屬於誰」之間的直接連結切斷,改用一個看似無意義的代號(假名、subject ID)去標記資料。這個代號要能發揮保護作用,前提是它必須跟原始身分之間沒有可被推導的關係,也就是說,代號本身不能被猜出來。

問題在於,「代號怎麼產生」這件事,在多數流程文件裡幾乎不被當成安全設計的一部分。它常常只是系統裡一段不起眼的程式碼,呼叫某個亂數函式、加上流水號、格式化成固定長度的字串。審查的重點會放在「這個代號有沒有被正確套用到每一筆資料」「對應表有沒有被適當隔離存放」,卻很少有人去問:產生這個代號的亂數,品質夠不夠。

如果這個亂數產生器的熵不足,例如受到系統開機時間、行程序號、或某個弱種子影響,使得輸出的代號在理論上可以被縮小猜測範圍,去識別化就會出現一個沒有被寫進任何流程文件的破口:代號本身變得可預測或可被逆推,資料被重新識別(re-identification,把已去識別化的資料重新對應回真實個人身分)的風險就此存在,而且這個風險不會因為流程「照規定跑完」而消失。

更關鍵的是,這個破口不會在任何一個環節被攔下來。資料照樣被去識別化、照樣通過品質檢查、照樣打包交付給研究單位或合作夥伴。所有人都會認為這批資料是安全的,因為系統的每一步都「正常運作」。唯一能發現問題的方式,是有人主動去檢驗這個亂數產生器本身的統計特性,而這件事通常不在任何人的待辦清單上,直到出事。

這正是「一次做錯、長期無感、爆發時無法回溯」在醫療資訊系統裡的第一個具體樣貌:去識別化流程本身沒有錯,錯的是流程裡一個被視為理所當然的元件。

檢查點二:SMART on FHIR 的 state 與 PKCE code_verifier

PKCE 機制的全部安全性,建立在 code_verifier 這串亂數字串不可被預測這一個前提上;亂數產生器出問題,授權流程外觀依然完整,依然核發看似正常的存取權杖。

FHIR(Fast Healthcare Interoperability Resources,一套由 HL7 International 制定的醫療資訊交換標準,讓不同醫療系統之間的資料能夠互通)是目前醫療系統資料交換最主流的標準之一。SMART on FHIR(Substitutable Medical Applications, Reusable Technologies,建構在 FHIR 之上的應用整合框架,讓第三方 App 能夠安全嵌入醫療系統、存取病患資料)則負責處理第三方應用程式該如何取得授權、存取哪些範圍的病患資料。這套授權流程,建立在 OAuth 2.0 之上,而 SMART App Launch 規範明確要求所有 SMART 應用程式都必須支援 PKCE(Proof Key for Code Exchange,一種防止授權碼被攔截或竄改的擴充機制),而且伺服器端只能接受 S256 這種雜湊過的驗證方式,不得接受未加密的 plain 方法。

PKCE 的運作邏輯很簡單,但每一步都仰賴同一個前提。應用程式先產生一個高熵的隨機字串,稱為 code_verifier,再對它做一次 SHA-256 雜湊並轉成 URL 安全格式,產生 code_challenge。應用程式在發起授權請求時,把 code_challenge 交給授權伺服器;等到要用授權碼換取存取權杖時,才把原始的 code_verifier 一併送出,伺服器驗證這兩者是否對得上,對得上才核發權杖。

定義 PKCE 機制的 IETF 標準文件 RFC 7636 明確指出,這整個機制的安全模型,仰賴 code_verifier 不會被攻擊者學到或猜到這件事,遵守此原則至關重要(the security model relies on the fact that the code verifier is not learned or guessed by the attacker)。任何違背這個前提的做法,都會讓機制形同虛設。

SMART on FHIR 使用 PKCE 的授權流程示意圖,標示 code_verifier 產生、雜湊為 code_challenge、送出授權請求、換取存取權杖四個步驟
PKCE 的安全性建立在一個前提上,這串亂數字串必須無法被猜到

問題就出在這裡:code_verifier 的產生,同樣仰賴系統裡的亂數產生器。如果這個產生器的熵不足,理論上應該有 256 位元強度的隨機字串,可能實際上只落在一個小得多、能被窮舉或縮小猜測範圍的空間裡。一旦攻擊者能夠猜出或推導出 code_verifier,PKCE 原本要防禦的授權碼攔截攻擊,防線就等於不存在。

而這裡最不容易被察覺的地方,跟去識別化那個檢查點一模一樣:**授權流程本身不會因為亂數變差而失敗。**伺服器收到授權請求、核發授權碼、驗證 code_verifier 與 code_challenge 是否吻合、核發存取權杖,每一步的邏輯判斷依然成立,回應依然是「授權成功」。從系統日誌、從監控儀表板、從使用者的操作體驗來看,一切正常。真正被破壞的,是這個「正常」背後原本應該存在、卻已經名存實亡的不可預測性。這跟 Coldcard 事件裡種子看起來完整、交易看起來合法,是同一種失效樣貌。

檢查點三:你怎麼知道亂數是好的

沒有任何一個執行期的判斷式能單獨回答「這串亂數夠不夠亂」,因為亂數的品質是統計特性,不是單筆輸出能反映的邏輯正確性,所以需要在開機、建置、審查三個不同時間點各自建立檢查機制。

這是三個檢查點裡最抽象、也最容易被忽略的一個,原因很直接:亂數品質問題無法用一般的程式除錯手法找出來。一般的邏輯錯誤,程式跑出不符預期的結果,馬上就能定位;亂數品質不足,程式跑出的每一個結果都「看起來合法」,格式正確、範圍正確、型別正確,只有把大量輸出放在一起做統計分析,才可能看出可預測的模式。Coldcard 事件的根因,正是一個原本設計良好的檢查邏輯,因為一個邊界條件的疏漏而失效超過五年,期間沒有任何一次呼叫回報錯誤。

  • 亂數品質不足不會讓程式報錯,只會讓「看起來正確」的輸出失去原本該有的不可預測性
  • 去識別化的假名、SMART on FHIR 的 code_verifier,兩者的安全性都完全建立在亂數不可預測這個前提上
  • 三個不同時間點的檢查各自負責不同的失效模式:開機自測抓硬體異常、熵源健康檢查抓長期退化、建置組態審查抓一次性錯誤配置

那麼,實務上該怎麼做?工程上的回應通常分成三個層次,各自對應不同時間點會發生的失效模式。

  1. 開機自測(power-on self-test):系統啟動時,對亂數產生器的輸出做基本統計檢驗(如連續性、分佈均勻性的初步檢查),異常就阻擋系統進入正常運作狀態,而不是讓它帶著問題默默啟動。
  2. 熵源健康檢查:針對硬體熵源(TRNG,true random number generator,利用實體物理程序產生亂數的硬體元件)建立持續性的健康監控,定期抽樣驗證輸出的統計特性,而不是只在裝置出廠時測試一次就視為終身有效。
  3. 建置組態納入安全審查範圍:把「亂數產生器的啟用旗標、後備機制、失敗時的行為」明確寫進系統的安全審查清單,而不是預設它只是一個底層基礎設施細節、交給編譯器與函式庫自行處理。Coldcard 事件的根因,正是一個組態旗標的檢查邏輯從未被納入過任何一輪安全審查。

Coinkite 在官方安全公告中明確指出:更新韌體不會改變或修復一個已經生成的種子(updating the firmware does not change or repair an existing seed)。這句話同樣適用於任何依賴亂數產生器的醫療資訊系統元件:修好程式碼能保護的,只有未來新產生的資料,不能追溯保護已經流通出去的部分。

這三層檢查機制的共通邏輯,是承認一件事:光靠「程式碼審查」跟「功能測試」抓不到亂數品質問題,必須額外建立針對統計特性的驗證機制,而且要在系統的生命週期裡重複執行,不是做一次就算數。

最好:無法回溯的東西要在源頭多花成本

對於一旦發生就無法補救的環節,多花一點成本做源頭的縱深防禦,通常比事後檢測更划算,因為事後檢測發現的往往已經是既成事實。

回到「問題、原因、方法、最好」這個分析順序。問題已經定義清楚:亂數品質不足會讓依賴它的機制形同虛設,而且系統不會主動提醒。原因也已經追到底:多半是一次性的組態錯誤或設計疏漏,長期沒有被任何機制檢查到。方法在上一節已經列出三層:開機自測、熵源健康檢查、建置組態審查。剩下要問的是「最好」的問題,不是哪一種方法功能最多,而是哪一種配置能從根本降低未來重複發生的機率。

對照維度Coldcard 冷錢包事件醫療資訊系統的對應版本
失效元件硬體亂數產生器啟用旗標的檢查邏輯假名/研究識別碼產生器、code_verifier 產生器
錯誤特徵值為零的巨集通過了「是否存在」的檢查弱亂數輸出通過了「格式是否正確」的檢查
系統表現種子照常生成、交易照常核發,無任何錯誤訊息資料照常去識別化、授權照常核發權杖,無任何錯誤訊息
可否回溯已生成的種子無法靠韌體更新修復已發出的識別碼、已核發的權杖無法靠程式修復追溯保護
縱深防禦手段骰子熵、獨立密碼短語(passphrase)開機自測、熵源健康檢查、建置組態安全審查

Coldcard 事件裡真正發揮作用、擋下部分風險的,不是任何一次事後的程式碼修正,而是使用者原本就採用的縱深防禦:額外加入至少五十次獨立骰子擲出貢獻的熵,以及一組強且唯一的密碼短語。這兩層防護的共同特性,是它們不依賴單一元件的品質,即使系統內建的亂數產生器出了問題,額外的熵來源與獨立防線依然能撐住結果。

醫療資訊系統裡對應的做法,同樣不是追求某一個亂數產生器做到完美無缺,而是在無法回溯的環節上,刻意疊加不完全依賴單一機制的防線:去識別化流程可以搭配定期的重新識別風險評估,而不是只信任產生器本身;SMART on FHIR 的實作可以搭配權杖有效期限縮短、異常存取模式監控,讓即使 code_verifier 品質出問題,攻擊者能利用的時間窗口也有限。這些做法的用意,是承認任何單一元件都可能在某個時間點失效,系統設計上要有能吸收這種失效的餘裕,而不是把所有信任押在一個元件品質完美無缺的假設上。

真正決定一套醫療資訊系統經不經得起這種考驗的,往往不是模型或功能夠不夠先進,而是在資料交付、身分保護、授權流程這些看起來「已經正常運作」的環節裡,有沒有人願意回頭問一句:如果這裡依賴的亂數不夠亂,系統會不會告訴我們?正在建置或維運去識別化流程與 SMART on FHIR 授權的讀者,可以直接用本文的三個檢查點,對照自己系統目前的設計。完整的 Coldcard 事件根因拆解與時間線,見姊妹作〈如何避免靜默降級:Coldcard冷錢包事件的3個檢查點〉。

醫療資訊系統亂數品質三層檢查機制資訊圖,並列呈現開機自測、熵源健康檢查、建置組態安全審查三個項目
三層檢查各自守住不同時間點會出現的漏洞,少一層都補不回來

常見問題

Q1: 這篇文章跟加密貨幣冷錢包事件是什麼關係?

本文是姊妹作,延伸 Coldcard 冷錢包亂數產生器事件揭露的根因結構,把「一次做錯、長期無感、爆發時無法回溯」這個失效模式,對應到醫療資訊系統裡去識別化與 SMART on FHIR 授權的實作細節,不重述加密貨幣事件本身的時間線與金額。

Q2: 什麼是去識別化?它跟亂數產生器有什麼關係?

去識別化是把病人的直接身分資訊移除或置換,改用假名或研究識別碼標記資料的技術。這個代號要能保護身分,前提是它不可被預測或推導,而代號通常由亂數產生器產生,所以產生器品質直接決定了去識別化實際能提供多少保護。

Q3: SMART on FHIR 是什麼?為什麼會用到 PKCE?

SMART on FHIR 是建構在 FHIR(醫療資訊交換標準)之上的授權框架,讓第三方應用程式能安全存取病患資料。授權過程建立在 OAuth 2.0 之上,規範要求所有應用程式支援 PKCE 機制,防止授權碼在傳輸過程中被攔截或重放,而 PKCE 的安全性完全仰賴 code_verifier 這串亂數字串不可被猜測。

Q4: 為什麼修好程式碼之後,問題還是沒有解決?

因為亂數品質不足留下的風險,附著在「已經產生的資料」上,不是留在程式邏輯裡。修正程式碼能防止未來繼續產生有問題的識別碼或字串,但無法讓已經發出去、已經被使用過的那些資料重新變得不可預測,這也是官方公告特別強調韌體更新無法修復既有種子的原因。

Q5: 一般的維運團隊該怎麼開始檢查這個問題?

可以從三個層次著手:系統啟動時是否對亂數輸出做過基本統計檢驗、是否有機制持續監控熵源的健康狀態、以及亂數產生器的組態設定是否曾經被納入過任何一輪安全審查。這三個問題如果都沒有明確答案,通常代表這個環節目前完全沒有被檢查過。