Skip to content

使用操作系統的原生憑證存儲 API 安全地存儲和檢索敏感憑證。

typescript
import { secrets } from "bun";

let githubToken: string | null = await secrets.get({
  service: "my-cli-tool",
  name: "github-token",
});

if (!githubToken) {
  githubToken = prompt("請輸入你的 GitHub 令牌");

  await secrets.set({
    service: "my-cli-tool",
    name: "github-token",
    value: githubToken,
  });

  console.log("GitHub 令牌已存儲");
}

const response = await fetch("https://api.github.com/user", {
  headers: { Authorization: `token ${githubToken}` },
});

console.log(`已登錄為 ${(await response.json()).login}`);

概述

Bun.secrets 提供一個跨平台 API,用於管理敏感憑證,這些憑證通常是 CLI 工具和開發應用程序以明文文件形式存儲的,如 ~/.npmrc~/.aws/credentials.env 文件。它使用:

  • macOS:Keychain Services
  • Linux:libsecret(GNOME Keyring、KWallet 等)
  • Windows:Windows Credential Manager

所有操作都是異步且非阻塞的,在 Bun 的線程池上運行。

NOTE

將來,我們可能會添加一個額外的 `provider` 選項,使其更適合生產部署憑證,但 目前此 API 主要用於本地開發工具。

API

Bun.secrets.get(options)

檢索存儲的憑證。

typescript
import { secrets } from "bun";

const password = await Bun.secrets.get({
  service: "my-app",
  name: "alice@example.com",
});
// 返回:string | null

// 或者,如果你更喜歡不使用對象
const password = await Bun.secrets.get("my-app", "alice@example.com");

參數:

  • options.service(字符串,必需)- 服務或應用程序名稱
  • options.name(字符串,必需)- 用戶名或賬戶標識符

返回值:

  • Promise<string | null> - 存儲的密碼,如果未找到則返回 null

Bun.secrets.set(options, value)

存儲或更新憑證。

typescript
import { secrets } from "bun";

await secrets.set({
  service: "my-app",
  name: "alice@example.com",
  value: "super-secret-password",
});

參數:

  • options.service(字符串,必需)- 服務或應用程序名稱
  • options.name(字符串,必需)- 用戶名或賬戶標識符
  • value(字符串,必需)- 要存儲的密碼或憑證

說明:

  • 如果已存在給定服務/名稱組合的憑證,它將被替換
  • 存儲的值由操作系統加密

Bun.secrets.delete(options)

刪除存儲的憑證。

typescript
const deleted = await Bun.secrets.delete({
  service: "my-app",
  name: "alice@example.com",
  value: "super-secret-password",
});
// 返回:boolean

參數:

  • options.service(字符串,必需)- 服務或應用程序名稱
  • options.name(字符串,必需)- 用戶名或賬戶標識符

返回值:

  • Promise<boolean> - 如果憑證被刪除則返回 true,如果未找到則返回 false

示例

存儲 CLI 工具憑證

javascript
// 存儲 GitHub CLI 令牌(而不是 ~/.config/gh/hosts.yml)
await Bun.secrets.set({
  service: "my-app.com",
  name: "github-token",
  value: "ghp_xxxxxxxxxxxxxxxxxxxx",
});

// 或者,如果你更喜歡不使用對象
await Bun.secrets.set("my-app.com", "github-token", "ghp_xxxxxxxxxxxxxxxxxxxx");

// 存儲 npm 注冊表令牌(而不是 ~/.npmrc)
await Bun.secrets.set({
  service: "npm-registry",
  name: "https://registry.npmjs.org",
  value: "npm_xxxxxxxxxxxxxxxxxxxx",
});

// 檢索用於 API 調用
const token = await Bun.secrets.get({
  service: "gh-cli",
  name: "github.com",
});

if (token) {
  const response = await fetch("https://api.github.com/name", {
    headers: {
      Authorization: `token ${token}`,
    },
  });
}

從明文配置文件遷移

javascript
// 而不是存儲在 ~/.aws/credentials
await Bun.secrets.set({
  service: "aws-cli",
  name: "AWS_SECRET_ACCESS_KEY",
  value: process.env.AWS_SECRET_ACCESS_KEY,
});

// 而不是 .env 文件中的敏感數據
await Bun.secrets.set({
  service: "my-app",
  name: "api-key",
  value: "sk_live_xxxxxxxxxxxxxxxxxxxx",
});

// 在運行時加載
const apiKey =
  (await Bun.secrets.get({
    service: "my-app",
    name: "api-key",
  })) || process.env.API_KEY; // CI/生產環境的回退方案

錯誤處理

javascript
try {
  await Bun.secrets.set({
    service: "my-app",
    name: "alice",
    value: "password123",
  });
} catch (error) {
  console.error("存儲憑證失敗:", error.message);
}

// 檢查憑證是否存在
const password = await Bun.secrets.get({
  service: "my-app",
  name: "alice",
});

if (password === null) {
  console.log("未找到憑證");
}

更新憑證

javascript
// 初始密碼
await Bun.secrets.set({
  service: "email-server",
  name: "admin@example.com",
  value: "old-password",
});

// 更新為新密碼
await Bun.secrets.set({
  service: "email-server",
  name: "admin@example.com",
  value: "new-password",
});

// 舊密碼被替換

平台行為

macOS (Keychain)

  • 憑證存儲在用戶的登錄鑰匙串中
  • 首次使用時,鑰匙串可能會提示訪問權限
  • 憑證在系統重啟後持久存在
  • 僅存儲憑證的用戶可訪問

Linux (libsecret)

  • 需要密鑰服務守護進程(GNOME Keyring、KWallet 等)
  • 憑證存儲在默認集合中
  • 如果密鑰環已鎖定,可能會提示解鎖
  • 密鑰服務必須正在運行

Windows (Credential Manager)

  • 憑證存儲在 Windows 憑證管理器中
  • 可在控制面板 → 憑證管理器 → Windows 憑證中查看
  • 使用 CRED_PERSIST_ENTERPRISE 標志持久化,因此按用戶范圍限定
  • 使用 Windows 數據保護 API 加密

安全注意事項

  1. 加密:憑證由操作系統的憑證管理器加密
  2. 訪問控制:只有存儲憑證的用戶才能檢索它
  3. 無明文:密碼從不以明文存儲
  4. 內存安全:Bun 在使用後清零密碼內存
  5. 進程隔離:憑證按用戶賬戶隔離

限制

  • 最大密碼長度因平台而異(通常為 2048-4096 字節)
  • 服務和名稱長度應合理(< 256 個字符)
  • 某些特殊字符可能需要轉義,具體取決於平台
  • 需要適當的系統服務:
    • Linux:密鑰服務守護進程必須正在運行
    • macOS:必須可用鑰匙串訪問
    • Windows:必須啟用憑證管理器服務

與環境變量的比較

與環境變量不同,Bun.secrets

  • ✅ 靜態加密憑證(感謝操作系統)
  • ✅ 避免在進程內存轉儲中暴露憑證(內存不再需要時被清零)
  • ✅ 在應用程序重啟後存活
  • ✅ 可更新而無需重啟應用程序
  • ✅ 提供用戶級訪問控制
  • ❌ 需要操作系統憑證服務
  • ❌ 對部署憑證不太有用(生產環境使用環境變量)

最佳實踐

  1. 使用描述性的服務名稱:匹配工具或應用程序名稱 如果你正在構建供外部使用的 CLI,你可能應該使用 UTI(統一類型標識符)作為服務名稱。

    javascript
    // 好 - 匹配實際工具
    { service: "com.docker.hub", name: "username" }
    { service: "com.vercel.cli", name: "team-name" }
    
    // 避免 - 太通用
    { service: "api", name: "key" }
  2. 僅用於憑證:不要在此 API 中存儲應用程序配置 此 API 很慢,你可能仍然需要使用配置文件來做某些事情。

  3. 用於本地開發工具

    • ✅ CLI 工具(gh、npm、docker、kubectl)
    • ✅ 本地開發服務器
    • ✅ 用於測試的個人 API 密鑰
    • ❌ 生產服務器(使用適當的憑證管理)

TypeScript

typescript
namespace Bun {
  interface SecretsOptions {
    service: string;
    name: string;
  }

  interface Secrets {
    get(options: SecretsOptions): Promise<string | null>;
    set(options: SecretsOptions, value: string): Promise<void>;
    delete(options: SecretsOptions): Promise<boolean>;
  }

  const secrets: Secrets;
}

Bun學習網由www.bunjs.com.cn整理維護