feat: 實作 vault 指令群 status/unlock/lock/list/get/create/edit/delete/restore/purge/folders/sync/password/rescue(issue #17)

- 對接 alterminal/bear#41 的 /api/v1/vault JSON API(PAT Bearer)
- 客戶端端到端加密,格式與 Web Vault(P2)完全一致:
  - 加密字串 2.<iv>.<ct>.<mac>(AES-256-CBC+HMAC-SHA-256、PKCS#7、encrypt-then-MAC)
  - 主金鑰 PBKDF2-SHA512(迭代數取自 /vault/config 與 profile,不寫死;salt=email 小寫)
  - BIP39 助記詞(12 字、128-bit、英文詞表)+救援路徑
- K_user 僅存記憶體:vault unlock 匯出 BEAR_VAULT_SESSION(base64),
  同 Bitwarden CLI BW_SESSION 慣例;lock 提示 unset;不寫入任何檔案
- docs/commands.md 補 §3.11 規格;README 同步
- 測試:fake API 注入+Bear.Vault.Crypto 密文樣本交叉驗證(雙向);
  BIP39 官方向量;46 個新測試,全套 196 passed
This commit is contained in:
2026-09-10 23:27:47 +08:00
parent a880bcea4d
commit 0b18d76cfd
9 changed files with 2488 additions and 2 deletions
+81
View File
@@ -223,6 +223,87 @@ defmodule BearCli.Api do
api_request(issuer, token, :post, "/api/v1/profile/mfa/recovery-codes", json: %{code: code})
end
# -- Vault API(alterminal/bear#41;PAT Bearer、任何有效帳號)--
@doc "`GET /api/v1/vault/config`:公告的 KDF 參數與支援的加密字串型別。"
def vault_config(issuer, token) do
api_request(issuer, token, :get, "/api/v1/vault/config")
end
@doc "`GET /api/v1/vault/profile`:vault profile(404=尚未初始化)。"
def vault_profile_get(issuer, token) do
api_request(issuer, token, :get, "/api/v1/vault/profile")
end
@doc "`PUT /api/v1/vault/profile`:建立或更新 profile(上傳重新包裝的金鑰)。"
def vault_profile_put(issuer, token, attrs) do
api_request(issuer, token, :put, "/api/v1/vault/profile", json: attrs)
end
@doc "`GET /api/v1/vault/sync`:完整同步(profile+folders+ciphers)。"
def vault_sync(issuer, token) do
api_request(issuer, token, :get, "/api/v1/vault/sync")
end
@doc "`GET /api/v1/vault/ciphers`:列出 ciphers(含回收桶)。"
def vault_ciphers_list(issuer, token) do
api_request(issuer, token, :get, "/api/v1/vault/ciphers")
end
@doc "`POST /api/v1/vault/ciphers`:以客戶端 UUID upsert(衝突時整列覆蓋)。"
def vault_ciphers_upsert(issuer, token, attrs) do
api_request(issuer, token, :post, "/api/v1/vault/ciphers", json: attrs)
end
@doc "`GET /api/v1/vault/ciphers/{uuid}`:單一 cipher。"
def vault_ciphers_get(issuer, token, uuid) do
api_request(issuer, token, :get, "/api/v1/vault/ciphers/" <> URI.encode(uuid))
end
@doc "`PUT /api/v1/vault/ciphers/{uuid}`:部分更新(遞增 revision_date)。"
def vault_ciphers_update(issuer, token, uuid, attrs) do
api_request(issuer, token, :put, "/api/v1/vault/ciphers/" <> URI.encode(uuid), json: attrs)
end
@doc "`POST /api/v1/vault/ciphers/{uuid}/delete`:丟進回收桶(soft delete)。"
def vault_ciphers_delete(issuer, token, uuid) do
api_request(issuer, token, :post, "/api/v1/vault/ciphers/" <> URI.encode(uuid) <> "/delete",
json: %{}
)
end
@doc "`POST /api/v1/vault/ciphers/{uuid}/restore`:從回收桶還原。"
def vault_ciphers_restore(issuer, token, uuid) do
api_request(issuer, token, :post, "/api/v1/vault/ciphers/" <> URI.encode(uuid) <> "/restore",
json: %{}
)
end
@doc "`DELETE /api/v1/vault/ciphers/{uuid}/purge`:永久刪除(hard delete)。"
def vault_ciphers_purge(issuer, token, uuid) do
api_request(issuer, token, :delete, "/api/v1/vault/ciphers/" <> URI.encode(uuid) <> "/purge")
end
@doc "`GET /api/v1/vault/folders`:列出資料夾。"
def vault_folders_list(issuer, token) do
api_request(issuer, token, :get, "/api/v1/vault/folders")
end
@doc "`POST /api/v1/vault/folders`:以客戶端 UUID upsert 資料夾。"
def vault_folders_upsert(issuer, token, attrs) do
api_request(issuer, token, :post, "/api/v1/vault/folders", json: attrs)
end
@doc "`PUT /api/v1/vault/folders/{uuid}`:重新命名(新的加密名稱)。"
def vault_folders_rename(issuer, token, uuid, attrs) do
api_request(issuer, token, :put, "/api/v1/vault/folders/" <> URI.encode(uuid), json: attrs)
end
@doc "`DELETE /api/v1/vault/folders/{uuid}`:刪除資料夾。"
def vault_folders_delete(issuer, token, uuid) do
api_request(issuer, token, :delete, "/api/v1/vault/folders/" <> URI.encode(uuid))
end
# -- 共用請求輔助 --
# 回傳: