bitwarden-cli-bio 0.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jean Regisser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # bitwarden-cli-bio
2
+
3
+ Unlock your Bitwarden CLI vault with biometrics (Touch ID, Windows Hello, Linux Polkit) instead of typing your master password. Again. And again.
4
+
5
+ ```bash
6
+ # before: ugh
7
+ bw get password github
8
+ ? Master password: [type your 30-character password]
9
+
10
+ # after: nice
11
+ bwbio get password github
12
+ # [Touch ID prompt] → done
13
+ ```
14
+
15
+ ## How?
16
+
17
+ `bwbio` talks to the Bitwarden Desktop app over IPC — the same protocol the browser extension uses — to unlock your vault with biometrics. Then it hands off to the official `bw` CLI with the session key. You still need `bw` installed; `bwbio` just handles the unlock part.
18
+
19
+ ```
20
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
21
+ │ │ │ Bitwarden │ │ Touch ID / │
22
+ │ bwbio │ IPC │ Desktop │ System │ Windows Hello │
23
+ │ │ ◄─────► │ App │ ◄─────► │ Linux Polkit │
24
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
25
+
26
+ │ delegates (with BW_SESSION)
27
+
28
+ ┌─────────────────┐
29
+ │ Official bw │
30
+ │ CLI │
31
+ └─────────────────┘
32
+ ```
33
+
34
+ If biometrics fail for any reason (Desktop app closed, prompt cancelled, etc.), it falls back to the regular password prompt. It never blocks you.
35
+
36
+ ## Setup
37
+
38
+ **You'll need:**
39
+ - Bitwarden Desktop app with biometrics enabled + "Allow browser integration" on
40
+ - Node.js >= 22
41
+ - Official `bw` CLI in your PATH
42
+
43
+ **Install:**
44
+
45
+ ```bash
46
+ npm install -g bitwarden-cli-bio
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ # The magic: alias it and forget about it
53
+ alias bw=bwbio
54
+ bw get password github # Touch ID, done
55
+ bw list items --search email # still Touch ID, still done
56
+
57
+ # Or use it directly
58
+ bwbio get password github
59
+
60
+ # For scripts — get a session key
61
+ eval $(bwbio unlock)
62
+ ```
63
+
64
+ If `BW_SESSION` is already set, `bwbio` stays out of the way and passes everything straight to `bw`.
65
+
66
+ ### Commands that skip biometrics
67
+
68
+ Some commands don't need an unlocked vault and go directly to `bw`:
69
+
70
+ ```
71
+ login, logout, lock, config, update, completion, status, serve
72
+ --help / -h, --version / -v
73
+ ```
74
+
75
+ Everything else triggers biometric unlock if the vault is locked.
76
+
77
+ ## Environment variables
78
+
79
+ | Variable | Description |
80
+ |----------|-------------|
81
+ | `BW_SESSION` | Already set? `bwbio` passes through to `bw` directly |
82
+ | `BW_QUIET` | Set to `true` to suppress all biometric-related messages |
83
+ | `BW_NOINTERACTION` | Set to `true` to skip biometric unlock (requires user interaction) |
84
+ | `BWBIO_VERBOSE` | Set to `true` for verbose logging |
85
+ | `BWBIO_DEBUG` | Set to `true` for raw IPC message dumps |
86
+ | `BWBIO_IPC_SOCKET_PATH` | Override the IPC socket path (advanced) |
87
+
88
+ ## Platforms
89
+
90
+ - **macOS** — Touch ID (including App Store builds) — tested
91
+ - **Windows** — Windows Hello — should work, not yet tested
92
+ - **Linux** — Polkit — should work, not yet tested
93
+
94
+ The IPC protocol is the same across platforms. If you try Windows or Linux, please [open an issue](https://github.com/jeanregisser/bitwarden-cli-bio/issues) and let us know how it goes!
95
+
96
+ ## Supply chain trust
97
+
98
+ Every npm release is automatically built and published from CI via [semantic-release](https://github.com/semantic-release/semantic-release), with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) enabled. This means:
99
+
100
+ - No human runs `npm publish` — releases come directly from GitHub Actions
101
+ - Each package on npm links back to the exact source commit and CI run that produced it
102
+ - You can verify this on the [npm package page](https://www.npmjs.com/package/bitwarden-cli-bio) (look for the "Provenance" badge)
103
+
104
+ ## Background
105
+
106
+ This should really be a feature of the official CLI. A [PR was proposed](https://github.com/bitwarden/clients/pull/18273) but was closed — the Bitwarden team wants to wait until they have a proper IPC framework. This wrapper fills the gap in the meantime using the same IPC code from that PR.
107
+
108
+ ## License
109
+
110
+ [MIT](LICENSE)
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,928 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/main.ts
4
+ import { spawn } from "child_process";
5
+
6
+ // src/biometrics.ts
7
+ import * as crypto4 from "crypto";
8
+
9
+ // src/ipc/ipc-socket.service.ts
10
+ import * as crypto from "crypto";
11
+ import * as fs from "fs";
12
+ import * as net from "net";
13
+ import * as os from "os";
14
+ import * as path from "path";
15
+ var DEBUG = process.env.BWBIO_DEBUG === "true";
16
+ var IpcSocketService = class {
17
+ socket = null;
18
+ messageBuffer = Buffer.alloc(0);
19
+ messageHandler = null;
20
+ disconnectHandler = null;
21
+ /**
22
+ * Get the IPC socket path for the current platform.
23
+ * This mirrors the logic in desktop_native/core/src/ipc/mod.rs
24
+ */
25
+ getSocketPath() {
26
+ if (process.env.BWBIO_IPC_SOCKET_PATH) {
27
+ return process.env.BWBIO_IPC_SOCKET_PATH;
28
+ }
29
+ const platform3 = os.platform();
30
+ if (platform3 === "win32") {
31
+ return this.getWindowsSocketPath();
32
+ }
33
+ if (platform3 === "darwin") {
34
+ return this.getMacSocketPath();
35
+ }
36
+ return this.getLinuxSocketPath();
37
+ }
38
+ /**
39
+ * Windows named pipe path - uses hash of home directory.
40
+ */
41
+ getWindowsSocketPath() {
42
+ const homeDir = os.homedir();
43
+ const hash = crypto.createHash("sha256").update(homeDir).digest();
44
+ const hashB64 = hash.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
45
+ return `\\\\.\\pipe\\${hashB64}.s.bw`;
46
+ }
47
+ /**
48
+ * Get the socket path on macOS.
49
+ * The Desktop app can be sandboxed (Mac App Store) or non-sandboxed.
50
+ * We check both paths and return the one that exists.
51
+ */
52
+ getMacSocketPath() {
53
+ const homeDir = os.homedir();
54
+ const sandboxedPath = path.join(
55
+ homeDir,
56
+ "Library",
57
+ "Group Containers",
58
+ "LTZ2PFU5D6.com.bitwarden.desktop",
59
+ "s.bw"
60
+ );
61
+ const nonSandboxedPath = path.join(
62
+ homeDir,
63
+ "Library",
64
+ "Caches",
65
+ "com.bitwarden.desktop",
66
+ "s.bw"
67
+ );
68
+ try {
69
+ fs.accessSync(sandboxedPath);
70
+ return sandboxedPath;
71
+ } catch {
72
+ }
73
+ try {
74
+ fs.accessSync(nonSandboxedPath);
75
+ return nonSandboxedPath;
76
+ } catch {
77
+ }
78
+ return sandboxedPath;
79
+ }
80
+ /**
81
+ * Linux socket path - uses XDG_CACHE_HOME or ~/.cache.
82
+ */
83
+ getLinuxSocketPath() {
84
+ const cacheDir = process.env.XDG_CACHE_HOME != null ? process.env.XDG_CACHE_HOME : path.join(os.homedir(), ".cache");
85
+ return path.join(cacheDir, "com.bitwarden.desktop", "s.bw");
86
+ }
87
+ /**
88
+ * Check if the desktop app socket exists (quick availability check).
89
+ */
90
+ async isSocketAvailable() {
91
+ const socketPath = this.getSocketPath();
92
+ try {
93
+ await fs.promises.access(socketPath);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Connect to the desktop app's IPC socket.
101
+ */
102
+ async connect() {
103
+ if (this.socket != null) {
104
+ return;
105
+ }
106
+ const socketPath = this.getSocketPath();
107
+ if (DEBUG) {
108
+ console.error(`[DEBUG] Connecting to socket: ${socketPath}`);
109
+ }
110
+ return new Promise((resolve2, reject) => {
111
+ const socket = net.createConnection(socketPath);
112
+ socket.on("connect", () => {
113
+ if (DEBUG) {
114
+ console.error(`[DEBUG] Socket connected`);
115
+ }
116
+ this.socket = socket;
117
+ resolve2();
118
+ });
119
+ socket.on("data", (data) => {
120
+ if (DEBUG) {
121
+ console.error(`[DEBUG] Received raw data: ${data.length} bytes`);
122
+ }
123
+ this.processIncomingData(data);
124
+ });
125
+ socket.on("error", (err) => {
126
+ if (this.socket == null) {
127
+ reject(new Error(`Failed to connect to desktop app: ${err.message}`));
128
+ }
129
+ });
130
+ socket.on("close", () => {
131
+ this.socket = null;
132
+ this.messageBuffer = Buffer.alloc(0);
133
+ if (this.disconnectHandler) {
134
+ this.disconnectHandler();
135
+ }
136
+ });
137
+ socket.setTimeout(5e3, () => {
138
+ if (this.socket == null) {
139
+ socket.destroy();
140
+ reject(new Error("Connection to desktop app timed out"));
141
+ }
142
+ });
143
+ });
144
+ }
145
+ /**
146
+ * Disconnect from the socket.
147
+ */
148
+ disconnect() {
149
+ if (this.socket != null) {
150
+ this.socket.destroy();
151
+ this.socket = null;
152
+ }
153
+ this.messageBuffer = Buffer.alloc(0);
154
+ }
155
+ /**
156
+ * Check if currently connected.
157
+ */
158
+ isConnected() {
159
+ return this.socket != null && !this.socket.destroyed;
160
+ }
161
+ /**
162
+ * Set the handler for incoming messages.
163
+ */
164
+ onMessage(handler) {
165
+ this.messageHandler = handler;
166
+ }
167
+ /**
168
+ * Set the handler for disconnect events.
169
+ */
170
+ onDisconnect(handler) {
171
+ this.disconnectHandler = handler;
172
+ }
173
+ /**
174
+ * Send a message to the desktop app.
175
+ * Uses length-delimited protocol: 4-byte little-endian length prefix + JSON payload.
176
+ */
177
+ sendMessage(message) {
178
+ if (this.socket == null || this.socket.destroyed) {
179
+ throw new Error("Not connected to desktop app");
180
+ }
181
+ const messageStr = JSON.stringify(message);
182
+ const messageBytes = Buffer.from(messageStr, "utf8");
183
+ const buffer = Buffer.alloc(4 + messageBytes.length);
184
+ buffer.writeUInt32LE(messageBytes.length, 0);
185
+ messageBytes.copy(buffer, 4);
186
+ if (DEBUG) {
187
+ console.error(
188
+ `[DEBUG] Sending ${buffer.length} bytes (message: ${messageBytes.length} bytes)`
189
+ );
190
+ }
191
+ this.socket.write(buffer);
192
+ }
193
+ /**
194
+ * Process incoming data from the socket.
195
+ * Messages are length-delimited: 4-byte LE length + JSON payload.
196
+ */
197
+ processIncomingData(data) {
198
+ this.messageBuffer = Buffer.concat([this.messageBuffer, data]);
199
+ while (this.messageBuffer.length >= 4) {
200
+ const messageLength = this.messageBuffer.readUInt32LE(0);
201
+ if (this.messageBuffer.length < 4 + messageLength) {
202
+ break;
203
+ }
204
+ const messageBytes = this.messageBuffer.subarray(4, 4 + messageLength);
205
+ const messageStr = messageBytes.toString("utf8");
206
+ this.messageBuffer = this.messageBuffer.subarray(4 + messageLength);
207
+ try {
208
+ const message = JSON.parse(messageStr);
209
+ if (this.messageHandler) {
210
+ this.messageHandler(message);
211
+ }
212
+ } catch {
213
+ }
214
+ }
215
+ }
216
+ };
217
+
218
+ // src/ipc/native-messaging-client.ts
219
+ import * as crypto2 from "crypto";
220
+ var MESSAGE_VALID_TIMEOUT = 10 * 1e3;
221
+ var DEFAULT_TIMEOUT = 10 * 1e3;
222
+ var USER_INTERACTION_TIMEOUT = 60 * 1e3;
223
+ var DEBUG2 = process.env.BWBIO_DEBUG === "true";
224
+ var BiometricsCommands = {
225
+ AuthenticateWithBiometrics: "authenticateWithBiometrics",
226
+ GetBiometricsStatus: "getBiometricsStatus",
227
+ UnlockWithBiometricsForUser: "unlockWithBiometricsForUser",
228
+ GetBiometricsStatusForUser: "getBiometricsStatusForUser",
229
+ CanEnableBiometricUnlock: "canEnableBiometricUnlock"
230
+ };
231
+ var BiometricsStatus = /* @__PURE__ */ ((BiometricsStatus2) => {
232
+ BiometricsStatus2[BiometricsStatus2["Available"] = 0] = "Available";
233
+ BiometricsStatus2[BiometricsStatus2["UnlockNeeded"] = 1] = "UnlockNeeded";
234
+ BiometricsStatus2[BiometricsStatus2["HardwareUnavailable"] = 2] = "HardwareUnavailable";
235
+ BiometricsStatus2[BiometricsStatus2["AutoSetupNeeded"] = 3] = "AutoSetupNeeded";
236
+ BiometricsStatus2[BiometricsStatus2["ManualSetupNeeded"] = 4] = "ManualSetupNeeded";
237
+ BiometricsStatus2[BiometricsStatus2["PlatformUnsupported"] = 5] = "PlatformUnsupported";
238
+ BiometricsStatus2[BiometricsStatus2["DesktopDisconnected"] = 6] = "DesktopDisconnected";
239
+ BiometricsStatus2[BiometricsStatus2["NotEnabledLocally"] = 7] = "NotEnabledLocally";
240
+ BiometricsStatus2[BiometricsStatus2["NotEnabledInConnectedDesktopApp"] = 8] = "NotEnabledInConnectedDesktopApp";
241
+ BiometricsStatus2[BiometricsStatus2["NativeMessagingPermissionMissing"] = 9] = "NativeMessagingPermissionMissing";
242
+ return BiometricsStatus2;
243
+ })(BiometricsStatus || {});
244
+ var NativeMessagingClient = class {
245
+ connected = false;
246
+ connecting = false;
247
+ appId;
248
+ secureChannel = null;
249
+ messageId = 0;
250
+ callbacks = /* @__PURE__ */ new Map();
251
+ ipcSocket;
252
+ userId = null;
253
+ constructor(appId, userId) {
254
+ this.appId = appId;
255
+ this.userId = userId ?? null;
256
+ this.ipcSocket = new IpcSocketService();
257
+ }
258
+ /**
259
+ * Check if the desktop app is available (socket exists).
260
+ */
261
+ async isDesktopAppAvailable() {
262
+ return this.ipcSocket.isSocketAvailable();
263
+ }
264
+ /**
265
+ * Connect to the desktop app.
266
+ */
267
+ async connect() {
268
+ if (this.connected || this.connecting) {
269
+ return;
270
+ }
271
+ this.connecting = true;
272
+ try {
273
+ await this.ipcSocket.connect();
274
+ this.ipcSocket.onMessage((message) => {
275
+ this.handleMessage(message);
276
+ });
277
+ this.ipcSocket.onDisconnect(() => {
278
+ this.connected = false;
279
+ this.secureChannel = null;
280
+ for (const callback of this.callbacks.values()) {
281
+ clearTimeout(callback.timeout);
282
+ callback.rejecter(new Error("Disconnected from Desktop app"));
283
+ }
284
+ this.callbacks.clear();
285
+ });
286
+ this.connected = true;
287
+ this.connecting = false;
288
+ } catch (e) {
289
+ this.connecting = false;
290
+ throw e;
291
+ }
292
+ }
293
+ /**
294
+ * Disconnect from the desktop app.
295
+ */
296
+ disconnect() {
297
+ this.ipcSocket.disconnect();
298
+ this.connected = false;
299
+ this.secureChannel = null;
300
+ }
301
+ /**
302
+ * Send a command to the desktop app and wait for a response.
303
+ */
304
+ async callCommand(message, timeoutMs = DEFAULT_TIMEOUT) {
305
+ const messageId = this.messageId++;
306
+ const callback = new Promise((resolver, rejecter) => {
307
+ const timeout = setTimeout(() => {
308
+ if (this.callbacks.has(messageId)) {
309
+ this.callbacks.delete(messageId);
310
+ rejecter(
311
+ new Error("Message timed out waiting for Desktop app response")
312
+ );
313
+ }
314
+ }, timeoutMs);
315
+ this.callbacks.set(messageId, { resolver, rejecter, timeout });
316
+ });
317
+ message.messageId = messageId;
318
+ try {
319
+ await this.send(message);
320
+ } catch (e) {
321
+ const cb = this.callbacks.get(messageId);
322
+ if (cb) {
323
+ clearTimeout(cb.timeout);
324
+ this.callbacks.delete(messageId);
325
+ cb.rejecter(e instanceof Error ? e : new Error(String(e)));
326
+ }
327
+ }
328
+ return callback;
329
+ }
330
+ /**
331
+ * Get biometrics status from the desktop app.
332
+ */
333
+ async getBiometricsStatus() {
334
+ const response = await this.callCommand({
335
+ command: BiometricsCommands.GetBiometricsStatus
336
+ });
337
+ return response.response;
338
+ }
339
+ /**
340
+ * Get biometrics status for a specific user.
341
+ */
342
+ async getBiometricsStatusForUser(userId) {
343
+ const response = await this.callCommand({
344
+ command: BiometricsCommands.GetBiometricsStatusForUser,
345
+ userId
346
+ });
347
+ return response.response;
348
+ }
349
+ /**
350
+ * Unlock with biometrics for a specific user.
351
+ * Returns the user key if successful.
352
+ */
353
+ async unlockWithBiometricsForUser(userId) {
354
+ const response = await this.callCommand(
355
+ {
356
+ command: BiometricsCommands.UnlockWithBiometricsForUser,
357
+ userId
358
+ },
359
+ USER_INTERACTION_TIMEOUT
360
+ );
361
+ if (response.response) {
362
+ return response.userKeyB64 ?? null;
363
+ }
364
+ return null;
365
+ }
366
+ /**
367
+ * Send a message to the desktop app (encrypted if secure channel is established).
368
+ */
369
+ async send(message) {
370
+ if (!this.connected) {
371
+ await this.connect();
372
+ }
373
+ message.userId = this.userId ?? void 0;
374
+ message.timestamp = Date.now();
375
+ this.postMessage({
376
+ appId: this.appId,
377
+ message: await this.encryptMessage(message)
378
+ });
379
+ }
380
+ /**
381
+ * Encrypt a message using the secure channel's shared secret.
382
+ */
383
+ async encryptMessage(message) {
384
+ if (this.secureChannel?.sharedSecret == null) {
385
+ await this.secureCommunication();
386
+ }
387
+ const sharedSecret = this.secureChannel.sharedSecret;
388
+ const messageJson = JSON.stringify(message);
389
+ const iv = crypto2.randomBytes(16);
390
+ const cipher = crypto2.createCipheriv(
391
+ "aes-256-cbc",
392
+ sharedSecret.subarray(0, 32),
393
+ iv
394
+ );
395
+ const encrypted = Buffer.concat([
396
+ cipher.update(messageJson, "utf8"),
397
+ cipher.final()
398
+ ]);
399
+ const macKey = sharedSecret.subarray(32, 64);
400
+ const hmac = crypto2.createHmac("sha256", macKey);
401
+ hmac.update(iv);
402
+ hmac.update(encrypted);
403
+ const mac = hmac.digest();
404
+ return {
405
+ encryptionType: 2,
406
+ // AesCbc256_HmacSha256_B64
407
+ encryptedString: `2.${iv.toString("base64")}|${encrypted.toString("base64")}|${mac.toString("base64")}`,
408
+ iv: iv.toString("base64"),
409
+ data: encrypted.toString("base64"),
410
+ mac: mac.toString("base64")
411
+ };
412
+ }
413
+ /**
414
+ * Post a message to the IPC socket.
415
+ */
416
+ postMessage(message) {
417
+ try {
418
+ this.ipcSocket.sendMessage(message);
419
+ } catch (e) {
420
+ this.secureChannel = null;
421
+ this.connected = false;
422
+ throw e;
423
+ }
424
+ }
425
+ /**
426
+ * Handle incoming messages from the desktop app.
427
+ */
428
+ async handleMessage(message) {
429
+ if (DEBUG2) {
430
+ console.error(
431
+ `[DEBUG] Received message:`,
432
+ JSON.stringify(message, null, 2)
433
+ );
434
+ }
435
+ switch (message.command) {
436
+ case "setupEncryption":
437
+ if (message.appId !== this.appId) {
438
+ return;
439
+ }
440
+ await this.handleSetupEncryption(message);
441
+ break;
442
+ case "invalidateEncryption": {
443
+ if (message.appId !== this.appId) {
444
+ return;
445
+ }
446
+ const invalidError = new Error(
447
+ "Encryption channel invalidated by Desktop app"
448
+ );
449
+ if (this.secureChannel?.setupReject) {
450
+ this.secureChannel.setupReject(invalidError);
451
+ }
452
+ this.secureChannel = null;
453
+ for (const callback of this.callbacks.values()) {
454
+ clearTimeout(callback.timeout);
455
+ callback.rejecter(invalidError);
456
+ }
457
+ this.callbacks.clear();
458
+ this.connected = false;
459
+ this.ipcSocket.disconnect();
460
+ break;
461
+ }
462
+ case "wrongUserId": {
463
+ const wrongUserError = new Error(
464
+ "Account mismatch: CLI and Desktop app are logged into different accounts"
465
+ );
466
+ if (this.secureChannel?.setupReject) {
467
+ this.secureChannel.setupReject(wrongUserError);
468
+ }
469
+ this.secureChannel = null;
470
+ for (const callback of this.callbacks.values()) {
471
+ clearTimeout(callback.timeout);
472
+ callback.rejecter(wrongUserError);
473
+ }
474
+ this.callbacks.clear();
475
+ this.connected = false;
476
+ this.ipcSocket.disconnect();
477
+ break;
478
+ }
479
+ case "verifyDesktopIPCFingerprint":
480
+ await this.showFingerprint();
481
+ break;
482
+ default:
483
+ if (message.appId !== this.appId) {
484
+ return;
485
+ }
486
+ if (message.message != null) {
487
+ await this.handleEncryptedMessage(message.message);
488
+ }
489
+ }
490
+ }
491
+ /**
492
+ * Handle the setupEncryption response from the desktop app.
493
+ */
494
+ async handleSetupEncryption(message) {
495
+ if (DEBUG2) {
496
+ console.error(
497
+ `[DEBUG] handleSetupEncryption called, sharedSecret present: ${message.sharedSecret != null}`
498
+ );
499
+ }
500
+ if (message.sharedSecret == null) {
501
+ if (DEBUG2) {
502
+ console.error(`[DEBUG] No sharedSecret in message`);
503
+ }
504
+ return;
505
+ }
506
+ if (this.secureChannel == null) {
507
+ if (DEBUG2) {
508
+ console.error(`[DEBUG] No secureChannel setup`);
509
+ }
510
+ return;
511
+ }
512
+ const encrypted = Buffer.from(message.sharedSecret, "base64");
513
+ if (DEBUG2) {
514
+ console.error(
515
+ `[DEBUG] Encrypted sharedSecret length: ${encrypted.length}`
516
+ );
517
+ }
518
+ const decrypted = crypto2.privateDecrypt(
519
+ {
520
+ key: this.secureChannel.privateKey,
521
+ oaepHash: "sha1",
522
+ padding: crypto2.constants.RSA_PKCS1_OAEP_PADDING
523
+ },
524
+ encrypted
525
+ );
526
+ this.secureChannel.sharedSecret = decrypted;
527
+ if (DEBUG2) {
528
+ console.error(
529
+ `[DEBUG] Decrypted sharedSecret length: ${this.secureChannel.sharedSecret.length}`
530
+ );
531
+ }
532
+ if (this.secureChannel.setupResolve) {
533
+ this.secureChannel.setupResolve();
534
+ }
535
+ }
536
+ /**
537
+ * Handle an encrypted message from the desktop app.
538
+ */
539
+ async handleEncryptedMessage(rawMessage) {
540
+ if (this.secureChannel?.sharedSecret == null) {
541
+ return;
542
+ }
543
+ let message;
544
+ if ("encryptionType" in rawMessage || "encryptedString" in rawMessage) {
545
+ const encMsg = rawMessage;
546
+ const iv = Buffer.from(encMsg.iv, "base64");
547
+ const data = Buffer.from(encMsg.data, "base64");
548
+ const mac = Buffer.from(encMsg.mac, "base64");
549
+ const sharedSecret = this.secureChannel.sharedSecret;
550
+ const encKey = sharedSecret.subarray(0, 32);
551
+ const macKey = sharedSecret.subarray(32, 64);
552
+ const hmac = crypto2.createHmac("sha256", macKey);
553
+ hmac.update(iv);
554
+ hmac.update(data);
555
+ const expectedMac = hmac.digest();
556
+ if (!crypto2.timingSafeEqual(mac, expectedMac)) {
557
+ throw new Error("Message integrity check failed");
558
+ }
559
+ const decipher = crypto2.createDecipheriv("aes-256-cbc", encKey, iv);
560
+ const decrypted = Buffer.concat([
561
+ decipher.update(data),
562
+ decipher.final()
563
+ ]);
564
+ message = JSON.parse(decrypted.toString("utf8"));
565
+ } else {
566
+ message = rawMessage;
567
+ }
568
+ this.processDecryptedMessage(message);
569
+ }
570
+ /**
571
+ * Process a decrypted message and resolve any pending callbacks.
572
+ */
573
+ processDecryptedMessage(message) {
574
+ if (DEBUG2) {
575
+ console.error(
576
+ `[DEBUG] Decrypted message:`,
577
+ JSON.stringify(message, null, 2)
578
+ );
579
+ }
580
+ if (Math.abs(message.timestamp - Date.now()) > MESSAGE_VALID_TIMEOUT) {
581
+ if (DEBUG2) {
582
+ console.error(
583
+ `[DEBUG] Message too old, ignoring. Timestamp: ${message.timestamp}, now: ${Date.now()}`
584
+ );
585
+ }
586
+ return;
587
+ }
588
+ const messageId = message.messageId;
589
+ if (this.callbacks.has(messageId)) {
590
+ const callback = this.callbacks.get(messageId);
591
+ clearTimeout(callback.timeout);
592
+ this.callbacks.delete(messageId);
593
+ callback.resolver(message);
594
+ } else if (DEBUG2) {
595
+ console.error(`[DEBUG] No callback found for messageId: ${messageId}`);
596
+ }
597
+ }
598
+ /**
599
+ * Set up secure communication with RSA key exchange.
600
+ */
601
+ async secureCommunication() {
602
+ const { publicKey, privateKey } = crypto2.generateKeyPairSync("rsa", {
603
+ modulusLength: 2048
604
+ });
605
+ const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
606
+ const publicKeyB64 = publicKeyDer.toString("base64");
607
+ const setupMessage = {
608
+ appId: this.appId,
609
+ message: {
610
+ command: "setupEncryption",
611
+ publicKey: publicKeyB64,
612
+ userId: this.userId ?? void 0,
613
+ messageId: this.messageId++,
614
+ timestamp: Date.now()
615
+ }
616
+ };
617
+ if (DEBUG2) {
618
+ console.error(
619
+ `[DEBUG] Sending setupEncryption:`,
620
+ JSON.stringify(
621
+ {
622
+ ...setupMessage,
623
+ message: {
624
+ ...setupMessage.message,
625
+ publicKey: `${publicKeyB64.slice(0, 50)}...`
626
+ }
627
+ },
628
+ null,
629
+ 2
630
+ )
631
+ );
632
+ }
633
+ this.postMessage(setupMessage);
634
+ return new Promise((resolve2, reject) => {
635
+ this.secureChannel = {
636
+ publicKey,
637
+ privateKey,
638
+ setupResolve: resolve2,
639
+ setupReject: reject
640
+ };
641
+ setTimeout(() => {
642
+ if (this.secureChannel && !this.secureChannel.sharedSecret) {
643
+ reject(new Error("Secure channel setup timed out"));
644
+ }
645
+ }, DEFAULT_TIMEOUT);
646
+ });
647
+ }
648
+ /**
649
+ * Display the fingerprint for verification.
650
+ */
651
+ async showFingerprint() {
652
+ if (this.secureChannel?.publicKey == null) {
653
+ return;
654
+ }
655
+ const publicKeyDer = this.secureChannel.publicKey.export({
656
+ type: "spki",
657
+ format: "der"
658
+ });
659
+ const hash = crypto2.createHash("sha256").update(publicKeyDer).digest();
660
+ const fingerprint = hash.toString("hex").slice(0, 25).toUpperCase();
661
+ const formatted = fingerprint.match(/.{1,5}/g)?.join("-") || fingerprint;
662
+ const dim = "\x1B[2m";
663
+ const cyan = "\x1B[36m";
664
+ const bold = "\x1B[1m";
665
+ const reset = "\x1B[0m";
666
+ console.error("");
667
+ console.error(`${bold}Bitwarden Desktop App Verification${reset}`);
668
+ console.error(
669
+ "Verify this fingerprint matches the one shown in the Desktop app:"
670
+ );
671
+ console.error("");
672
+ console.error(`${dim} ${cyan}${formatted}${reset}`);
673
+ console.error("");
674
+ console.error("Accept the connection in the Desktop app to continue.");
675
+ console.error("");
676
+ }
677
+ };
678
+
679
+ // src/log.ts
680
+ function log(message) {
681
+ if (process.env.BW_QUIET !== "true") {
682
+ console.error(message);
683
+ }
684
+ }
685
+ function logVerbose(message) {
686
+ if (process.env.BWBIO_VERBOSE === "true" && process.env.BW_QUIET !== "true") {
687
+ console.error(message);
688
+ }
689
+ }
690
+
691
+ // src/session-storage.ts
692
+ import * as crypto3 from "crypto";
693
+ import * as fs2 from "fs";
694
+ import * as os2 from "os";
695
+ import * as path2 from "path";
696
+ var ENCRYPTION_TYPE = 2;
697
+ function getCliDataDir() {
698
+ if (process.env.BITWARDENCLI_APPDATA_DIR) {
699
+ return path2.resolve(process.env.BITWARDENCLI_APPDATA_DIR);
700
+ }
701
+ const platform3 = os2.platform();
702
+ const homeDir = os2.homedir();
703
+ if (platform3 === "darwin") {
704
+ return path2.join(homeDir, "Library/Application Support/Bitwarden CLI");
705
+ } else if (platform3 === "win32") {
706
+ return path2.join(process.env.APPDATA ?? homeDir, "Bitwarden CLI");
707
+ } else {
708
+ const configDir = process.env.XDG_CONFIG_HOME ?? path2.join(homeDir, ".config");
709
+ return path2.join(configDir, "Bitwarden CLI");
710
+ }
711
+ }
712
+ function generateSessionKey() {
713
+ const keyBytes = crypto3.randomBytes(64);
714
+ return keyBytes.toString("base64");
715
+ }
716
+ function encryptWithSessionKey(data, sessionKey) {
717
+ const keyBytes = Buffer.from(sessionKey, "base64");
718
+ if (keyBytes.length !== 64) {
719
+ throw new Error("Session key must be 64 bytes");
720
+ }
721
+ const encKey = keyBytes.subarray(0, 32);
722
+ const macKey = keyBytes.subarray(32, 64);
723
+ const iv = crypto3.randomBytes(16);
724
+ const cipher = crypto3.createCipheriv("aes-256-cbc", encKey, iv);
725
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
726
+ const hmac = crypto3.createHmac("sha256", macKey);
727
+ hmac.update(iv);
728
+ hmac.update(encrypted);
729
+ const mac = hmac.digest();
730
+ const result = Buffer.alloc(1 + 16 + 32 + encrypted.length);
731
+ result.writeUInt8(ENCRYPTION_TYPE, 0);
732
+ iv.copy(result, 1);
733
+ mac.copy(result, 17);
734
+ encrypted.copy(result, 49);
735
+ return result.toString("base64");
736
+ }
737
+ function readCliData() {
738
+ const dataPath = path2.join(getCliDataDir(), "data.json");
739
+ try {
740
+ const content = fs2.readFileSync(dataPath, "utf-8");
741
+ return content ? JSON.parse(content) : {};
742
+ } catch {
743
+ return {};
744
+ }
745
+ }
746
+ function getActiveUserId() {
747
+ const data = readCliData();
748
+ const userId = data.global_account_activeAccountId;
749
+ return typeof userId === "string" ? userId : null;
750
+ }
751
+ function writeCliData(data) {
752
+ const dataDir = getCliDataDir();
753
+ const dataPath = path2.join(dataDir, "data.json");
754
+ if (!fs2.existsSync(dataDir)) {
755
+ fs2.mkdirSync(dataDir, { recursive: true, mode: 448 });
756
+ }
757
+ fs2.writeFileSync(dataPath, JSON.stringify(data, null, 2), { mode: 384 });
758
+ }
759
+ function storeUserKeyForSession(userKeyB64, userId, sessionKey) {
760
+ const userKeyBytes = Buffer.from(userKeyB64, "base64");
761
+ const encryptedUserKey = encryptWithSessionKey(userKeyBytes, sessionKey);
762
+ const storageKey = `__PROTECTED__${userId}_user_auto`;
763
+ const data = readCliData();
764
+ data[storageKey] = encryptedUserKey;
765
+ writeCliData(data);
766
+ }
767
+
768
+ // src/biometrics.ts
769
+ function getBiometricMethodName() {
770
+ switch (process.platform) {
771
+ case "darwin":
772
+ return "Touch ID";
773
+ case "win32":
774
+ return "Windows Hello";
775
+ default:
776
+ return "Polkit";
777
+ }
778
+ }
779
+ function generateAppId() {
780
+ return `bwbio-${crypto4.randomUUID()}`;
781
+ }
782
+ async function attemptBiometricUnlock(options = {}) {
783
+ const userId = options.userId || getActiveUserId();
784
+ if (!userId) {
785
+ logVerbose(
786
+ "Biometric unlock unavailable: No user ID available - please log in first"
787
+ );
788
+ return { success: false };
789
+ }
790
+ const appId = generateAppId();
791
+ const client = new NativeMessagingClient(appId, userId);
792
+ try {
793
+ const available = await client.isDesktopAppAvailable();
794
+ if (!available) {
795
+ logVerbose(
796
+ "Biometric unlock unavailable: Bitwarden Desktop app is not running"
797
+ );
798
+ return { success: false };
799
+ }
800
+ logVerbose("Connecting to Bitwarden Desktop...");
801
+ await client.connect();
802
+ logVerbose("Checking biometrics status...");
803
+ const userStatus = await client.getBiometricsStatusForUser(userId);
804
+ if (userStatus !== 0 /* Available */) {
805
+ const statusName = BiometricsStatus[userStatus] || `Unknown(${userStatus})`;
806
+ logVerbose(`Biometric unlock unavailable: ${statusName}`);
807
+ return { success: false };
808
+ }
809
+ log(
810
+ `Authenticate with ${getBiometricMethodName()} on Desktop app to continue...`
811
+ );
812
+ const userKey = await client.unlockWithBiometricsForUser(userId);
813
+ if (!userKey) {
814
+ log("Biometric unlock was denied or failed");
815
+ return { success: false };
816
+ }
817
+ return {
818
+ success: true,
819
+ userKeyB64: userKey,
820
+ userId
821
+ };
822
+ } catch (err) {
823
+ const error = err instanceof Error ? err.message : String(err);
824
+ log(`Biometric unlock failed: ${error}`);
825
+ return { success: false };
826
+ } finally {
827
+ client.disconnect();
828
+ }
829
+ }
830
+
831
+ // src/passthrough.ts
832
+ var PASSTHROUGH_COMMANDS = /* @__PURE__ */ new Set([
833
+ "login",
834
+ "logout",
835
+ "lock",
836
+ "config",
837
+ "update",
838
+ "completion",
839
+ "status",
840
+ "serve"
841
+ ]);
842
+ var PASSTHROUGH_FLAGS = /* @__PURE__ */ new Set(["--help", "-h", "--version", "-v"]);
843
+ function isPassthroughCommand(args2) {
844
+ if (args2.length === 0) {
845
+ return true;
846
+ }
847
+ const firstArg = args2[0];
848
+ if (args2.some((arg) => PASSTHROUGH_FLAGS.has(arg))) {
849
+ return true;
850
+ }
851
+ if (PASSTHROUGH_COMMANDS.has(firstArg)) {
852
+ return true;
853
+ }
854
+ return false;
855
+ }
856
+
857
+ // src/main.ts
858
+ function writeLn(s) {
859
+ if (process.env.BW_QUIET !== "true") {
860
+ process.stdout.write(`${s}
861
+ `);
862
+ }
863
+ }
864
+ function getBwPath() {
865
+ return "bw";
866
+ }
867
+ async function executeBw(args2, sessionKey) {
868
+ return new Promise((resolve2) => {
869
+ const env = { ...process.env };
870
+ if (sessionKey) {
871
+ env.BW_SESSION = sessionKey;
872
+ }
873
+ const child = spawn(getBwPath(), args2, {
874
+ stdio: "inherit",
875
+ env,
876
+ // On Windows, spawn needs shell to resolve .cmd wrappers (e.g. bw.cmd)
877
+ shell: process.platform === "win32"
878
+ });
879
+ child.on("error", (err) => {
880
+ console.error(`Failed to execute bw: ${err.message}`);
881
+ resolve2(1);
882
+ });
883
+ child.on("close", (code) => {
884
+ resolve2(code ?? 0);
885
+ });
886
+ });
887
+ }
888
+ async function handleUnlock(args2, sessionKey) {
889
+ const isRaw = args2.includes("--raw");
890
+ if (isRaw) {
891
+ writeLn(sessionKey);
892
+ } else {
893
+ writeLn(`export BW_SESSION="${sessionKey}"`);
894
+ writeLn(`# Run this command to set the session: eval $(bwbio unlock)`);
895
+ }
896
+ return 0;
897
+ }
898
+ async function main(args2) {
899
+ if (args2.includes("--quiet")) {
900
+ process.env.BW_QUIET = "true";
901
+ }
902
+ if (args2.includes("--nointeraction")) {
903
+ process.env.BW_NOINTERACTION = "true";
904
+ }
905
+ if (process.env.BW_SESSION || process.env.BW_NOINTERACTION === "true" || isPassthroughCommand(args2)) {
906
+ return executeBw(args2);
907
+ }
908
+ const result = await attemptBiometricUnlock();
909
+ if (result.success) {
910
+ const sessionKey = generateSessionKey();
911
+ storeUserKeyForSession(result.userKeyB64, result.userId, sessionKey);
912
+ if (args2[0] === "unlock") {
913
+ return handleUnlock(args2, sessionKey);
914
+ }
915
+ return executeBw(args2, sessionKey);
916
+ }
917
+ return executeBw(args2);
918
+ }
919
+
920
+ // src/index.ts
921
+ var args = process.argv.slice(2);
922
+ main(args).then((code) => {
923
+ process.exit(code);
924
+ }).catch((err) => {
925
+ console.error("Unexpected error:", err);
926
+ process.exit(1);
927
+ });
928
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/main.ts","../src/biometrics.ts","../src/ipc/ipc-socket.service.ts","../src/ipc/native-messaging-client.ts","../src/log.ts","../src/session-storage.ts","../src/passthrough.ts","../src/index.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { attemptBiometricUnlock } from \"./biometrics\";\nimport { isPassthroughCommand } from \"./passthrough\";\nimport { generateSessionKey, storeUserKeyForSession } from \"./session-storage\";\n\nfunction writeLn(s: string): void {\n if (process.env.BW_QUIET !== \"true\") {\n process.stdout.write(`${s}\\n`);\n }\n}\n\n/**\n * Get the path to the official bw CLI.\n *\n * Shell aliases don't apply when spawning via child_process,\n * so aliasing bwbio as bw won't cause a conflict here.\n */\nfunction getBwPath(): string {\n return \"bw\";\n}\n\n/**\n * Execute the official bw CLI with the given arguments.\n *\n * @param args - Command line arguments to pass to bw\n * @param sessionKey - Optional BW_SESSION value to set\n * @returns Exit code from bw\n */\nasync function executeBw(args: string[], sessionKey?: string): Promise<number> {\n return new Promise((resolve) => {\n const env = { ...process.env };\n\n if (sessionKey) {\n env.BW_SESSION = sessionKey;\n }\n\n const child = spawn(getBwPath(), args, {\n stdio: \"inherit\",\n env,\n // On Windows, spawn needs shell to resolve .cmd wrappers (e.g. bw.cmd)\n shell: process.platform === \"win32\",\n });\n\n child.on(\"error\", (err) => {\n console.error(`Failed to execute bw: ${err.message}`);\n resolve(1);\n });\n\n child.on(\"close\", (code) => {\n resolve(code ?? 0);\n });\n });\n}\n\n/**\n * Handle the 'unlock' command specially to output BW_SESSION export.\n */\nasync function handleUnlock(\n args: string[],\n sessionKey: string,\n): Promise<number> {\n // Check if --raw flag is present\n const isRaw = args.includes(\"--raw\");\n\n if (isRaw) {\n writeLn(sessionKey);\n } else {\n writeLn(`export BW_SESSION=\"${sessionKey}\"`);\n writeLn(`# Run this command to set the session: eval $(bwbio unlock)`);\n }\n\n return 0;\n}\n\n/**\n * Main entry point for the CLI wrapper.\n *\n * Attempts biometric unlock via the Desktop app, then delegates to bw.\n * Skips biometrics when BW_SESSION is set, in non-interactive mode, or for passthrough commands.\n */\nexport async function main(args: string[]): Promise<number> {\n // Mirror --quiet and --nointeraction flags to env vars (same as bw CLI)\n if (args.includes(\"--quiet\")) {\n process.env.BW_QUIET = \"true\";\n }\n if (args.includes(\"--nointeraction\")) {\n process.env.BW_NOINTERACTION = \"true\";\n }\n\n // Skip biometric unlock when not needed or not possible\n if (\n process.env.BW_SESSION ||\n process.env.BW_NOINTERACTION === \"true\" ||\n isPassthroughCommand(args)\n ) {\n return executeBw(args);\n }\n\n // Attempt biometric unlock\n const result = await attemptBiometricUnlock();\n\n if (result.success) {\n // Generate a new session key and store the user key\n const sessionKey = generateSessionKey();\n storeUserKeyForSession(result.userKeyB64, result.userId, sessionKey);\n\n // Check if this is an explicit 'unlock' command\n if (args[0] === \"unlock\") {\n return handleUnlock(args, sessionKey);\n }\n\n // Execute the requested command with the session\n return executeBw(args, sessionKey);\n }\n\n // Biometric unlock failed or unavailable - fall back to regular bw CLI\n return executeBw(args);\n}\n","import * as crypto from \"node:crypto\";\nimport { BiometricsStatus, NativeMessagingClient } from \"./ipc\";\nimport { log, logVerbose } from \"./log\";\nimport { getActiveUserId } from \"./session-storage\";\n\n/**\n * Get the platform-specific biometric method name.\n */\nfunction getBiometricMethodName(): string {\n switch (process.platform) {\n case \"darwin\":\n return \"Touch ID\";\n case \"win32\":\n return \"Windows Hello\";\n default:\n return \"Polkit\";\n }\n}\n\n/**\n * Result of a biometric unlock attempt.\n */\nexport type BiometricUnlockResult =\n | {\n success: true;\n /** The user's encryption key (base64 encoded) - NOT the session key */\n userKeyB64: string;\n userId: string;\n }\n | {\n success: false;\n };\n\n/**\n * Options for biometric unlock.\n */\nexport interface BiometricUnlockOptions {\n userId?: string;\n}\n\n/**\n * Generate a unique app ID for this CLI instance.\n */\nfunction generateAppId(): string {\n return `bwbio-${crypto.randomUUID()}`;\n}\n\n/**\n * Attempt to unlock the vault using biometrics via the Desktop app.\n */\nexport async function attemptBiometricUnlock(\n options: BiometricUnlockOptions = {},\n): Promise<BiometricUnlockResult> {\n // Get the user ID from CLI data - this is required for the desktop app\n const userId = options.userId || getActiveUserId();\n if (!userId) {\n logVerbose(\n \"Biometric unlock unavailable: No user ID available - please log in first\",\n );\n return { success: false };\n }\n\n const appId = generateAppId();\n const client = new NativeMessagingClient(appId, userId);\n\n try {\n // Check if desktop app is available\n const available = await client.isDesktopAppAvailable();\n if (!available) {\n logVerbose(\n \"Biometric unlock unavailable: Bitwarden Desktop app is not running\",\n );\n return { success: false };\n }\n\n logVerbose(\"Connecting to Bitwarden Desktop...\");\n\n await client.connect();\n\n // Get user-specific biometrics status\n logVerbose(\"Checking biometrics status...\");\n\n const userStatus = await client.getBiometricsStatusForUser(userId);\n\n // BiometricsStatus is an enum - Available (0) means biometrics can be used\n if (userStatus !== BiometricsStatus.Available) {\n const statusName =\n BiometricsStatus[userStatus] || `Unknown(${userStatus})`;\n logVerbose(`Biometric unlock unavailable: ${statusName}`);\n return { success: false };\n }\n\n // Request biometric unlock\n log(\n `Authenticate with ${getBiometricMethodName()} on Desktop app to continue...`,\n );\n\n const userKey = await client.unlockWithBiometricsForUser(userId);\n\n if (!userKey) {\n log(\"Biometric unlock was denied or failed\");\n return { success: false };\n }\n\n return {\n success: true,\n userKeyB64: userKey,\n userId,\n };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n log(`Biometric unlock failed: ${error}`);\n return { success: false };\n } finally {\n client.disconnect();\n }\n}\n\n/**\n * Check biometrics availability without attempting unlock.\n */\nexport async function checkBiometricsAvailable(): Promise<boolean> {\n const userId = getActiveUserId();\n if (!userId) {\n return false;\n }\n\n const appId = generateAppId();\n const client = new NativeMessagingClient(appId, userId);\n\n try {\n const available = await client.isDesktopAppAvailable();\n if (!available) {\n return false;\n }\n\n await client.connect();\n const status = await client.getBiometricsStatusForUser(userId);\n return status === BiometricsStatus.Available;\n } catch {\n return false;\n } finally {\n client.disconnect();\n }\n}\n","import * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as net from \"node:net\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\n\nconst DEBUG = process.env.BWBIO_DEBUG === \"true\";\n\n/**\n * Platform-specific IPC socket service for connecting to the Bitwarden desktop app.\n *\n * The desktop app listens on a Unix domain socket (macOS/Linux) or named pipe (Windows).\n * This service provides a platform-agnostic way to connect and communicate with it.\n */\nexport class IpcSocketService {\n private socket: net.Socket | null = null;\n private messageBuffer: Buffer = Buffer.alloc(0);\n private messageHandler: ((message: unknown) => void) | null = null;\n private disconnectHandler: (() => void) | null = null;\n\n /**\n * Get the IPC socket path for the current platform.\n * This mirrors the logic in desktop_native/core/src/ipc/mod.rs\n */\n getSocketPath(): string {\n if (process.env.BWBIO_IPC_SOCKET_PATH) {\n return process.env.BWBIO_IPC_SOCKET_PATH;\n }\n\n const platform = os.platform();\n\n if (platform === \"win32\") {\n return this.getWindowsSocketPath();\n }\n\n if (platform === \"darwin\") {\n return this.getMacSocketPath();\n }\n\n // Linux: use XDG cache directory or fallback\n return this.getLinuxSocketPath();\n }\n\n /**\n * Windows named pipe path - uses hash of home directory.\n */\n private getWindowsSocketPath(): string {\n const homeDir = os.homedir();\n const hash = crypto.createHash(\"sha256\").update(homeDir).digest();\n // Use URL-safe base64 without padding (like Rust's URL_SAFE_NO_PAD)\n const hashB64 = hash\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n return `\\\\\\\\.\\\\pipe\\\\${hashB64}.s.bw`;\n }\n\n /**\n * Get the socket path on macOS.\n * The Desktop app can be sandboxed (Mac App Store) or non-sandboxed.\n * We check both paths and return the one that exists.\n */\n private getMacSocketPath(): string {\n const homeDir = os.homedir();\n\n // Path for sandboxed Desktop app (Mac App Store version)\n const sandboxedPath = path.join(\n homeDir,\n \"Library\",\n \"Group Containers\",\n \"LTZ2PFU5D6.com.bitwarden.desktop\",\n \"s.bw\",\n );\n\n // Path for non-sandboxed Desktop app\n const nonSandboxedPath = path.join(\n homeDir,\n \"Library\",\n \"Caches\",\n \"com.bitwarden.desktop\",\n \"s.bw\",\n );\n\n // Check sandboxed path first (most common for Mac App Store users)\n try {\n fs.accessSync(sandboxedPath);\n return sandboxedPath;\n } catch {\n // Socket not found at sandboxed path\n }\n\n // Check non-sandboxed path\n try {\n fs.accessSync(nonSandboxedPath);\n return nonSandboxedPath;\n } catch {\n // Socket not found at non-sandboxed path either\n }\n\n // Default to sandboxed path\n return sandboxedPath;\n }\n\n /**\n * Linux socket path - uses XDG_CACHE_HOME or ~/.cache.\n */\n private getLinuxSocketPath(): string {\n const cacheDir =\n process.env.XDG_CACHE_HOME != null\n ? process.env.XDG_CACHE_HOME\n : path.join(os.homedir(), \".cache\");\n return path.join(cacheDir, \"com.bitwarden.desktop\", \"s.bw\");\n }\n\n /**\n * Check if the desktop app socket exists (quick availability check).\n */\n async isSocketAvailable(): Promise<boolean> {\n const socketPath = this.getSocketPath();\n try {\n await fs.promises.access(socketPath);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Connect to the desktop app's IPC socket.\n */\n async connect(): Promise<void> {\n if (this.socket != null) {\n return;\n }\n\n const socketPath = this.getSocketPath();\n\n if (DEBUG) {\n console.error(`[DEBUG] Connecting to socket: ${socketPath}`);\n }\n\n return new Promise((resolve, reject) => {\n const socket = net.createConnection(socketPath);\n\n socket.on(\"connect\", () => {\n if (DEBUG) {\n console.error(`[DEBUG] Socket connected`);\n }\n this.socket = socket;\n resolve();\n });\n\n socket.on(\"data\", (data: Buffer) => {\n if (DEBUG) {\n console.error(`[DEBUG] Received raw data: ${data.length} bytes`);\n }\n this.processIncomingData(data);\n });\n\n socket.on(\"error\", (err) => {\n if (this.socket == null) {\n reject(new Error(`Failed to connect to desktop app: ${err.message}`));\n }\n });\n\n socket.on(\"close\", () => {\n this.socket = null;\n this.messageBuffer = Buffer.alloc(0);\n if (this.disconnectHandler) {\n this.disconnectHandler();\n }\n });\n\n // Timeout for initial connection\n socket.setTimeout(5000, () => {\n if (this.socket == null) {\n socket.destroy();\n reject(new Error(\"Connection to desktop app timed out\"));\n }\n });\n });\n }\n\n /**\n * Disconnect from the socket.\n */\n disconnect(): void {\n if (this.socket != null) {\n this.socket.destroy();\n this.socket = null;\n }\n this.messageBuffer = Buffer.alloc(0);\n }\n\n /**\n * Check if currently connected.\n */\n isConnected(): boolean {\n return this.socket != null && !this.socket.destroyed;\n }\n\n /**\n * Set the handler for incoming messages.\n */\n onMessage(handler: (message: unknown) => void): void {\n this.messageHandler = handler;\n }\n\n /**\n * Set the handler for disconnect events.\n */\n onDisconnect(handler: () => void): void {\n this.disconnectHandler = handler;\n }\n\n /**\n * Send a message to the desktop app.\n * Uses length-delimited protocol: 4-byte little-endian length prefix + JSON payload.\n */\n sendMessage(message: unknown): void {\n if (this.socket == null || this.socket.destroyed) {\n throw new Error(\"Not connected to desktop app\");\n }\n\n const messageStr = JSON.stringify(message);\n const messageBytes = Buffer.from(messageStr, \"utf8\");\n\n // Create buffer with 4-byte length prefix (little-endian)\n const buffer = Buffer.alloc(4 + messageBytes.length);\n buffer.writeUInt32LE(messageBytes.length, 0);\n messageBytes.copy(buffer, 4);\n\n if (DEBUG) {\n console.error(\n `[DEBUG] Sending ${buffer.length} bytes (message: ${messageBytes.length} bytes)`,\n );\n }\n\n this.socket.write(buffer);\n }\n\n /**\n * Process incoming data from the socket.\n * Messages are length-delimited: 4-byte LE length + JSON payload.\n */\n private processIncomingData(data: Buffer): void {\n this.messageBuffer = Buffer.concat([this.messageBuffer, data]);\n\n // Process all complete messages in the buffer\n while (this.messageBuffer.length >= 4) {\n const messageLength = this.messageBuffer.readUInt32LE(0);\n\n // Check if we have the full message\n if (this.messageBuffer.length < 4 + messageLength) {\n break;\n }\n\n // Extract and parse the message\n const messageBytes = this.messageBuffer.subarray(4, 4 + messageLength);\n const messageStr = messageBytes.toString(\"utf8\");\n\n // Update buffer to remove processed message\n this.messageBuffer = this.messageBuffer.subarray(4 + messageLength);\n\n try {\n const message = JSON.parse(messageStr);\n if (this.messageHandler) {\n this.messageHandler(message);\n }\n } catch {\n // Failed to parse message\n }\n }\n }\n}\n","import * as crypto from \"node:crypto\";\nimport { IpcSocketService } from \"./ipc-socket.service\";\n\nconst MESSAGE_VALID_TIMEOUT = 10 * 1000; // 10 seconds\nconst DEFAULT_TIMEOUT = 10 * 1000; // 10 seconds for protocol messages\nconst USER_INTERACTION_TIMEOUT = 60 * 1000; // 60 seconds for biometric prompts\n\nconst DEBUG = process.env.BWBIO_DEBUG === \"true\";\n\n/**\n * Biometrics commands matching the desktop app's expected commands.\n */\nexport const BiometricsCommands = {\n AuthenticateWithBiometrics: \"authenticateWithBiometrics\",\n GetBiometricsStatus: \"getBiometricsStatus\",\n UnlockWithBiometricsForUser: \"unlockWithBiometricsForUser\",\n GetBiometricsStatusForUser: \"getBiometricsStatusForUser\",\n CanEnableBiometricUnlock: \"canEnableBiometricUnlock\",\n} as const;\n\n/**\n * Biometrics status enum matching the desktop app's BiometricsStatus.\n */\nexport enum BiometricsStatus {\n Available = 0,\n UnlockNeeded = 1,\n HardwareUnavailable = 2,\n AutoSetupNeeded = 3,\n ManualSetupNeeded = 4,\n PlatformUnsupported = 5,\n DesktopDisconnected = 6,\n NotEnabledLocally = 7,\n NotEnabledInConnectedDesktopApp = 8,\n NativeMessagingPermissionMissing = 9,\n}\n\ntype Message = {\n command: string;\n messageId?: number;\n userId?: string;\n timestamp?: number;\n publicKey?: string;\n};\n\ntype OuterMessage = {\n message: Message | EncryptedMessage;\n appId: string;\n};\n\ntype EncryptedMessage = {\n encryptedString: string;\n encryptionType: number;\n data: string;\n iv: string;\n mac: string;\n};\n\ntype ReceivedMessage = {\n timestamp: number;\n command: string;\n messageId: number;\n response?: unknown;\n userKeyB64?: string;\n};\n\ntype ReceivedMessageOuter = {\n command: string;\n appId: string;\n messageId?: number;\n message?: ReceivedMessage | EncryptedMessage;\n sharedSecret?: string;\n};\n\ntype Callback = {\n resolver: (value: ReceivedMessage) => void;\n rejecter: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n};\n\ntype SecureChannel = {\n privateKey: crypto.KeyObject;\n publicKey: crypto.KeyObject;\n sharedSecret?: Buffer;\n setupResolve?: () => void;\n setupReject?: (reason?: unknown) => void;\n};\n\n/**\n * Native messaging client for communicating with the Bitwarden desktop app.\n *\n * This implements the same IPC protocol used by the browser extension:\n * 1. Connect to the desktop app via Unix socket / named pipe\n * 2. Set up encrypted communication using RSA key exchange\n * 3. Send/receive encrypted commands (biometric unlock, status checks, etc.)\n */\nexport class NativeMessagingClient {\n private connected = false;\n private connecting = false;\n private appId: string;\n\n private secureChannel: SecureChannel | null = null;\n private messageId = 0;\n private callbacks = new Map<number, Callback>();\n\n private ipcSocket: IpcSocketService;\n private userId: string | null = null;\n\n constructor(appId: string, userId?: string) {\n this.appId = appId;\n this.userId = userId ?? null;\n this.ipcSocket = new IpcSocketService();\n }\n\n /**\n * Check if the desktop app is available (socket exists).\n */\n async isDesktopAppAvailable(): Promise<boolean> {\n return this.ipcSocket.isSocketAvailable();\n }\n\n /**\n * Connect to the desktop app.\n */\n async connect(): Promise<void> {\n if (this.connected || this.connecting) {\n return;\n }\n\n this.connecting = true;\n\n try {\n await this.ipcSocket.connect();\n\n // Set up message handler\n this.ipcSocket.onMessage((message) => {\n this.handleMessage(message as ReceivedMessageOuter);\n });\n\n this.ipcSocket.onDisconnect(() => {\n this.connected = false;\n this.secureChannel = null;\n\n // Clear timeouts and reject all pending callbacks\n for (const callback of this.callbacks.values()) {\n clearTimeout(callback.timeout);\n callback.rejecter(new Error(\"Disconnected from Desktop app\"));\n }\n this.callbacks.clear();\n });\n\n this.connected = true;\n this.connecting = false;\n } catch (e) {\n this.connecting = false;\n throw e;\n }\n }\n\n /**\n * Disconnect from the desktop app.\n */\n disconnect(): void {\n this.ipcSocket.disconnect();\n this.connected = false;\n this.secureChannel = null;\n }\n\n /**\n * Send a command to the desktop app and wait for a response.\n */\n async callCommand(\n message: Message,\n timeoutMs: number = DEFAULT_TIMEOUT,\n ): Promise<ReceivedMessage> {\n const messageId = this.messageId++;\n\n const callback = new Promise<ReceivedMessage>((resolver, rejecter) => {\n const timeout = setTimeout(() => {\n if (this.callbacks.has(messageId)) {\n this.callbacks.delete(messageId);\n rejecter(\n new Error(\"Message timed out waiting for Desktop app response\"),\n );\n }\n }, timeoutMs);\n\n this.callbacks.set(messageId, { resolver, rejecter, timeout });\n });\n\n message.messageId = messageId;\n\n try {\n await this.send(message);\n } catch (e) {\n const cb = this.callbacks.get(messageId);\n if (cb) {\n clearTimeout(cb.timeout);\n this.callbacks.delete(messageId);\n cb.rejecter(e instanceof Error ? e : new Error(String(e)));\n }\n }\n\n return callback;\n }\n\n /**\n * Get biometrics status from the desktop app.\n */\n async getBiometricsStatus(): Promise<BiometricsStatus> {\n const response = await this.callCommand({\n command: BiometricsCommands.GetBiometricsStatus,\n });\n return response.response as BiometricsStatus;\n }\n\n /**\n * Get biometrics status for a specific user.\n */\n async getBiometricsStatusForUser(userId: string): Promise<BiometricsStatus> {\n const response = await this.callCommand({\n command: BiometricsCommands.GetBiometricsStatusForUser,\n userId: userId,\n });\n return response.response as BiometricsStatus;\n }\n\n /**\n * Unlock with biometrics for a specific user.\n * Returns the user key if successful.\n */\n async unlockWithBiometricsForUser(userId: string): Promise<string | null> {\n const response = await this.callCommand(\n {\n command: BiometricsCommands.UnlockWithBiometricsForUser,\n userId: userId,\n },\n USER_INTERACTION_TIMEOUT,\n );\n\n if (response.response) {\n return response.userKeyB64 ?? null;\n }\n\n return null;\n }\n\n /**\n * Send a message to the desktop app (encrypted if secure channel is established).\n */\n private async send(message: Message): Promise<void> {\n if (!this.connected) {\n await this.connect();\n }\n\n message.userId = this.userId ?? undefined;\n message.timestamp = Date.now();\n\n this.postMessage({\n appId: this.appId,\n message: await this.encryptMessage(message),\n });\n }\n\n /**\n * Encrypt a message using the secure channel's shared secret.\n */\n private async encryptMessage(\n message: Message,\n ): Promise<EncryptedMessage | Message> {\n if (this.secureChannel?.sharedSecret == null) {\n await this.secureCommunication();\n }\n\n // biome-ignore lint/style/noNonNullAssertion: guaranteed by secureCommunication() above\n const sharedSecret = this.secureChannel!.sharedSecret!;\n const messageJson = JSON.stringify(message);\n\n // Use AES-256-CBC encryption (matching Bitwarden's EncryptionType.AesCbc256_HmacSha256_B64 = 2)\n const iv = crypto.randomBytes(16);\n const cipher = crypto.createCipheriv(\n \"aes-256-cbc\",\n sharedSecret.subarray(0, 32),\n iv,\n );\n const encrypted = Buffer.concat([\n cipher.update(messageJson, \"utf8\"),\n cipher.final(),\n ]);\n\n // Create HMAC using the second half of the key\n const macKey = sharedSecret.subarray(32, 64);\n const hmac = crypto.createHmac(\"sha256\", macKey);\n hmac.update(iv);\n hmac.update(encrypted);\n const mac = hmac.digest();\n\n return {\n encryptionType: 2, // AesCbc256_HmacSha256_B64\n encryptedString: `2.${iv.toString(\"base64\")}|${encrypted.toString(\"base64\")}|${mac.toString(\"base64\")}`,\n iv: iv.toString(\"base64\"),\n data: encrypted.toString(\"base64\"),\n mac: mac.toString(\"base64\"),\n };\n }\n\n /**\n * Post a message to the IPC socket.\n */\n private postMessage(message: OuterMessage): void {\n try {\n this.ipcSocket.sendMessage(message);\n } catch (e) {\n this.secureChannel = null;\n this.connected = false;\n throw e;\n }\n }\n\n /**\n * Handle incoming messages from the desktop app.\n */\n private async handleMessage(message: ReceivedMessageOuter): Promise<void> {\n if (DEBUG) {\n console.error(\n `[DEBUG] Received message:`,\n JSON.stringify(message, null, 2),\n );\n }\n\n switch (message.command) {\n case \"setupEncryption\":\n if (message.appId !== this.appId) {\n return;\n }\n await this.handleSetupEncryption(message);\n break;\n\n case \"invalidateEncryption\": {\n if (message.appId !== this.appId) {\n return;\n }\n const invalidError = new Error(\n \"Encryption channel invalidated by Desktop app\",\n );\n if (this.secureChannel?.setupReject) {\n this.secureChannel.setupReject(invalidError);\n }\n this.secureChannel = null;\n for (const callback of this.callbacks.values()) {\n clearTimeout(callback.timeout);\n callback.rejecter(invalidError);\n }\n this.callbacks.clear();\n this.connected = false;\n this.ipcSocket.disconnect();\n break;\n }\n\n case \"wrongUserId\": {\n const wrongUserError = new Error(\n \"Account mismatch: CLI and Desktop app are logged into different accounts\",\n );\n if (this.secureChannel?.setupReject) {\n this.secureChannel.setupReject(wrongUserError);\n }\n this.secureChannel = null;\n for (const callback of this.callbacks.values()) {\n clearTimeout(callback.timeout);\n callback.rejecter(wrongUserError);\n }\n this.callbacks.clear();\n this.connected = false;\n this.ipcSocket.disconnect();\n break;\n }\n\n case \"verifyDesktopIPCFingerprint\":\n await this.showFingerprint();\n break;\n\n default:\n // Ignore messages for other apps\n if (message.appId !== this.appId) {\n return;\n }\n\n if (message.message != null) {\n await this.handleEncryptedMessage(message.message);\n }\n }\n }\n\n /**\n * Handle the setupEncryption response from the desktop app.\n */\n private async handleSetupEncryption(\n message: ReceivedMessageOuter,\n ): Promise<void> {\n if (DEBUG) {\n console.error(\n `[DEBUG] handleSetupEncryption called, sharedSecret present: ${message.sharedSecret != null}`,\n );\n }\n\n if (message.sharedSecret == null) {\n if (DEBUG) {\n console.error(`[DEBUG] No sharedSecret in message`);\n }\n return;\n }\n\n if (this.secureChannel == null) {\n if (DEBUG) {\n console.error(`[DEBUG] No secureChannel setup`);\n }\n return;\n }\n\n // Decrypt the shared secret using our private key (RSA-OAEP with SHA-1)\n const encrypted = Buffer.from(message.sharedSecret, \"base64\");\n if (DEBUG) {\n console.error(\n `[DEBUG] Encrypted sharedSecret length: ${encrypted.length}`,\n );\n }\n\n const decrypted = crypto.privateDecrypt(\n {\n key: this.secureChannel.privateKey,\n oaepHash: \"sha1\",\n padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,\n },\n encrypted,\n );\n\n this.secureChannel.sharedSecret = decrypted;\n\n if (DEBUG) {\n console.error(\n `[DEBUG] Decrypted sharedSecret length: ${this.secureChannel.sharedSecret.length}`,\n );\n }\n\n if (this.secureChannel.setupResolve) {\n this.secureChannel.setupResolve();\n }\n }\n\n /**\n * Handle an encrypted message from the desktop app.\n */\n private async handleEncryptedMessage(\n rawMessage: ReceivedMessage | EncryptedMessage,\n ): Promise<void> {\n if (this.secureChannel?.sharedSecret == null) {\n return;\n }\n\n let message: ReceivedMessage;\n\n if (\"encryptionType\" in rawMessage || \"encryptedString\" in rawMessage) {\n // Decrypt the message\n const encMsg = rawMessage as EncryptedMessage;\n const iv = Buffer.from(encMsg.iv, \"base64\");\n const data = Buffer.from(encMsg.data, \"base64\");\n const mac = Buffer.from(encMsg.mac, \"base64\");\n\n const sharedSecret = this.secureChannel.sharedSecret;\n const encKey = sharedSecret.subarray(0, 32);\n const macKey = sharedSecret.subarray(32, 64);\n\n // Verify HMAC\n const hmac = crypto.createHmac(\"sha256\", macKey);\n hmac.update(iv);\n hmac.update(data);\n const expectedMac = hmac.digest();\n\n if (!crypto.timingSafeEqual(mac, expectedMac)) {\n throw new Error(\"Message integrity check failed\");\n }\n\n // Decrypt\n const decipher = crypto.createDecipheriv(\"aes-256-cbc\", encKey, iv);\n const decrypted = Buffer.concat([\n decipher.update(data),\n decipher.final(),\n ]);\n message = JSON.parse(decrypted.toString(\"utf8\"));\n } else {\n message = rawMessage as ReceivedMessage;\n }\n\n this.processDecryptedMessage(message);\n }\n\n /**\n * Process a decrypted message and resolve any pending callbacks.\n */\n private processDecryptedMessage(message: ReceivedMessage): void {\n if (DEBUG) {\n console.error(\n `[DEBUG] Decrypted message:`,\n JSON.stringify(message, null, 2),\n );\n }\n\n if (Math.abs(message.timestamp - Date.now()) > MESSAGE_VALID_TIMEOUT) {\n if (DEBUG) {\n console.error(\n `[DEBUG] Message too old, ignoring. Timestamp: ${message.timestamp}, now: ${Date.now()}`,\n );\n }\n return;\n }\n\n const messageId = message.messageId;\n\n if (this.callbacks.has(messageId)) {\n // biome-ignore lint/style/noNonNullAssertion: guaranteed by .has() check above\n const callback = this.callbacks.get(messageId)!;\n clearTimeout(callback.timeout);\n this.callbacks.delete(messageId);\n callback.resolver(message);\n } else if (DEBUG) {\n console.error(`[DEBUG] No callback found for messageId: ${messageId}`);\n }\n }\n\n /**\n * Set up secure communication with RSA key exchange.\n */\n private async secureCommunication(): Promise<void> {\n // Generate RSA key pair\n const { publicKey, privateKey } = crypto.generateKeyPairSync(\"rsa\", {\n modulusLength: 2048,\n });\n\n // Export public key in SPKI/DER format (base64 encoded)\n const publicKeyDer = publicKey.export({ type: \"spki\", format: \"der\" });\n const publicKeyB64 = publicKeyDer.toString(\"base64\");\n\n const setupMessage = {\n appId: this.appId,\n message: {\n command: \"setupEncryption\",\n publicKey: publicKeyB64,\n userId: this.userId ?? undefined,\n messageId: this.messageId++,\n timestamp: Date.now(),\n },\n };\n\n if (DEBUG) {\n console.error(\n `[DEBUG] Sending setupEncryption:`,\n JSON.stringify(\n {\n ...setupMessage,\n message: {\n ...setupMessage.message,\n publicKey: `${publicKeyB64.slice(0, 50)}...`,\n },\n },\n null,\n 2,\n ),\n );\n }\n\n this.postMessage(setupMessage);\n\n return new Promise((resolve, reject) => {\n this.secureChannel = {\n publicKey,\n privateKey,\n setupResolve: resolve,\n setupReject: reject,\n };\n\n // Timeout for key exchange\n setTimeout(() => {\n if (this.secureChannel && !this.secureChannel.sharedSecret) {\n reject(new Error(\"Secure channel setup timed out\"));\n }\n }, DEFAULT_TIMEOUT);\n });\n }\n\n /**\n * Display the fingerprint for verification.\n */\n private async showFingerprint(): Promise<void> {\n if (this.secureChannel?.publicKey == null) {\n return;\n }\n\n // Generate fingerprint from public key\n const publicKeyDer = this.secureChannel.publicKey.export({\n type: \"spki\",\n format: \"der\",\n });\n const hash = crypto.createHash(\"sha256\").update(publicKeyDer).digest();\n\n // Format as 5 groups of alphanumeric characters (like Bitwarden)\n const fingerprint = hash.toString(\"hex\").slice(0, 25).toUpperCase();\n const formatted = fingerprint.match(/.{1,5}/g)?.join(\"-\") || fingerprint;\n\n // Write to stderr so it doesn't interfere with command output\n const dim = \"\\x1b[2m\";\n const cyan = \"\\x1b[36m\";\n const bold = \"\\x1b[1m\";\n const reset = \"\\x1b[0m\";\n\n console.error(\"\");\n console.error(`${bold}Bitwarden Desktop App Verification${reset}`);\n console.error(\n \"Verify this fingerprint matches the one shown in the Desktop app:\",\n );\n console.error(\"\");\n console.error(`${dim} ${cyan}${formatted}${reset}`);\n console.error(\"\");\n console.error(\"Accept the connection in the Desktop app to continue.\");\n console.error(\"\");\n }\n}\n","/**\n * Log a message to stderr unless BW_QUIET is set.\n */\nexport function log(message: string): void {\n if (process.env.BW_QUIET !== \"true\") {\n console.error(message);\n }\n}\n\n/**\n * Log a message to stderr only when BWBIO_VERBOSE is set (and not quiet).\n */\nexport function logVerbose(message: string): void {\n if (process.env.BWBIO_VERBOSE === \"true\" && process.env.BW_QUIET !== \"true\") {\n console.error(message);\n }\n}\n","import * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\n\n/**\n * AesCbc256_HmacSha256_B64 encryption type (matches Bitwarden's format)\n */\nconst ENCRYPTION_TYPE = 2;\n\n/**\n * Get the CLI data directory path for the current platform.\n */\nfunction getCliDataDir(): string {\n if (process.env.BITWARDENCLI_APPDATA_DIR) {\n return path.resolve(process.env.BITWARDENCLI_APPDATA_DIR);\n }\n\n const platform = os.platform();\n const homeDir = os.homedir();\n\n if (platform === \"darwin\") {\n return path.join(homeDir, \"Library/Application Support/Bitwarden CLI\");\n } else if (platform === \"win32\") {\n return path.join(process.env.APPDATA ?? homeDir, \"Bitwarden CLI\");\n } else {\n // Linux\n const configDir =\n process.env.XDG_CONFIG_HOME ?? path.join(homeDir, \".config\");\n return path.join(configDir, \"Bitwarden CLI\");\n }\n}\n\n/**\n * Generate a new session key (64 random bytes, base64 encoded).\n */\nexport function generateSessionKey(): string {\n const keyBytes = crypto.randomBytes(64);\n return keyBytes.toString(\"base64\");\n}\n\n/**\n * Encrypt data using AES-256-CBC with HMAC-SHA256 (Bitwarden's type 2 format).\n *\n * @param data - The data to encrypt (as Uint8Array)\n * @param sessionKey - The session key (base64 encoded, 64 bytes when decoded)\n * @returns Encrypted data as base64 string\n */\nfunction encryptWithSessionKey(data: Uint8Array, sessionKey: string): string {\n // Decode session key - first 32 bytes for AES, last 32 for HMAC\n const keyBytes = Buffer.from(sessionKey, \"base64\");\n if (keyBytes.length !== 64) {\n throw new Error(\"Session key must be 64 bytes\");\n }\n\n const encKey = keyBytes.subarray(0, 32);\n const macKey = keyBytes.subarray(32, 64);\n\n // Generate random IV\n const iv = crypto.randomBytes(16);\n\n // Encrypt with AES-256-CBC\n const cipher = crypto.createCipheriv(\"aes-256-cbc\", encKey, iv);\n const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);\n\n // Calculate HMAC-SHA256 over IV + ciphertext\n const hmac = crypto.createHmac(\"sha256\", macKey);\n hmac.update(iv);\n hmac.update(encrypted);\n const mac = hmac.digest();\n\n // Assemble: [encType, iv, mac, ciphertext]\n const result = Buffer.alloc(1 + 16 + 32 + encrypted.length);\n result.writeUInt8(ENCRYPTION_TYPE, 0);\n iv.copy(result, 1);\n mac.copy(result, 17);\n encrypted.copy(result, 49);\n\n return result.toString(\"base64\");\n}\n\n/**\n * Read the CLI's data.json file.\n */\nfunction readCliData(): Record<string, unknown> {\n const dataPath = path.join(getCliDataDir(), \"data.json\");\n\n try {\n const content = fs.readFileSync(dataPath, \"utf-8\");\n return content ? JSON.parse(content) : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Get the active user ID from CLI's data storage.\n */\nexport function getActiveUserId(): string | null {\n const data = readCliData();\n const userId = data.global_account_activeAccountId;\n return typeof userId === \"string\" ? userId : null;\n}\n\n/**\n * Write to the CLI's data.json file.\n */\nfunction writeCliData(data: Record<string, unknown>): void {\n const dataDir = getCliDataDir();\n const dataPath = path.join(dataDir, \"data.json\");\n\n // Ensure directory exists\n if (!fs.existsSync(dataDir)) {\n fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n }\n\n // Write with restrictive permissions\n fs.writeFileSync(dataPath, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\n/**\n * Store the user key in CLI's data storage, encrypted with the session key.\n *\n * This mimics what the CLI does internally:\n * - Encrypts the user key with BW_SESSION\n * - Stores it in data.json with key \"__PROTECTED__{userId}_user_auto\"\n *\n * @param userKeyB64 - The user key from biometric unlock (base64 encoded)\n * @param userId - The user ID\n * @param sessionKey - The BW_SESSION key (base64 encoded, 64 bytes)\n */\nexport function storeUserKeyForSession(\n userKeyB64: string,\n userId: string,\n sessionKey: string,\n): void {\n // The user key is already base64 - convert to bytes for encryption\n const userKeyBytes = Buffer.from(userKeyB64, \"base64\");\n\n // Encrypt the user key with the session key\n const encryptedUserKey = encryptWithSessionKey(userKeyBytes, sessionKey);\n\n // Store in CLI's data.json\n const storageKey = `__PROTECTED__${userId}_user_auto`;\n\n const data = readCliData();\n data[storageKey] = encryptedUserKey;\n writeCliData(data);\n}\n\n/**\n * Clean up the stored user key.\n *\n * @param userId - The user ID\n */\nexport function clearStoredUserKey(userId: string): void {\n const storageKey = `__PROTECTED__${userId}_user_auto`;\n\n const data = readCliData();\n delete data[storageKey];\n writeCliData(data);\n}\n","/**\n * Passthrough command detection\n *\n * These commands are passed directly to `bw` without attempting biometric unlock.\n */\n\nconst PASSTHROUGH_COMMANDS = new Set([\n \"login\",\n \"logout\",\n \"lock\",\n \"config\",\n \"update\",\n \"completion\",\n \"status\",\n \"serve\",\n]);\n\nconst PASSTHROUGH_FLAGS = new Set([\"--help\", \"-h\", \"--version\", \"-v\"]);\n\n/**\n * Determines if the given arguments represent a passthrough command.\n *\n * Passthrough commands are executed directly without biometric unlock:\n * - Commands that don't need an unlocked vault (login, logout, status, etc.)\n * - Help and version flags\n *\n * @param args - Command line arguments (without node and script path)\n * @returns true if this is a passthrough command\n */\nexport function isPassthroughCommand(args: string[]): boolean {\n if (args.length === 0) {\n // No args = show help, which is passthrough\n return true;\n }\n\n const firstArg = args[0];\n\n // Check for passthrough flags anywhere in args\n if (args.some((arg) => PASSTHROUGH_FLAGS.has(arg))) {\n return true;\n }\n\n // Check if first arg is a passthrough command\n if (PASSTHROUGH_COMMANDS.has(firstArg)) {\n return true;\n }\n\n return false;\n}\n","#!/usr/bin/env node\n\nimport { main } from \"./main\";\n\n// Get command line arguments (skip node and script path)\nconst args = process.argv.slice(2);\n\nmain(args)\n .then((code) => {\n process.exit(code);\n })\n .catch((err) => {\n console.error(\"Unexpected error:\", err);\n process.exit(1);\n });\n"],"mappings":"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","names":["crypto","platform","resolve","crypto","DEBUG","BiometricsStatus","resolve","crypto","fs","os","path","platform","args","args","resolve"]}
package/package.json CHANGED
@@ -1,8 +1,24 @@
1
1
  {
2
2
  "name": "bitwarden-cli-bio",
3
- "version": "0.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A CLI wrapper for Bitwarden that adds biometric unlock support via the Desktop app",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "bwbio": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts --clean",
12
+ "dev": "tsup src/index.ts --format esm --watch",
13
+ "test": "vitest run --project unit",
14
+ "test:e2e": "vitest run --project e2e",
15
+ "test:watch": "vitest",
16
+ "typecheck": "tsc --noEmit",
17
+ "lint": "biome check .",
18
+ "lint:fix": "biome check --write .",
19
+ "prepublishOnly": "npm run build",
20
+ "release": "semantic-release"
21
+ },
6
22
  "keywords": [
7
23
  "bitwarden",
8
24
  "cli",
@@ -11,5 +27,38 @@
11
27
  "windows-hello"
12
28
  ],
13
29
  "author": "Jean Regisser",
14
- "license": "MIT"
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/jeanregisser/bitwarden-cli-bio.git"
33
+ },
34
+ "homepage": "https://github.com/jeanregisser/bitwarden-cli-bio",
35
+ "bugs": {
36
+ "url": "https://github.com/jeanregisser/bitwarden-cli-bio/issues"
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ },
49
+ "release": {
50
+ "branches": [
51
+ "main"
52
+ ],
53
+ "preset": "conventionalcommits"
54
+ },
55
+ "devDependencies": {
56
+ "@biomejs/biome": "2.3.13",
57
+ "@types/node": "^22.19.7",
58
+ "conventional-changelog-conventionalcommits": "^9.1.0",
59
+ "semantic-release": "^25.0.2",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.9.3",
62
+ "vitest": "^4.0.18"
63
+ }
15
64
  }