Korat
寫給認真的讀者

帳號、訪問權限,以及我們還做不到的事

這一頁是按向另一位工程師解釋的方式寫的:什麼在保護你的帳號,什麼在保護資料庫裡屬於你的那些行,以及——在頁面底部、用同樣大的字號——Korat 做不到、也不該被記在功勞簿上的那些事。

兩個想法承擔了大部分重量。你的帳號是一個真實的認證會話,而不是一行我們拿來比對密碼的記錄。以及決定你能讀到什麼的是資料庫,不是應用——所以一個被改過的客戶端,或者拿著公開金鑰直接和 API 對話的人,得到的答案和應用完全一樣。

帳號:bcrypt、真實會話,以及通往資料的一座橋

身份存在 Supabase Auth 裡。密碼在服務端用 bcrypt 雜湊;應用從不接觸密碼雜湊,登入路徑上也沒有任何自制的比對邏輯。登入產生一個屬於你的 JWT,而你資料行上的一個欄位(auth_uid)是認證使用者與資料行之間唯一的連線——正是它讓下面每一條策略得以表達。

你可以用郵箱加密碼登入,也可以透過安卓的 Credential Manager 用 Google 登入,後者會把一個真實的 Google ID 令牌交給 Supabase 換取會話。全域性登出會在所有地方終止會話。

還有一個遺留的 PIN 欄位,來自啟用 Supabase Auth 之前。沒有任何東西讀它,新帳號建立時它是空的,而且已排入刪除佇列。寫在這裡只是因為它確實存在,你遲早會翻到。

通行金鑰:在服務端驗證,並帶重放防護

Korat 支援通行金鑰(passkey)——一對在你手機安全硬體裡生成、用指紋或面容解鎖、永不離開裝置的金鑰。依賴方是 koratland.com,透過 assetlinks.json 與安卓應用繫結,而驗證跑在一個 Cloudflare Worker 裡,不是調庫:

  • 挑戰值單次有效、五分鐘過期,用掉即刪除該行
  • authenticator data 裡的 rpIdHash 必須等於 koratland.com 的 SHA-256
  • user-present 標誌必須被置位
  • 簽名用 WebCrypto 驗證——ECDSA P-256 或 RS256
  • 簽名計數器必須遞增;重複或倒退的計數器按重放拒絕

註冊要求已有會話,且呼叫者的身份取自其 bearer token,絕不取自請求體裡的使用者 id——否則任何人都能把一枚通行金鑰註冊到任何人的帳號上。儲存憑據的那一行完全沒有 UPDATE 策略,所以連它的所有者也無法改寫自己的公鑰或簽名計數器;重新命名通行金鑰走的是一個只能改標籤的函式。

有一處刻意的省略:不驗證 attestation。Korat 不檢查認證器由哪家廠商製造,所以這裡的任何內容都不應被讀成硬體背書的宣告。

誠實的狀態。通行金鑰的*註冊*已驗證可用。通行金鑰*登入*尚未端到端跑通,網頁版則完全沒有通行金鑰支援——它是一項處於推進中的安卓功能。日常使用的路徑是郵箱加密碼,以及 Google 登入。

行級安全:回答問題的是資料庫,不是應用

行級安全在整個資料庫上啟用。策略圍繞四個函式書寫,而 Korat 裡幾乎每條規則都是其中之一:

app_uid()
呼叫者的資料 id,未登入時為 null。每一條“只能讀自己那行”策略的基礎。
is_member_of(business_id)
對店主、或對該商家持有有效成員記錄的人為真。
has_capability(business_id, capability)
在服務端展開角色預設——店主、經理、服務員、廚房——所以資料庫從不聽信客戶端對角色含義的理解。
can_review(business_id)
讀取資格檢視,讓“只有真實顧客能評價”成為資料庫的屬性,而不是應用給出的一句承諾。

有三條規則的代價高到值得寫下來。只從店家視角書寫的策略會把顧客排除在外——一次就餐會話、一次呼叫服務員和一筆交易都有兩位合法讀者,所以它們寫成 user_id = app_uid() or is_member_of(business_id)子表的可讀範圍必須與父表完全一致,所以評論和點贊把判斷委派給帖子,而不是各自持有一套關於誰可以讀的看法。以及定義者函式以所有者身份執行,因此它必須自己檢查權限。

RLS 做不到的事,以及用什麼來替代

行級安全能回答*誰在呼叫*。它永遠無法回答*這個請求是否攜帶了有效令牌*——桌臺二維碼、分享連結、一份邀請。所以 Korat 裡每一條由令牌把守的路徑,都是一個自己檢查令牌的 SECURITY DEFINER 函式,而表本身保持關閉。

加入餐桌。正要掃碼的人既不是會話的主人也不是店員,所以他根本讀不到就餐會話表——連查一下令牌都不行。join_session_by_token 在服務端匹配令牌,把第一個掃碼的人設為主人,加入參與者,而令牌不對時什麼都不返回,而不是給出一句能被用來縮小猜測範圍的提示。

分享連結。resolve_share 只返回連結的型別和目標,別的什麼都沒有——從不返回是誰分享的,從不返回點選數。記一次點選走 record_share_click,所以任何人都能被計入,卻不需要對統計表有任何插入權限;那張表刻意對任何人都沒有插入策略。

金額。下單完全跑在伺服器上。它自己給每一行定價,並從商家自己那行讀取配送費,忽略客戶端發來的任何值——因為一筆客戶端能設定的費用,就是一張客戶端能自己發給自己的折扣券。

聯絡方式、裝置,以及那次不是你的登入

備用郵箱的驗證碼由一個只有 service role 能呼叫的函式生成。只儲存驗證碼的 SHA-256,十分鐘過期,允許五次嘗試,且限流為每分鐘一次請求。負責發信的 Worker 從不把驗證碼回傳給應用。

你不能把自己的聯絡方式標記為已驗證。資料庫觸發器會在任何客戶端寫入時把已驗證標誌強制為 false,只有真正核對過驗證碼的定義者函式才能置位。這看起來偏執,直到你把它推演完:沒有它,把你的地址加到別人的帳號上再點“忘記密碼”,就是一次完整的帳號接管。出於同樣的理由,一個聯絡方式必須先驗證才能設為主要——主要地址正是系統發信的那個地址。

每一臺登入過的裝置都列在設定裡,可以逐臺吊銷。裝置表完全沒有插入、更新或刪除策略,因為一臺剛被吊銷的裝置手上還握著有效令牌,否則它就能清掉自己的吊銷狀態。吊銷一臺裝置會刪除底層會話,重新整理令牌隨之失效。

從新裝置登入時,應用內提醒與建立裝置行在同一個事務裡寫入,因此不會丟失,同時也給你發郵件——只發往已驗證的聯絡方式,因為未驗證的地址可能屬於別人。郵件對每臺裝置最多發一次,且只針對最近一小時內建立的裝置,所以這個端點無法被變成刷屏轟炸帳號所有者的工具。

吊銷的邊界,直說。一枚已經籤發出去的訪問令牌大約存活一小時,無法在飛行途中召回。一臺離線的、已被吊銷的裝置會一直工作到它的令牌過期。這是 JWT 的本性而不是 bug,也正是我們提供裝置列表、而不是聲稱“吊銷即時生效”的原因。

關於你自己的帳號,你能做什麼

刪除帳號可以自助完成,就在設定裡——不用給誰寫郵件。它是一條被記錄下來、可以撤銷的*請求*,而不是你凌晨兩點按下去就沒法回頭的按鈕。它的狀態在應用裡可以檢視。privacy@koratland.com 仍然承接其他 PDPA 權利:查閱、更正、可攜帶和撤回同意。

舉報同時覆蓋內容和人——個人主頁、帖子、評論、快拍、訊息和商家主頁。接收舉報的那個函式是 SECURITY INVOKER,是整個後臺裡唯一一個這樣的。如果它以資料庫所有者的權限執行,它就會*看見*被隱藏的帖子和私密帖子,並對它們給出不同的答覆,那會把舉報按鈕變成一種探測「我無權看到的內容是否存在」的手段。以呼叫者的權限執行時,「沒有這條帖子」和「這條帖子不是你能看的」就塌縮成同一個答覆——這是誠實的,因為站在舉報者的位置上,那兩件事本來就是同一件事。頻率限制在觸發器裡,不在應用裡。

Korat 做不到什麼

這份清單正是這一頁其餘部分值得讀的理由。下面每一條今天都為真。

  • 沒有任何東西是端到端加密的。訊息、檔案和通話在傳輸中由 TLS 保護,在靜態時由行級安全保護。伺服器能讀到它們。Korat 沒有裝置金鑰、沒有金鑰管理,凡是需要這些的場景都不該選它。
  • 除託管資料庫自帶的部分外,沒有應用層的靜態加密。
  • 從未核驗過任何身份證件。沒有 eKYC。Korat 裡沒有任何地方寫著“身份已認證”,信任徽章說的是存在哪些證據。
  • 電話號碼無法驗證——沒有接入任何簡訊服務商,端點會直說這一點而不是假裝可以。
  • 上傳的媒體可以透過 URL 被任何人讀取。頭像、帖子圖片和聊天圖片存在公開儲存桶裡;上傳或修改需要會話,但一條洩露的 URL 就是一份可被取走的檔案。目前沒有簽名 URL。
  • 商家控制檯裡的權限檢查目前跑在裝置上,服務端存在同一套矩陣 has_capability()。在那次推進完成之前,請把控制檯權限當作一種組織管理手段,而不是一道安全邊界。
  • 通話沒有 TURN 伺服器,也沒有端到端加密;部分跨網路的通話會連不上。
  • 當前方案下沒有資料庫備份,也沒有自動化測試套件。
  • 網頁版沒有推送通知。推送是安卓應用的能力;在網頁上,你會在開啟標籤頁時看到通知,而不是它到達的時候。

如果其中任何一條改變了,這一頁會跟著改。寫一段關於“銀行級加密”的話然後翻篇會容易得多;這樣寫更有用。

常見問題

Korat 的訊息是端到端加密的嗎?

不是。訊息經 TLS 傳輸,在資料庫裡由行級安全策略保護,只有會話中的兩位參與者能讀——但伺服器能讀到它們。如果你需要端到端加密,那段對話不該用 Korat。

如果有人偷了我的密碼會怎樣?

他第一次從你沒用過的裝置登入時,會觸發新裝置提醒:應用內一條,以及發往你已驗證聯絡方式的一封郵件。你可以在設定裡吊銷那臺裝置,這會刪除它的會話,然後修改密碼。加一枚通行金鑰,能在那臺裝置上把密碼整個從攻擊面裡去掉。

我在一家店下過單,這家店能看到我的資料嗎?

店家看到的是服務這筆訂單所需要的:桌上的你的名字、點了什麼,以及配送時你填的地址。你的資料欄位各自帶公開/僅好友/私密設定,並在別人檢視你的資料時生效。會員積分和客戶名單由銷售流水構建,不是由你的資料構建。

Korat 符合 PDPA 嗎?

Korat 在建立帳號前徵求 PDPA 同意,介面必須滾動到底,並有一條真實的拒絕路徑。它說明收集了什麼、為什麼收集,不出售個人資料,並透過 privacy@koratland.com 支援訪問、更正、刪除、反對、可攜帶與撤回同意。合規是一項持續義務而不是一枚徽章,上面列出的缺口也是這個誠實答案的一部分。

怎樣報告安全問題?

把細節發到 hello@koratland.com。這裡沒有賞金計劃,但一份真實的報告會得到一個真實的答覆。

這一頁沒回答你的問題?

寫信到 hello@koratland.com。安全問題會得到直接的答覆,包括“這個我們還沒做”。