direct_client.go

v1.0.0
Doc Versions Source
1
package bitwarden
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"fmt"
7
	"net/http"
8
	"sync"
9
	"time"
10
11
	"sourcecraft.dev/bigbes/go-bitwarden/crypto"
12
	"sourcecraft.dev/bigbes/go-bitwarden/internal/api"
13
)
14
15
// DirectClientOption configures the DirectClient using the functional options pattern.
16
type DirectClientOption func(*DirectClient)
17
18
// WithDirectHTTPClient sets a custom http.Client for direct server communication.
19
// Use this to configure timeouts, proxies, or custom transport.
20
func WithDirectHTTPClient(hc *http.Client) DirectClientOption {
21
	return func(dc *DirectClient) {
22
		dc.httpClient = hc
23
	}
24
}
25
26
// WithDeviceInfo sets device information for token requests.
27
// This information appears in the Bitwarden account's device list.
28
// If not provided, default device info will be used.
29
func WithDeviceInfo(name, identifier string, deviceType int) DirectClientOption {
30
	return func(dc *DirectClient) {
31
		dc.device = api.DeviceInfo{Name: name, Identifier: identifier, Type: deviceType}
32
	}
33
}
34
35
// WithIdentityURL sets a separate identity URL for authentication.
36
// This is typically used for Bitwarden cloud where identity and API URLs differ.
37
// For self-hosted Vaultwarden, this is usually not needed.
38
func WithIdentityURL(url string) DirectClientOption {
39
	return func(dc *DirectClient) {
40
		dc.identityURL = url
41
	}
42
}
43
44
// WithTwoFactorProvider sets a callback function for handling two-factor authentication.
45
// The callback receives the available 2FA providers and should return the selected
46
// provider type and code. This is called during Login/Unlock if 2FA is enabled.
47
func WithTwoFactorProvider(p api.TwoFactorProvider) DirectClientOption {
48
	return func(dc *DirectClient) {
49
		dc.twoFA = p
50
	}
51
}
52
53
// WithAPIKey configures API key authentication using the client_credentials grant.
54
// API keys can be generated in the Bitwarden web vault and bypass 2FA requirements,
55
// but the master password is still required for vault decryption.
56
// The clientID is typically in the format "user.<user_uuid>".
57
//
58
// Example:
59
//
60
//	client := bitwarden.NewDirectClient(
61
//	    serverURL, email, masterPassword,
62
//	    bitwarden.WithAPIKey("user.abc-123", "secret-key"),
63
//	)
64
func WithAPIKey(clientID, clientSecret string) DirectClientOption {
65
	return func(dc *DirectClient) {
66
		dc.apiKeyClientID = clientID
67
		dc.apiKeyClientSecret = clientSecret
68
	}
69
}
70
71
// DirectClient communicates directly with a Bitwarden/Vaultwarden server,
72
// handling authentication and end-to-end encryption entirely in Go without
73
// requiring the Bitwarden CLI. This is useful for applications that need
74
// to integrate Bitwarden functionality without external dependencies.
75
//
76
// DirectClient implements the VaultClient interface and provides full access
77
// to vault operations including items, folders, collections, organizations,
78
// Send, and attachments.
79
//
80
// Security considerations:
81
//   - The master password is kept in memory for re-authentication
82
//   - Encryption keys are derived from the master password using PBKDF2 or Argon2id
83
//   - All vault data is encrypted/decrypted locally
84
//   - Keys can be cleared from memory using Lock()
85
type DirectClient struct {
86
	serverURL          string
87
	identityURL        string
88
	httpClient         *http.Client
89
	device             api.DeviceInfo
90
	twoFA              api.TwoFactorProvider
91
	apiKeyClientID     string
92
	apiKeyClientSecret string
93
94
	mu            sync.RWMutex
95
	transport     *api.Transport
96
	auth          *api.AuthClient
97
	keyChain      *crypto.KeyChain
98
	profile       *api.SyncProfile
99
	email         string
100
	password      string
101
	cache         vaultCache
102
	notifications *api.NotificationsClient
103
	notifyHandler NotificationHandler
104
105
	items          *DirectItemsService
106
	folders        *DirectFoldersService
107
	collections    *DirectCollectionsService
108
	orgCollections *DirectOrgCollectionsService
109
	organizations  *DirectOrganizationsService
110
	orgMembers     *DirectOrgMembersService
111
	send           *DirectSendService
112
	attachments    *DirectAttachmentsService
113
}
114
115
// NewDirectClient creates a new client that communicates directly with the
116
// Bitwarden/Vaultwarden server without requiring the CLI. The client handles
117
// authentication and end-to-end encryption automatically.
118
//
119
// Parameters:
120
//   - serverURL: The base URL of the Bitwarden/Vaultwarden server (e.g., "https://vault.bitwarden.com")
121
//   - email: The user's email address for authentication
122
//   - password: The master password for vault decryption
123
//   - opts: Optional configuration using With* functions
124
//
125
// The client is created in a locked state. Call Login() first, then Unlock()
126
// or call Unlock() directly which handles both authentication and key derivation.
127
//
128
// Example:
129
//
130
//	client := bitwarden.NewDirectClient(
131
//	    "https://vault.bitwarden.com",
132
//	    "user@example.com",
133
//	    "master-password",
134
//	    bitwarden.WithTwoFactorProvider(my2FAHandler),
135
//	)
136
//	err := client.Unlock(context.Background(), "master-password")
137
//	client.Sync(context.Background())
138
func NewDirectClient(serverURL, email, password string, opts ...DirectClientOption) *DirectClient {
139
	dc := &DirectClient{
140
		serverURL: serverURL,
141
		email:     email,
142
		password:  password,
143
		device:    api.DefaultDeviceInfo(),
144
		httpClient: &http.Client{
145
			Timeout: 30 * time.Second,
146
		},
147
	}
148
149
	for _, opt := range opts {
150
		opt(dc)
151
	}
152
153
	dc.transport = api.NewTransport(serverURL, dc.httpClient)
154
	dc.auth = api.NewAuthClient(dc.transport, dc.device, dc.identityURL)
155
	if dc.twoFA != nil {
156
		dc.auth.SetTwoFactorProvider(dc.twoFA)
157
	}
158
159
	dc.items = &DirectItemsService{dc: dc}
160
	dc.folders = &DirectFoldersService{dc: dc}
161
	dc.collections = &DirectCollectionsService{dc: dc}
162
	dc.orgCollections = &DirectOrgCollectionsService{dc: dc}
163
	dc.organizations = &DirectOrganizationsService{dc: dc}
164
	dc.orgMembers = &DirectOrgMembersService{dc: dc}
165
	dc.send = &DirectSendService{dc: dc}
166
	dc.attachments = &DirectAttachmentsService{dc: dc}
167
168
	return dc
169
}
170
171
// Login authenticates with the Bitwarden server using the configured credentials.
172
// This must be called before other operations (or use Unlock which calls Login internally).
173
//
174
// If an API key was configured via WithAPIKey, it uses client_credentials grant
175
// which bypasses 2FA requirements. Otherwise, it uses the standard password grant
176
// which may trigger the 2FA callback if two-factor is enabled on the account.
177
//
178
// After successful login, the encryption keys are available but the vault data
179
// still needs to be synced using Sync().
180
func (dc *DirectClient) Login(ctx context.Context) error {
181
	var kc *crypto.KeyChain
182
	var err error
183
184
	if dc.apiKeyClientID != "" {
185
		_, kc, err = dc.auth.LoginWithAPIKey(ctx, dc.apiKeyClientID, dc.apiKeyClientSecret, dc.email, dc.password)
186
	} else {
187
		_, kc, err = dc.auth.Login(ctx, dc.email, dc.password)
188
	}
189
	if err != nil {
190
		return fmt.Errorf("login: %w", err)
191
	}
192
193
	dc.mu.Lock()
194
	dc.keyChain = kc
195
	dc.mu.Unlock()
196
197
	return nil
198
}
199
200
// Register creates a new Bitwarden account on the server.
201
// This is typically only used for testing or automated account provisioning.
202
// The cfg parameter specifies the key derivation function configuration (PBKDF2 or Argon2id).
203
func (dc *DirectClient) Register(ctx context.Context, name string, cfg crypto.DeriveKeyConfig) error {
204
	return dc.auth.Register(ctx, dc.email, dc.password, name, cfg)
205
}
206
207
// Status returns the current client status including lock state, user information,
208
// and server URL. This does not require the vault to be unlocked.
209
func (dc *DirectClient) Status(ctx context.Context) (*Status, error) {
210
	dc.mu.RLock()
211
	kc := dc.keyChain
212
	profile := dc.profile
213
	dc.mu.RUnlock()
214
215
	s := &Status{
216
		ServerURL: dc.serverURL,
217
	}
218
219
	if profile != nil {
220
		s.UserEmail = profile.Email
221
		s.UserID = profile.ID
222
	}
223
224
	if kc != nil {
225
		s.Status = "unlocked"
226
	} else {
227
		s.Status = "locked"
228
	}
229
230
	return s, nil
231
}
232
233
// Sync downloads the vault data from the server and decrypts it locally.
234
// The vault must be unlocked before calling Sync. After sync, the decrypted
235
// data is available through the various service APIs (Items, Folders, etc.).
236
//
237
// Sync also loads organization keys and populates the local cache for faster access.
238
func (dc *DirectClient) Sync(ctx context.Context) error {
239
	kc := dc.getKeyChain()
240
	if kc == nil {
241
		return &VaultLockedError{}
242
	}
243
244
	data, err := dc.transport.Get(ctx, "/api/sync", nil)
245
	if err != nil {
246
		return fmt.Errorf("sync: %w", err)
247
	}
248
249
	var syncResp api.SyncResponse
250
	if err := json.Unmarshal(data, &syncResp); err != nil {
251
		return fmt.Errorf("parse sync response: %w", err)
252
	}
253
254
	// Load org keys
255
	for _, org := range syncResp.Profile.Organizations {
256
		if org.Key != "" {
257
			if err := kc.AddOrgKey(org.ID, org.Key); err != nil {
258
				return fmt.Errorf("load org key %s: %w", org.ID, err)
259
			}
260
		}
261
	}
262
263
	dc.mu.Lock()
264
	dc.profile = &syncResp.Profile
265
	dc.mu.Unlock()
266
267
	// Decrypt and cache vault data
268
	items := make([]Item, 0, len(syncResp.Ciphers))
269
	for _, raw := range syncResp.Ciphers {
270
		item, err := decryptCipher(raw, kc)
271
		if err != nil {
272
			continue
273
		}
274
		items = append(items, *item)
275
	}
276
277
	folders := make([]Folder, 0, len(syncResp.Folders))
278
	for _, raw := range syncResp.Folders {
279
		f, err := decryptFolder(raw, kc)
280
		if err != nil {
281
			continue
282
		}
283
		folders = append(folders, *f)
284
	}
285
286
	sends := make([]Send, 0, len(syncResp.Sends))
287
	for _, raw := range syncResp.Sends {
288
		s, err := decryptSend(raw, kc)
289
		if err != nil {
290
			continue
291
		}
292
		sends = append(sends, *s)
293
	}
294
295
	dc.cache.populate(items, folders, sends)
296
297
	return nil
298
}
299
300
// Lock clears the encryption keys and cached vault data from memory.
301
// If real-time notifications are running, they are stopped.
302
// After locking, Unlock must be called before accessing vault data again.
303
func (dc *DirectClient) Lock(ctx context.Context) error {
304
	_ = dc.StopNotifications()
305
	dc.mu.Lock()
306
	if dc.keyChain != nil {
307
		dc.keyChain.Clear()
308
		dc.keyChain = nil
309
	}
310
	dc.mu.Unlock()
311
	dc.cache.invalidate()
312
	return nil
313
}
314
315
// Unlock re-authenticates with the server and re-derives the encryption keys
316
// from the master password. This combines Login and key derivation into one step.
317
//
318
// If 2FA is enabled, the configured TwoFactorProvider callback will be invoked.
319
// After unlock, call Sync to download and decrypt the vault data.
320
func (dc *DirectClient) Unlock(ctx context.Context, password string) error {
321
	// Get the protected keys from the server via a fresh login.
322
	_, kc, err := dc.auth.Login(ctx, dc.email, password)
323
	if err != nil {
324
		return fmt.Errorf("unlock: %w", err)
325
	}
326
327
	dc.mu.Lock()
328
	dc.keyChain = kc
329
	dc.password = password
330
	dc.mu.Unlock()
331
332
	return nil
333
}
334
335
// Generate generates a password or passphrase locally using cryptographically
336
// secure random number generation. This does not require server communication.
337
// Use GenerateOptions to configure the password complexity or passphrase settings.
338
func (dc *DirectClient) Generate(ctx context.Context, opts GenerateOptions) (string, error) {
339
	return generatePassword(opts)
340
}
341
342
// Items returns the vault items (ciphers) service for CRUD operations on vault items.
343
func (dc *DirectClient) Items() ItemsAPI { return dc.items }
344
345
// Folders returns the folders service for organizing vault items.
346
func (dc *DirectClient) Folders() FoldersAPI { return dc.folders }
347
348
// Collections returns the collections service for accessing user collections.
349
func (dc *DirectClient) Collections() CollectionsAPI { return dc.collections }
350
351
// OrgCollections returns the organization collections service for managing org collections.
352
func (dc *DirectClient) OrgCollections() OrgCollectionsAPI { return dc.orgCollections }
353
354
// Organizations returns the organizations service for listing user organizations.
355
func (dc *DirectClient) Organizations() OrganizationsAPI { return dc.organizations }
356
357
// OrgMembers returns the organization members service for member management.
358
func (dc *DirectClient) OrgMembers() OrgMembersAPI { return dc.orgMembers }
359
360
// Send returns the Bitwarden Send service for creating secure shares.
361
func (dc *DirectClient) Send() SendAPI { return dc.send }
362
363
// Attachments returns the attachments service for file attachment operations.
364
func (dc *DirectClient) Attachments() AttachmentsAPI { return dc.attachments }
365
366
// GetAPIKey retrieves an API key for the current user. The client must be
367
// logged in (have a valid access token). The API key can be used for
368
// authenticating without 2FA in automated environments.
369
//
370
// Returns:
371
//   - clientID: The API key identifier (typically "user.<uuid>")
372
//   - clientSecret: The API key secret
373
func (dc *DirectClient) GetAPIKey(ctx context.Context) (clientID, clientSecret string, err error) {
374
	return dc.auth.GetAPIKey(ctx, dc.email, dc.password)
375
}
376
377
// ServerURL returns the configured Bitwarden/Vaultwarden server URL.
378
func (dc *DirectClient) ServerURL() string {
379
	return dc.serverURL
380
}
381
382
func (dc *DirectClient) getKeyChain() *crypto.KeyChain {
383
	dc.mu.RLock()
384
	defer dc.mu.RUnlock()
385
	return dc.keyChain
386
}
387
388
// Verify DirectClient satisfies VaultClient at compile time.
389
var _ VaultClient = (*DirectClient)(nil)
390

Source Files