承上文,本篇看看「安心出行」所收集的資料及「加密」方式。
程式紀錄了什麼資料
Check in
在用戶以程式掃描 QR Code 後,程式會讀取及驗証 QR Code 內容,以記錄用戶到訪地點。與的士車牌的做法不同,為減少上網核實或讀取地點資料引起私隱問題, QR Code 的設計是在 QR Code 內包含所有要紀錄的地點資料,以及核實用的 hash 編碼。
例如報載「良田體育館」的 QR Code,用普通掃描程式即可讀到以下編碼:
HKEN:0gxr8l5A6eyJtZXRhZGF0YSI6bnVsbCwibmFtZVpoIjoi6Imv55Sw6auU6IKy6aSoIiwibmFtZUVuIjoiTGV1bmcgVGluIFNwb3J0cyBDZW50cmUiLCJ0eXBlIjoiU1BPUlRTQ1VMVFVSQUxSRUNSRUFUSU9OQUwiLCJoYXNoIjoiZTczODE4YjFjN2U4MDUzM2E4MjI3NjE5NzZhMmMwNTJmMWUwNTQzZTQ2ODM0MDk1ODZmZDg5OTRhM2MxZmU3NiJ9
當中開始的 HKEN:
為「安心出行」編碼固定的前綴,接下來的一位 0
標示此為場地編碼,再之後的 8 位 gxr8l5A6
是地方 ID,之後剩下的(eyJtZ....3NiJ9
)全部是經 base64 encode 的 JSON 地方資料,經任何 base64 decoder 即可還原為
{"metadata":null, "nameZh":"良田體育館", "nameEn":"Leung Tin Sports Centre", "type":"SPORTSCULTURALRECREATIONAL", "hash":"e73818b1c7e80533a822761976a2c052f1e0543e4683409586fd8994a3c1fe76"}
解碼後程式會核對 hash code (checkHash
),方法是將上述前綴 HKEN:
,加上地方 ID 及四位的後綴(可在程式內找到,不在此公開),併合為一字串再做一次 SHA256 hash 編碼,將結果以 16 進數列出,便應與 JSON 內的 hash 刎合。
這裏指出一點,由於要離線驗證, hash 方法完全存在於程式中,可輕易找到產生 hash 的方法,沒有保安可言。但 QR Code 卻將整個 hash code 編碼納入,佔去三份之一的長度,令 QR Code 不必要地長。其實只核對頭幾個 byte,如
e73818b1
在實際上是毫無分別的。
在核對後,程式會將有關資料,連同目前時間 (13 位 millisecond Unix timestamp) 作為 Check In 時間,暫時以非加密方式儲存到手機檔案系統 (saveVenueVisit
),當中是使用 React 的 AsyncStorage
。
{ "venueId": "gxr8l5A6", "venueName": { "name_en":"Leung Tin Sports Centre", "name_zh_hk":"良田體育館", "name_zh_cn":"良田體育館", "type":"SPORTSCULTURALRECREATIONAL" }, "scanType": "CITIZEN_CHECK_IN", "meta": [將 venueName 資料 JSON 做 UTF 編碼再用 base64 編碼], "inTs": [Check In 時間] }
如果是 Check in 的士的話 (saveTaxiRecord
),資料為
{ "venueId": "00[車牌]", // left pad to 8 char "venueName": { "taxiNo": [車牌], "type":"TAXI" }, "meta": [將 venueName 資料 JSON 做 UTF 編碼再用 base64 編碼], "inTs": [Check In 時間] }
Check out
用戶離開 Check In 地點,會使用 Check Out 功能,程式從檔案系統取出之前 Check In 資料(及從 AsyncStorage
刪除該份非加密資料),紀錄 Check out 時間(或用戶輸入時間),成為以下資料:
{ "venueId": "gxr8l5A6" 或 "00[車牌]", // left pad to 8 char "groupId": null, // 確診群組? "inTs": [Check In 時間], "outTs": [Check Out 時間], "meta": [將 venueName 資料 JSON 做 UTF 編碼再用 base64 編碼] }
之後程式會將紀錄進行「加密」及儲存。
所謂加密
程式的「加密」方法 (generateCheckInRecord
),是先將以下資料併為一字串(暫稱 dataString
):
- 隨機 8 位字串
- 8 位 venueId
- 8 位 groupId (若沒有,用 00000000 (NO_GROUP) 代替)
- 13 位 inTs (轉為字串)
- 13 位 outTs
- meta (長度不限),即包括上述
venueName
內的地方資料
再用以下方法產生 16-byte Key (128 bit):
- 將
inTs
除以 60000 成為分鐘單位,僅保留整數部份 (keyInterval
) - 將
keyInterval
轉為字串,使用 HKDF (HMAC-based Extract-and-Expand Key Derivation Function)(見 futoin-hkdf 程式庫)產生 Key,當中指定使用 SHA-256 為 HMAC function,及特定的info
参數(可在程式內找到),沒有使用salt
参數。
aesKey = CryptoJS.enc.Hex.parse( hkdf(String(keyInterval), 16 /* 16 bytes */, { salt: null, info: ..., /* info 参數 */ hash: "SHA-256" }).toString('hex'));
然後利用 CryptoJS.AES
以 key 加密資料字串(當中 Initialization vector (IV) 可在程式內找到),再轉為 base64 encoding。
keyData = CryptoJS.AES.encrypt(dataString, aesKey, { iv: ... /* HKCT_IV */ }).ciphertext.toString(CryptoJS.enc.Base64)
之後,將 keyInterval
及加密的 keyData
,連同 upload
flag (false
,表示未上載) ,作為出行記錄數據,一併存到資料庫中的 LocationHistory
table。
以上就是政府不斷強調的加密儲存,例如創科局局長最近(11 月 18 日)在特區人大立法會會議指出,
感染風險通知流動應用程式「安心出行」已通過獨立第三方的保安風險評估及審計和個人私隱影響評估,確保符合《個人資料(私隱)條例》規定。…. 程式採用 AES-256 加密標準保護儲存在用戶手機內的出行記錄數據。[1]
先不論局方將用 128-bit key 的 AES 加密說成 AES-256(因為這不是重點),因為只要知悉有關方法(簡單分析 JavaScript 碼使可得出),任何人若取得 LocationHistory
table 中的資料,即可從紀錄中的 keyInterval
欄位,輕易透過標準的 HKDF ,取得之前加密用的 128-bit AES key ,再輕鬆用 AES-128 還原 keyData
(AES 是所謂對稱密鑰演算法,加密及解密是用同一條 key)。這類保安方式,頂多稱為 security by obscurity。向公眾聲稱資料受 AES-256 加密保護,即使不是 128-bit 充 256-bit,也是極具誤導性。這類似將門鎖鎖匙藏在門前地氈底,門鎖有多堅固,也是徒然。[2]
加密虛招何來?
公平地說,在「安心出行」的各種限制下(不上傳資料,在用戶端背景配對確診資料,為普及及方便使用而不利用一些手機的硬件保安功能,如生物辨識,或要求每次 Check in 或配對前輸入密碼),在用戶手機內的資訊,根本難以作有效加密,因為所有解密所需的資料都在程式當中(當然從陰謀論者角度,在此機制下,政府本身亦可輕易解密)。若手機失竊,有關資料只能和很多其他私隱資料一樣,靠手機本身的保安設定保護。同樣道理,QR Code 的 hash 編碼,不論用幾多 bit ,在這情況下亦難以保證 QR Code 的真確性。
不過,政府制訂招標要求時,往往無視上述限制,既要方便又要保安,寫入了無意義的加密要求。開發商也許明知沒有用,也要照花資源應付這種表面性的要求,只需提高標價,袋袋平安,。而這種門鎖藏在地氈底的保安虛招,竟又可通過政府創科局,以至花公帑外聘的「獨立第三方的保安風險評估及審計」,成果亦可供局長吹噓,各取所需,皆大歡喜。
反正 False Security is Worse than No Security 這類老生常談,在講求 PR 的環境下是不太適用的。
下篇再看看配對及上傳。
[1] 來源:政府新聞網
[2] 退一步說,這個個案,即使沒有取得 keyInterval
,只要知道密鑰產生方法,亦能在短時間解密 keyData
,因為程式不知何故,故意以除數丟掉隨機的秒/毫秒部份,令產生 key 的數據缺乏 entropy,僅以分鐘數產生密鑰。由於每日只有 24 * 60 = 1440 分鐘,即使程式自推出起計,經過一年半載,每項紀錄也只是 brute-force 解密數十萬次,以今天的計算速度可謂易如反掌。