28 KiB
Bitwarden Client Applications - Claude Code Configuration
Bitwarden client applications monorepo containing all non-mobile clients: web vault, browser extension, desktop app (Electron), and CLI. An open-source password manager focused on security, privacy, and cross-platform accessibility.
Overview
What This Project Does
- Password Management: Secure storage and synchronization of passwords, notes, cards, identities, and SSH keys across all platforms
- Key Entry Points:
- Web Vault:
apps/web/src/main.ts(Angular SPA) - Browser Extension:
apps/browser/src/popup/main.ts(popup),apps/browser/src/platform/background.ts(service worker) - Desktop:
apps/desktop/src/main.ts(Electron main process) - CLI:
apps/cli/src/bw.ts(Node.js command-line tool)
- Web Vault:
- Target Users: Individual users, teams, and enterprise organizations requiring secure credential management
Key Concepts
- Cipher: The core encrypted vault item (login, card, identity, secure note, SSH key)
- Collection: Organizational grouping for sharing ciphers with team members
- Folder: Personal organizational structure for ciphers (user-only, not shared)
- Organization: Enterprise/team account that manages users, collections, and policies
- Master Password: Primary authentication credential used to derive encryption keys
- User Key: Symmetric key that encrypts all user vault data, itself encrypted by the master key
- Trusted Device Encryption (TDE): Allows device-based vault decryption without master password
- EncString: Encrypted string type used throughout for all sensitive data storage
Architecture & Patterns
System Architecture
User Request
|
┌────┴────┐
│ Client │ (Browser/Desktop/CLI/Web)
└────┬────┘
│
┌────┴─────────────────────────────────────────┐
│ @bitwarden/angular │
│ (Shared Angular Components) │
└────┬─────────────────────────────────────────┘
│
┌────┴─────────────────────────────────────────┐
│ @bitwarden/auth │
│ (Login Strategies, Guards, 2FA, SSO) │
└────┬─────────────────────────────────────────┘
│
┌────┴─────────────────────────────────────────┐
│ @bitwarden/common │
│ (Platform-agnostic: Models, Services, Crypto)│
└────┬─────────────────────────────────────────┘
│
┌────┴─────────────────────────────────────────┐
│ @bitwarden/key-management │
│ (Cryptographic Key Operations) │
└────┬─────────────────────────────────────────┘
│
┌────┴─────────────────────────────────────────┐
│ @bitwarden/state │
│ (Reactive State Management) │
└────┴─────────────────────────────────────────┘
│
Bitwarden API Server
Code Organization
bitwarden-clients/
├── apps/
│ ├── browser/ # Chrome/Firefox/Safari/Edge extension
│ │ ├── src/popup/ # Extension popup UI
│ │ ├── src/background/ # Service worker
│ │ └── src/platform/ # BrowserApi abstraction
│ ├── cli/ # Command-line interface
│ │ ├── src/commands/ # CLI command implementations
│ │ └── src/service-container/ # DI setup
│ ├── desktop/ # Electron desktop app
│ │ ├── src/main/ # Electron main process
│ │ └── src/ # Renderer process (Angular)
│ └── web/ # Web vault (Angular SPA)
│ ├── src/app/ # Angular application
│ └── config/ # Environment configurations
├── libs/
│ ├── common/ # Platform-agnostic core (NEVER import Angular/Node here)
│ │ ├── src/vault/ # Cipher models and services
│ │ ├── src/auth/ # Auth types and utilities
│ │ └── src/platform/ # Platform abstractions
│ ├── angular/ # Shared Angular components/services
│ ├── auth/ # Authentication (login strategies, guards)
│ ├── key-management/ # Cryptographic operations
│ ├── state/ # State provider framework
│ ├── vault/ # Vault UI components
│ └── components/ # UI component library (Tailwind)
└── bitwarden_license/ # Commercial/Enterprise features
├── bit-browser/ # Licensed browser features
├── bit-cli/ # Licensed CLI features
├── bit-common/ # Licensed common services
└── bit-web/ # Licensed web features
Key Principles
- Dependency Boundaries:
libs/cannot import fromapps/;libs/commoncannot import Angular or Node - OSS vs Commercial Separation: Open-source builds exclude
bitwarden_license/; commercial builds include it - Platform Abstraction: Use service abstractions (e.g.,
BrowserApi) instead of direct platform APIs - Zero-Knowledge Architecture: All encryption/decryption happens client-side; server never sees plaintext
Core Patterns
Login Strategy Pattern
Purpose: Handle diverse authentication methods (password, SSO, passkey, device auth) through Strategy Design Pattern
Implementation (libs/auth/src/common/login-strategies/):
// Each auth method has its own strategy extending the base
export abstract class LoginStrategy {
abstract logIn(credentials: Credentials): Promise<AuthResult>;
protected async startLogIn(): Promise<AuthResult> {
// POST to /connect/token endpoint
// Process IdentityTokenResponse, IdentityTwoFactorResponse, or IdentityDeviceVerificationResponse
// Returns AuthResult for routing decisions
}
}
// Available strategies:
// - PasswordLoginStrategy
// - SsoLoginStrategy
// - AuthRequestLoginStrategy (Login with Device)
// - WebAuthnLoginStrategy (Passkey)
// - UserApiLoginStrategy (API Key - CLI only)
Usage:
// LoginStrategyService orchestrates strategy selection
const credentials = new PasswordLoginCredentials(email, masterPassword);
const authResult = await loginStrategyService.logIn(credentials);
if (authResult.requiresTwoFactor) {
// Route to 2FA component
} else if (authResult.requiresDeviceVerification) {
// Route to device verification
}
State Provider Pattern
Purpose: Enforce consistent state management with account switching support and clear ownership
Implementation (libs/state/):
// Define state location and namespace
export const VAULT_DISK = new StateDefinition("vault", "disk");
// Define specific state key with cleanup behavior
const CIPHERS_STATE = new UserKeyDefinition<CipherData[]>(
VAULT_DISK,
"ciphers",
{
deserializer: (data) => data?.map(c => new CipherData(c)) ?? [],
clearOn: ["logout"], // Clear on logout, not lock
}
);
// Access via StateProvider
class CipherService {
constructor(private stateProvider: StateProvider) {}
getCiphers$(userId: UserId): Observable<CipherData[]> {
return this.stateProvider.getUser(userId, CIPHERS_STATE).state$;
}
async updateCipher(userId: UserId, cipher: CipherData): Promise<void> {
await this.stateProvider.getUser(userId, CIPHERS_STATE).update(
(state) => [...state.filter(c => c.id !== cipher.id), cipher],
{ shouldUpdate: (current) => !this.isEqual(current, cipher) }
);
}
}
Angular Guard Pattern
Purpose: Protect routes based on authentication state, lock status, and permissions
Implementation (libs/angular/src/auth/guards/):
// Auth guard - redirects unauthenticated users
export const authGuard = (): CanActivateFn => {
return async () => {
const authService = inject(AuthService);
const router = inject(Router);
const authStatus = await firstValueFrom(authService.authStatus$);
if (authStatus === AuthenticationStatus.Unlocked) {
return true;
}
return router.createUrlTree(["/login"]);
};
};
// Organization permission guard
export const orgPermissionsGuard = (permissions: OrganizationPermission[]): CanActivateFn => {
return async (route) => {
const organizationService = inject(OrganizationService);
const org = await organizationService.get(route.params.organizationId);
return permissions.every(p => org.hasPermission(p));
};
};
Development Guide
Adding a New Vault Item Type
Step-by-step checklist for adding a new cipher type (e.g., the SSH Key type added recently).
1. Define the Enum (libs/common/src/vault/enums/cipher-type.ts)
// Use const object pattern (NOT TypeScript enum per ADR-0025)
export const CipherType = {
Login: 1,
SecureNote: 2,
Card: 3,
Identity: 4,
SshKey: 5, // New type
} as const;
export type CipherType = (typeof CipherType)[keyof typeof CipherType];
2. Create Domain Model (libs/common/src/vault/models/domain/ssh-key.ts)
export class SshKey extends Domain {
privateKey: EncString;
publicKey: EncString;
fingerprint: EncString;
constructor(obj?: SshKeyData) {
super();
if (obj == null) return;
this.privateKey = new EncString(obj.privateKey);
this.publicKey = new EncString(obj.publicKey);
this.fingerprint = new EncString(obj.fingerprint);
}
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<SshKeyView> {
// Implement decryption
}
}
3. Create View Model (libs/common/src/vault/models/view/ssh-key.view.ts)
export class SshKeyView implements View {
privateKey: string;
publicKey: string;
fingerprint: string;
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
return Object.assign(new SshKeyView(), obj);
}
}
4. Create Data Model (libs/common/src/vault/models/data/ssh-key.data.ts)
export class SshKeyData {
privateKey: string;
publicKey: string;
fingerprint: string;
constructor(response?: SshKeyApi) {
if (response == null) return;
this.privateKey = response.privateKey;
this.publicKey = response.publicKey;
this.fingerprint = response.fingerprint;
}
}
5. Update Cipher Model (libs/common/src/vault/models/domain/cipher.ts)
// Add property and constructor case
export class Cipher extends Domain {
sshKey?: SshKey;
constructor(obj?: CipherData) {
// ...existing code...
switch (this.type) {
case CipherType.SshKey:
this.sshKey = new SshKey(obj.sshKey);
break;
}
}
}
6. Create UI Components (platform-specific in apps/ or shared in libs/vault/)
@Component({
selector: 'app-ssh-key-view',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush, // Required
})
export class SshKeyViewComponent {
protected cipher = input.required<CipherView>(); // Signal input
protected copyText = inject(CopyService);
}
7. Write Tests (libs/common/src/vault/models/domain/ssh-key.spec.ts)
describe("SshKey", () => {
it("should decrypt ssh key fields", async () => {
const data = new SshKeyData(mockResponse);
const sshKey = new SshKey(data);
const view = await sshKey.decrypt("orgId", mockKey);
expect(view.privateKey).toBe("decrypted-private-key");
});
});
Common Patterns
Creating an Angular Component (Modern Pattern)
import { Component, ChangeDetectionStrategy, inject, input, output } from "@angular/core";
@Component({
selector: "app-example",
templateUrl: "./example.component.html",
// standalone: true is default, omit it
changeDetection: ChangeDetectionStrategy.OnPush, // REQUIRED
imports: [CommonModule, BitButtonModule],
})
export class ExampleComponent {
// Use inject() instead of constructor injection
protected router = inject(Router);
protected cipherService = inject(CipherService);
// Signal inputs/outputs preferred
cipherId = input.required<string>();
onSave = output<CipherView>();
// Use protected for template-accessible, private for internal
protected loading = signal(false);
protected async save(): Promise<void> {
this.loading.set(true);
try {
const cipher = await this.cipherService.save(this.cipherId());
this.onSave.emit(cipher);
} finally {
this.loading.set(false);
}
}
}
Service with State Management
@Injectable({ providedIn: "root" })
export class FolderService {
constructor(
private stateProvider: StateProvider,
private apiService: ApiService,
) {}
// Expose as observable, take userId explicitly
folders$(userId: UserId): Observable<FolderView[]> {
return this.stateProvider.getUser(userId, FOLDERS_STATE).state$.pipe(
map(folders => folders?.map(f => new FolderView(f)) ?? [])
);
}
async createFolder(userId: UserId, name: string): Promise<void> {
// API call first
const response = await this.apiService.postFolder({ name });
// Then update local state
await this.stateProvider.getUser(userId, FOLDERS_STATE).update(
state => [...state, new FolderData(response)]
);
}
}
Error Handling
// Use typed errors for catchable conditions
export class AuthenticationError extends Error {
constructor(
message: string,
public readonly twoFactorRequired: boolean = false,
) {
super(message);
}
}
// In services, throw specific errors
async login(credentials: PasswordLoginCredentials): Promise<AuthResult> {
try {
return await this.loginStrategyService.logIn(credentials);
} catch (e) {
if (e instanceof ErrorResponse && e.statusCode === 400) {
throw new AuthenticationError("Invalid credentials");
}
throw e;
}
}
Data Models
Core Types
// Cipher - The encrypted vault item
interface Cipher {
id: string;
organizationId?: string;
folderId?: string;
type: CipherType;
name: EncString;
notes?: EncString;
login?: Login;
card?: Card;
identity?: Identity;
secureNote?: SecureNote;
sshKey?: SshKey;
fields?: Field[];
attachments?: Attachment[];
collectionIds: string[];
favorite: boolean;
reprompt: CipherRepromptType;
revisionDate: Date;
deletedDate?: Date;
}
// CipherView - Decrypted cipher for display
interface CipherView {
id: string;
name: string; // Decrypted
notes?: string; // Decrypted
login?: LoginView;
// ...other decrypted fields
}
// UserId - Branded type for type safety
type UserId = Opaque<string, "UserId">;
// EncString - Encrypted string container
class EncString {
encryptionType: EncryptionType;
data: string;
iv?: string;
mac?: string;
static async encrypt(plaintext: string, key: SymmetricCryptoKey): Promise<EncString>;
async decrypt(key: SymmetricCryptoKey): Promise<string>;
}
State Definitions
// State definitions follow pattern: DOMAIN_STORAGE_LOCATION
export const VAULT_DISK = new StateDefinition("vault", "disk");
export const AUTH_MEMORY = new StateDefinition("auth", "memory");
// Key definitions for specific data
const CIPHERS_KEY = new UserKeyDefinition<Record<string, CipherData>>(
VAULT_DISK,
"ciphers",
{
deserializer: (data) => data ?? {},
clearOn: ["logout"], // "logout", "lock", or both
}
);
// Global state (not user-scoped)
const ENVIRONMENT_KEY = new KeyDefinition<EnvironmentData>(
ENVIRONMENT_DISK,
"environment",
{ deserializer: (data) => data }
);
Security & Configuration
Security Rules
MANDATORY - These rules have no exceptions:
- Never log sensitive data: No passwords, keys, tokens, or plaintext vault data in console or logs
- Always use EncString for sensitive storage: Never store plaintext credentials or vault content
- Validate all user input: Use TypeScript types and runtime validation at system boundaries
- Never bypass user verification: Master password reprompt and biometric checks are security features
- Clear keys on lock/logout: Use
clearOninUserKeyDefinitionto ensure proper cleanup - No direct browser APIs in extension: Always use
BrowserApiabstraction for cross-browser safety - Respect rate limiting: Handle 429 responses gracefully; never retry aggressively
Security Functions
| Function | Purpose | Usage |
|---|---|---|
encryptService.encrypt() |
Encrypt plaintext with user key | All vault data before storage |
cryptoService.getKey() |
Get current user's encryption key | Decrypt operations only |
userVerificationService.verify() |
Confirm user identity | Before sensitive operations |
lockService.lock() |
Clear decrypted data from memory | Timeout or manual lock |
logoutService.logout() |
Full account cleanup | User logout or session end |
Environment Configuration
| Variable | Required | Description | Example |
|---|---|---|---|
NODE_ENV |
No | Build environment | development, production |
BW_RESPONSE |
No (CLI) | Output JSON format | true |
BW_QUIET |
No (CLI) | Suppress non-essential output | true |
BW_CLEANEXIT |
No (CLI) | Exit 0 even on errors | true |
BW_SESSION |
No (CLI) | Session key for unlocked vault | <session-token> |
Web Configuration Files
Located in apps/web/config/:
base.json- Default configurationdevelopment.json- Local development (npm run build:bit:dev:watch)cloud.json- Production cloud deploymentselfhosted.json- Self-hosted instances
// Example: apps/web/config/development.json
{
"dev": true,
"urls": {
"base": "https://vault.bitwarden.com",
"api": "http://localhost:4000",
"identity": "http://localhost:33656"
}
}
Authentication & Authorization
- Master Password: Derives master key via PBKDF2/Argon2; master key encrypts user key
- Two-Factor Authentication: TOTP, email, hardware keys (YubiKey), Duo
- SSO: SAML 2.0 and OpenID Connect with optional Key Connector
- Trusted Device Encryption: Device-stored keys for passwordless unlock
- Organization Roles: Owner, Admin, Manager, User with granular permissions
Testing
Test Structure
<project>/
├── src/
│ ├── feature/
│ │ ├── feature.component.ts
│ │ └── feature.component.spec.ts # Co-located unit tests
│ └── services/
│ ├── feature.service.ts
│ └── feature.service.spec.ts
└── jest.config.js
Writing Tests
Unit Test Template:
import { mock, MockProxy } from "jest-mock-extended";
describe("CipherService", () => {
let service: CipherService;
let stateProvider: MockProxy<StateProvider>;
let apiService: MockProxy<ApiService>;
beforeEach(() => {
stateProvider = mock<StateProvider>();
apiService = mock<ApiService>();
service = new CipherService(stateProvider, apiService);
});
describe("getCipher", () => {
it("should return decrypted cipher", async () => {
// Arrange
const mockCipherData = createMockCipherData();
stateProvider.getUser.mockReturnValue({
state$: of({ [mockCipherData.id]: mockCipherData })
});
// Act
const result = await firstValueFrom(service.getCipher$(userId, mockCipherData.id));
// Assert
expect(result.name).toBe("Test Cipher");
});
});
});
State Testing with FakeStateProvider:
import { FakeStateProvider } from "@bitwarden/common/spec";
describe("FolderService with state", () => {
let stateProvider: FakeStateProvider;
let service: FolderService;
beforeEach(() => {
stateProvider = new FakeStateProvider();
service = new FolderService(stateProvider);
});
it("should update folder state", async () => {
// Arrange
const userId = "user-id" as UserId;
const fakeState = stateProvider.getUser(userId, FOLDERS_KEY);
fakeState.nextState([]);
// Act
await service.createFolder(userId, "New Folder");
// Assert
expect(fakeState.state).toHaveLength(1);
expect(fakeState.state[0].name).toBe("New Folder");
});
});
Running Tests
# Run all tests
npm test
# Run tests for specific project
npx nx test web
npx nx test cli
npx nx test @bitwarden/common
# Run specific test file
npx nx test web -- --testPathPattern="cipher.service.spec.ts"
# Run with coverage
npm test -- --coverage
# Watch mode for development
npx nx test web -- --watch
Test Utilities
libs/core-test-utils/- Async test tools for state and clientslibs/state-test-utils/-FakeStateProvider,FakeGlobalState,FakeUserStatelibs/storage-test-utils/- Mock storage implementationsjest-mock-extended- Type-safe mocking library (preferred)
Code Style & Standards
Formatting
- Prettier: Auto-formatting with
npm run prettier - Line width: 100 characters
- Quotes: Double quotes for strings
- Semicolons: Required
- Trailing commas: ES5 style
Naming Conventions
camelCasefor: variables, functions, methods, propertiesPascalCasefor: classes, interfaces, types, enums (const objects), componentsSCREAMING_SNAKE_CASEfor:StateDefinitionexports (e.g.,VAULT_DISK)kebab-casefor: file names, CSS classes, Angular selectors
Imports
// 1. External packages (alphabetized)
import { Component, inject } from "@angular/core";
import { Observable } from "rxjs";
// 2. @bitwarden/* packages
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { ButtonModule } from "@bitwarden/components";
// 3. Relative imports
import { FeatureComponent } from "./feature.component";
Key ESLint Rules
no-console: error- Use logging services, neverconsole.log()@typescript-eslint/no-floating-promises: error- Always handle promises@typescript-eslint/no-explicit-any: error- Avoidanytypescurly: ["error", "all"]- Always use braces for blocks@typescript-eslint/explicit-member-accessibility- Explicitprotected/private(nopublic)@angular-eslint/prefer-on-push-component-change-detection- OnPush required
Tailwind CSS
- Use
tw-prefix for all Tailwind classes (e.g.,tw-flex tw-gap-2) - Component library in
libs/componentswith shared Tailwind config - Custom utilities defined in
tailwind.config.js
Pre-commit Hooks
Husky runs automatically:
npm run lint- ESLint checksnpm run prettier- Format verification- Commit message format validation
Anti-Patterns
DO
- ✅ Use
inject()function for dependency injection in components - ✅ Use signal inputs/outputs (
input(),output()) over decorators - ✅ Use
OnPushchange detection on all components - ✅ Use
takeUntilDestroyed()for observable subscriptions - ✅ Use
SingleUserStatewith explicituserId(notActiveUserState) - ✅ Use
BrowserApifor all browser extension API calls - ✅ Use
CliUtils.writeLn()for CLI output - ✅ Use const objects with type aliases instead of TypeScript enums
- ✅ Extract business logic to services, keep components thin
- ✅ Use
shouldUpdateoption in state updates to avoid redundant writes
DON'T
- ❌ Use TypeScript
enumkeyword (use const objects per ADR-0025) - ❌ Use
ngClassorngStyle(use[class.*]/[style.*]bindings) - ❌ Use code regions (
#region/#endregion) - ❌ Use
ActiveUserState.update()(deprecated, causes race conditions) - ❌ Use
firstValueFrom()immediately after state update (use returned value) - ❌ Import
apps/fromlibs/ - ❌ Import Angular/Node modules in
libs/common - ❌ Use direct
chrome.*/browser.*APIs in extension code - ❌ Use
console.log()anywhere (use logging services) - ❌ Store plaintext sensitive data in localStorage/sessionStorage
- ❌ Skip master password reprompt or user verification checks
- ❌ Hardcode API URLs or credentials
Deployment
Building
# Development builds (with hot reload)
npm run build:bit:dev:watch --prefix apps/web
npm run build:watch:chrome --prefix apps/browser
npm run build:dev --prefix apps/desktop
# Production builds
npx nx build web --configuration=commercial
npx nx build cli --configuration=oss
npx nx build browser --configuration=commercial
# Output locations
# - dist/apps/<app>/<configuration>/
# - Example: dist/apps/cli/oss-dev/bw.js
Versioning
Follow semantic versioning: MAJOR.MINOR.PATCH
- MAJOR: Breaking API changes, major feature overhauls
- MINOR: New features, backwards-compatible enhancements
- PATCH: Bug fixes, security patches
CI/CD
- GitHub Actions workflows in
.github/workflows/ build-*.yml- Build and test each application- Artifacts published to respective stores (Chrome Web Store, npm, etc.)
Troubleshooting
Common Issues
Browser Extension Not Loading
Problem: Extension fails to load in Chrome/Firefox
Solution:
- Check
npm cicompleted successfully - Run
npm run build:watch:chrome --prefix apps/browser - Load unpacked from
apps/browser/dist/ - Check browser console for errors
- Verify manifest.json is valid for target browser
State Not Persisting
Problem: User data disappears after refresh
Solution:
- Verify
StateDefinitionuses"disk"not"memory" - Check
clearOnsetting inUserKeyDefinition - Ensure proper
deserializerimplementation - Check browser storage limits (extension context)
Desktop IPC Errors
Problem: Communication failure between main and renderer
Solution:
- Check preload script exports the IPC method
- Verify contextIsolation settings in webPreferences
- Ensure renderer isn't importing Node modules directly
- Check main process logs for errors
CLI Session Issues
Problem: CLI reports "vault is locked" after unlock
Solution:
- Export session:
export BW_SESSION=$(bw unlock --raw) - Use
--sessionflag:bw list items --session <token> - Check
BW_SESSIONenvironment variable is set
Debug Tips
- Enable verbose logging: Set
logging.levelin configuration - Browser extension: Use
chrome://extensions-> Inspect service worker - Desktop: Use
--inspectflag for Node debugging - Web: Browser DevTools with Angular DevTools extension
- CLI: Use
BW_DEBUG=trueenvironment variable
References
Official Documentation
Internal Documentation
Security & Reporting
- Security Policy - Responsible disclosure via HackerOne
- HackerOne Program
Related Repositories
- bitwarden/server - Backend API
- bitwarden/ios - iOS app
- bitwarden/android - Android app