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:
@@ -0,0 +1,139 @@
|
||||
defmodule BearCli.Vault.BIP39 do
|
||||
@moduledoc """
|
||||
BIP39 助記詞(12 字、128-bit 熵、英文詞表),與 Web Vault 的
|
||||
assets/js/vault/bip39.js 完全一致:
|
||||
|
||||
- 產生:16 bytes 熵 → 熵+SHA-256 前 4 bit 校驗 → 12 × 11-bit 索引
|
||||
- 種子:PBKDF2-HMAC-SHA512(password=正規化助記詞、salt=
|
||||
`"mnemonic"`(Bear 不用 BIP39 passphrase)、2048 迭代、64 bytes)
|
||||
- 救援金鑰:種子 hex 字串 → PBKDF2-SHA512(salt=帳號 email 小寫、
|
||||
迭代數同主金鑰),與主金鑰同形
|
||||
|
||||
正規化:小寫、去首尾空白、內部連續空白收斂為單一空格。
|
||||
"""
|
||||
|
||||
import BearCli.Vault.BIP39.Wordlist, only: [words: 0, index: 1]
|
||||
import Bitwise
|
||||
|
||||
@strength_bits 128
|
||||
@word_count 12
|
||||
@seed_iterations 2048
|
||||
@seed_keylen 64
|
||||
|
||||
# -- 正規化 --
|
||||
|
||||
@doc "正規化助記詞(小寫、trim、內部空白收斂);回傳單字清單。"
|
||||
def normalize(mnemonic) do
|
||||
mnemonic
|
||||
|> to_string()
|
||||
|> String.downcase()
|
||||
|> String.trim()
|
||||
|> String.split(~r/\s+/, trim: true)
|
||||
end
|
||||
|
||||
@doc "正規化後以單一空格連接的助記詞字串。"
|
||||
def normalize_joined(mnemonic) do
|
||||
normalize(mnemonic) |> Enum.join(" ")
|
||||
end
|
||||
|
||||
# -- 產生 --
|
||||
|
||||
@doc "以 CSPRNG 產生 12 字助記詞(128-bit 熵)。"
|
||||
def generate do
|
||||
mnemonic_from_entropy(:crypto.strong_rand_bytes(div(@strength_bits, 8)))
|
||||
end
|
||||
|
||||
@doc "16 bytes 熵 → 12 字助記詞(熵+SHA-256 前 4 bit 校驗)。"
|
||||
def mnemonic_from_entropy(entropy) when byte_size(entropy) == 16 do
|
||||
checksum = :crypto.hash(:sha256, entropy)
|
||||
|
||||
bits =
|
||||
(entropy <> checksum)
|
||||
|> bytes_to_bits()
|
||||
|> Enum.take(12 * 11)
|
||||
|
||||
bits
|
||||
|> Enum.chunk_every(11)
|
||||
|> Enum.map(fn eleven ->
|
||||
Enum.reduce(eleven, 0, fn bit, acc -> acc * 2 + bit end)
|
||||
|> then(&Enum.at(words(), &1))
|
||||
end)
|
||||
|> Enum.join(" ")
|
||||
end
|
||||
|
||||
# -- 驗證 --
|
||||
|
||||
@doc "驗證助記詞:12 字、全部在詞表、校驗和正確。回 true/false。"
|
||||
def valid?(mnemonic) do
|
||||
list = normalize(mnemonic)
|
||||
|
||||
if length(list) != @word_count do
|
||||
false
|
||||
else
|
||||
indices = Enum.map(list, &index/1)
|
||||
|
||||
if Enum.any?(indices, &is_nil/1) do
|
||||
false
|
||||
else
|
||||
bits = Enum.flat_map(indices, &int_to_bits(&1, 11))
|
||||
entropy_bits = Enum.take(bits, 128)
|
||||
checksum_bits = Enum.drop(bits, 128)
|
||||
|
||||
entropy = bits_to_bytes(entropy_bits)
|
||||
checksum = :crypto.hash(:sha256, entropy)
|
||||
|
||||
expected =
|
||||
checksum
|
||||
|> bytes_to_bits()
|
||||
|> Enum.take(4)
|
||||
|
||||
checksum_bits == expected
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# -- 種子與救援金鑰 --
|
||||
|
||||
@doc """
|
||||
BIP39 種子:PBKDF2-HMAC-SHA512(2048 迭代、64 bytes、salt=`"mnemonic"`)。
|
||||
password=正規化助記詞(與 bip39.js `mnemonicToSeed` 相同)。
|
||||
"""
|
||||
def mnemonic_to_seed(mnemonic) do
|
||||
:crypto.pbkdf2_hmac(
|
||||
:sha512,
|
||||
normalize_joined(mnemonic),
|
||||
"mnemonic",
|
||||
@seed_iterations,
|
||||
@seed_keylen
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
救援金鑰:種子 hex 字串 → PBKDF2-SHA512(salt=帳號 email 小寫、迭代數
|
||||
同主金鑰)。與 bip39.js `rescueKeyFromSeed` 相同構造。
|
||||
"""
|
||||
def rescue_key_from_seed(seed, email, iterations) do
|
||||
seed_hex = Base.encode16(seed, case: :lower)
|
||||
:crypto.pbkdf2_hmac(:sha512, seed_hex, String.downcase(email), iterations, 64)
|
||||
end
|
||||
|
||||
# -- Private --
|
||||
|
||||
defp bytes_to_bits(binary) do
|
||||
for(<<byte <- binary>>, do: for(i <- 7..0//-1, do: Bitwise.bsr(byte, i) &&& 1))
|
||||
|> List.flatten()
|
||||
end
|
||||
|
||||
defp int_to_bits(value, n) do
|
||||
for(i <- (n - 1)..0//-1, do: Bitwise.bsr(value, i) &&& 1)
|
||||
end
|
||||
|
||||
defp bits_to_bytes(bits) do
|
||||
bits
|
||||
|> Enum.chunk_every(8)
|
||||
|> Enum.map(fn eight ->
|
||||
Enum.reduce(eight, 0, fn bit, acc -> acc * 2 + bit end)
|
||||
end)
|
||||
|> :binary.list_to_bin()
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user