bitty-tui 0.0.15 → 0.0.17
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/dist/clients/bw.js +39 -71
- package/dist/hooks/bw.d.ts +6 -0
- package/dist/hooks/bw.js +29 -6
- package/dist/login/LoginView.js +13 -1
- package/package.json +1 -1
- package/readme.md +10 -4
package/dist/clients/bw.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
* - Cipher-specific key if available
|
|
18
18
|
* 4. Decrypt cipher fields using the chosen key
|
|
19
19
|
*/
|
|
20
|
-
import https from "node:https";
|
|
21
20
|
import crypto from "node:crypto";
|
|
22
21
|
import * as argon2 from "argon2";
|
|
23
22
|
export class FetchError extends Error {
|
|
@@ -32,54 +31,14 @@ export class FetchError extends Error {
|
|
|
32
31
|
return JSON.parse(this.data);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
};
|
|
44
|
-
const req = https.request(requestOptions, (res) => {
|
|
45
|
-
let data = "";
|
|
46
|
-
const onData = (chunk) => {
|
|
47
|
-
data += chunk;
|
|
48
|
-
};
|
|
49
|
-
const onEnd = () => {
|
|
50
|
-
cleanup();
|
|
51
|
-
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
52
|
-
reject(new FetchError(res.statusCode, data, `HTTP error: ${res.statusCode} ${res.statusMessage}`));
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
resolve({
|
|
56
|
-
status: res.statusCode,
|
|
57
|
-
json: () => Promise.resolve(JSON.parse(data)),
|
|
58
|
-
text: () => Promise.resolve(data),
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
const onError = (error) => {
|
|
62
|
-
cleanup();
|
|
63
|
-
reject(error);
|
|
64
|
-
};
|
|
65
|
-
const cleanup = () => {
|
|
66
|
-
res.removeListener("data", onData);
|
|
67
|
-
res.removeListener("end", onEnd);
|
|
68
|
-
res.removeListener("error", onError);
|
|
69
|
-
};
|
|
70
|
-
res.on("data", onData);
|
|
71
|
-
res.on("end", onEnd);
|
|
72
|
-
res.on("error", onError);
|
|
73
|
-
});
|
|
74
|
-
req.on("error", (error) => {
|
|
75
|
-
reject(error);
|
|
76
|
-
});
|
|
77
|
-
if (body) {
|
|
78
|
-
req.write(typeof body === "string" ? body : JSON.stringify(body));
|
|
79
|
-
}
|
|
80
|
-
req.end();
|
|
81
|
-
});
|
|
82
|
-
}
|
|
34
|
+
const fetchApi = async (...args) => {
|
|
35
|
+
const response = await fetch(...args);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const data = await response.text();
|
|
38
|
+
throw new FetchError(response.status, data);
|
|
39
|
+
}
|
|
40
|
+
return response;
|
|
41
|
+
};
|
|
83
42
|
export var CipherType;
|
|
84
43
|
(function (CipherType) {
|
|
85
44
|
CipherType[CipherType["Login"] = 1] = "Login";
|
|
@@ -409,13 +368,12 @@ export class Client {
|
|
|
409
368
|
async login(email, password, skipPrelogin = false, opts) {
|
|
410
369
|
let keys = this.keys;
|
|
411
370
|
if (!skipPrelogin) {
|
|
412
|
-
const prelogin = await
|
|
371
|
+
const prelogin = await fetchApi(`${this.identityUrl}/accounts/prelogin`, {
|
|
413
372
|
method: "POST",
|
|
414
373
|
headers: {
|
|
415
374
|
"Content-Type": "application/json",
|
|
416
375
|
},
|
|
417
|
-
|
|
418
|
-
email,
|
|
376
|
+
body: JSON.stringify({ email }),
|
|
419
377
|
}).then((r) => r.json());
|
|
420
378
|
keys = await mcbw.deriveMasterKey(email, password, prelogin);
|
|
421
379
|
this.keys = keys;
|
|
@@ -432,16 +390,22 @@ export class Client {
|
|
|
432
390
|
for (const [key, value] of Object.entries(opts || {})) {
|
|
433
391
|
bodyParams.append(key, value);
|
|
434
392
|
}
|
|
435
|
-
const identityReq = await
|
|
393
|
+
const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
|
|
436
394
|
method: "POST",
|
|
437
395
|
headers: {
|
|
438
|
-
|
|
396
|
+
accept: "*/*",
|
|
397
|
+
"accept-language": "en-US",
|
|
398
|
+
"bitwarden-client-name": "web",
|
|
399
|
+
"bitwarden-client-version": "2025.9.0",
|
|
400
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
439
401
|
},
|
|
440
|
-
|
|
402
|
+
body: bodyParams.toString(),
|
|
403
|
+
}).then((r) => r.json());
|
|
441
404
|
this.token = identityReq.access_token;
|
|
442
405
|
this.refreshToken = identityReq.refresh_token;
|
|
443
406
|
this.tokenExpiration = Date.now() + identityReq.expires_in * 1000;
|
|
444
407
|
const { userKey, privateKey } = mcbw.decodeUserKeys(identityReq.Key, identityReq.PrivateKey || "", keys);
|
|
408
|
+
keys.masterKey = undefined; // Clear master key from memory
|
|
445
409
|
this.keys = {
|
|
446
410
|
...keys,
|
|
447
411
|
userKey,
|
|
@@ -452,19 +416,20 @@ export class Client {
|
|
|
452
416
|
this.orgKeys = {};
|
|
453
417
|
}
|
|
454
418
|
async sendEmailMfaCode(email) {
|
|
455
|
-
|
|
419
|
+
fetchApi(`${this.apiUrl}/two-factor/send-email-login`, {
|
|
456
420
|
method: "POST",
|
|
457
421
|
headers: {
|
|
458
422
|
"Content-Type": "application/json",
|
|
459
423
|
},
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
email: email,
|
|
426
|
+
masterPasswordHash: this.keys.masterPasswordHash,
|
|
427
|
+
ssoEmail2FaSessionToken: "",
|
|
428
|
+
deviceIdentifier: DEVICE_IDENTIFIER,
|
|
429
|
+
authRequestAccessCode: "",
|
|
430
|
+
authRequestId: "",
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
468
433
|
}
|
|
469
434
|
// Check and refresh token if needed
|
|
470
435
|
async checkToken() {
|
|
@@ -472,12 +437,13 @@ export class Client {
|
|
|
472
437
|
if (!this.refreshToken) {
|
|
473
438
|
throw new Error("No refresh token available. Please login first.");
|
|
474
439
|
}
|
|
475
|
-
const identityReq = await
|
|
440
|
+
const identityReq = await fetchApi(`${this.identityUrl}/connect/token`, {
|
|
476
441
|
method: "POST",
|
|
477
442
|
headers: {
|
|
478
443
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
479
444
|
},
|
|
480
|
-
|
|
445
|
+
body: `refresh_token=${this.refreshToken}&grant_type=refresh_token&client_id=web&scope=api%20offline_access`,
|
|
446
|
+
}).then((r) => r.json());
|
|
481
447
|
this.token = identityReq.access_token;
|
|
482
448
|
this.refreshToken = identityReq.refresh_token;
|
|
483
449
|
this.tokenExpiration = Date.now() + identityReq.expires_in * 1000;
|
|
@@ -490,7 +456,7 @@ export class Client {
|
|
|
490
456
|
*/
|
|
491
457
|
async syncRefresh() {
|
|
492
458
|
await this.checkToken();
|
|
493
|
-
this.syncCache = await
|
|
459
|
+
this.syncCache = await fetchApi(`${this.apiUrl}/sync?excludeDomains=true`, {
|
|
494
460
|
method: "GET",
|
|
495
461
|
headers: {
|
|
496
462
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -636,13 +602,14 @@ export class Client {
|
|
|
636
602
|
}
|
|
637
603
|
async createSecret(obj) {
|
|
638
604
|
const key = this.getDecryptionKey(obj);
|
|
639
|
-
const s = await
|
|
605
|
+
const s = await fetchApi(`${this.apiUrl}/ciphers`, {
|
|
640
606
|
method: "POST",
|
|
641
607
|
headers: {
|
|
642
608
|
Authorization: `Bearer ${this.token}`,
|
|
643
609
|
"Content-Type": "application/json",
|
|
644
610
|
},
|
|
645
|
-
|
|
611
|
+
body: JSON.stringify(this.encryptCipher(obj, key)),
|
|
612
|
+
});
|
|
646
613
|
return s.json();
|
|
647
614
|
}
|
|
648
615
|
objectDiff(obj1, obj2) {
|
|
@@ -682,13 +649,14 @@ export class Client {
|
|
|
682
649
|
const key = this.getDecryptionKey(patch);
|
|
683
650
|
const data = this.patchObject(original, this.encryptCipher(obj, key));
|
|
684
651
|
data.data = undefined;
|
|
685
|
-
const s = await
|
|
652
|
+
const s = await fetchApi(`${this.apiUrl}/ciphers/${id}`, {
|
|
686
653
|
method: "PUT",
|
|
687
654
|
headers: {
|
|
688
655
|
Authorization: `Bearer ${this.token}`,
|
|
689
656
|
"Content-Type": "application/json",
|
|
690
657
|
},
|
|
691
|
-
|
|
658
|
+
body: JSON.stringify(data),
|
|
659
|
+
});
|
|
692
660
|
this.decryptedSyncCache = null;
|
|
693
661
|
this.syncCache = null;
|
|
694
662
|
return s.json();
|
package/dist/hooks/bw.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ interface BwConfig {
|
|
|
5
5
|
refreshToken: string;
|
|
6
6
|
}
|
|
7
7
|
export declare const bwClient: Client;
|
|
8
|
+
export interface LoginHints {
|
|
9
|
+
email?: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadLoginHints(): Promise<LoginHints>;
|
|
13
|
+
export declare function saveLoginHints(hints: LoginHints): Promise<void>;
|
|
8
14
|
export declare function loadConfig(): Promise<boolean>;
|
|
9
15
|
export declare function saveConfig(config: BwConfig): Promise<void>;
|
|
10
16
|
export declare function clearConfig(): Promise<void>;
|
package/dist/hooks/bw.js
CHANGED
|
@@ -4,7 +4,34 @@ import path from "path";
|
|
|
4
4
|
import { CipherType, Client } from "../clients/bw.js";
|
|
5
5
|
import { useCallback, useEffect, useState } from "react";
|
|
6
6
|
export const bwClient = new Client();
|
|
7
|
-
const
|
|
7
|
+
const configDir = path.join(os.homedir(), ".config", "bitty");
|
|
8
|
+
const configPath = path.join(configDir, "config.json");
|
|
9
|
+
export async function loadLoginHints() {
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(configPath)) {
|
|
12
|
+
const content = await fs.promises.readFile(configPath, "utf-8");
|
|
13
|
+
const config = JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
|
|
14
|
+
return { email: config.email, baseUrl: config.baseUrl };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
export async function saveLoginHints(hints) {
|
|
21
|
+
let config = {};
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(configPath)) {
|
|
24
|
+
const content = await fs.promises.readFile(configPath, "utf-8");
|
|
25
|
+
config = JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
config.email = hints.email;
|
|
30
|
+
config.baseUrl = hints.baseUrl;
|
|
31
|
+
const encoded = Buffer.from(JSON.stringify(config)).toString("base64");
|
|
32
|
+
await fs.promises.mkdir(configDir, { recursive: true });
|
|
33
|
+
await fs.promises.writeFile(configPath, encoded);
|
|
34
|
+
}
|
|
8
35
|
export async function loadConfig() {
|
|
9
36
|
try {
|
|
10
37
|
if (fs.existsSync(configPath)) {
|
|
@@ -15,8 +42,6 @@ export async function loadConfig() {
|
|
|
15
42
|
}
|
|
16
43
|
if (config.keys && config.refreshToken) {
|
|
17
44
|
const keys = {};
|
|
18
|
-
if (config.keys.masterKey)
|
|
19
|
-
keys.masterKey = Uint8Array.from(config.keys.masterKey);
|
|
20
45
|
if (config.keys.masterPasswordHash)
|
|
21
46
|
keys.masterPasswordHash = config.keys.masterPasswordHash;
|
|
22
47
|
if (config.keys.privateKey)
|
|
@@ -50,8 +75,6 @@ export async function loadConfig() {
|
|
|
50
75
|
}
|
|
51
76
|
export async function saveConfig(config) {
|
|
52
77
|
const keys = {};
|
|
53
|
-
if (config.keys.masterKey)
|
|
54
|
-
keys.masterKey = Array.from(config.keys.masterKey);
|
|
55
78
|
if (config.keys.masterPasswordHash)
|
|
56
79
|
keys.masterPasswordHash = config.keys.masterPasswordHash;
|
|
57
80
|
if (config.keys.privateKey)
|
|
@@ -73,7 +96,7 @@ export async function saveConfig(config) {
|
|
|
73
96
|
...config,
|
|
74
97
|
keys,
|
|
75
98
|
})).toString("base64");
|
|
76
|
-
await fs.promises.mkdir(
|
|
99
|
+
await fs.promises.mkdir(configDir, { recursive: true });
|
|
77
100
|
await fs.promises.writeFile(configPath, encConfig);
|
|
78
101
|
}
|
|
79
102
|
export async function clearConfig() {
|
package/dist/login/LoginView.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
|
|
4
4
|
import { TextInput } from "../components/TextInput.js";
|
|
5
5
|
import { Button } from "../components/Button.js";
|
|
6
6
|
import { primary } from "../theme/style.js";
|
|
7
|
-
import { bwClient, loadConfig, saveConfig } from "../hooks/bw.js";
|
|
7
|
+
import { bwClient, loadConfig, loadLoginHints, saveConfig, saveLoginHints } from "../hooks/bw.js";
|
|
8
8
|
import { useStatusMessage } from "../hooks/status-message.js";
|
|
9
9
|
import { Checkbox } from "../components/Checkbox.js";
|
|
10
10
|
import { FetchError, TwoFactorProvider } from "../clients/bw.js";
|
|
@@ -94,6 +94,12 @@ export function LoginView({ onLogin }) {
|
|
|
94
94
|
refreshToken: bwClient.refreshToken,
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
+
else {
|
|
98
|
+
saveLoginHints({
|
|
99
|
+
email: email?.trim() || undefined,
|
|
100
|
+
baseUrl: url?.trim() || undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
97
103
|
}
|
|
98
104
|
catch (e) {
|
|
99
105
|
showStatusMessage("Login failed, please check your credentials.", "error");
|
|
@@ -105,7 +111,13 @@ export function LoginView({ onLogin }) {
|
|
|
105
111
|
const loggedIn = await loadConfig();
|
|
106
112
|
if (loggedIn) {
|
|
107
113
|
onLogin();
|
|
114
|
+
return;
|
|
108
115
|
}
|
|
116
|
+
const hints = await loadLoginHints();
|
|
117
|
+
if (hints.baseUrl)
|
|
118
|
+
setUrl(hints.baseUrl);
|
|
119
|
+
if (hints.email)
|
|
120
|
+
setEmail(hints.email);
|
|
109
121
|
}
|
|
110
122
|
catch (e) {
|
|
111
123
|
showStatusMessage("Failed to load config file", "error");
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
## Install
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
6
|
+
npm install -g bitty-tui
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Run with:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bitty
|
|
7
13
|
```
|
|
8
14
|
|
|
9
15
|
## Description
|
|
@@ -20,13 +26,13 @@ If you check "Remember me" during login, your vault encryption keys will be stor
|
|
|
20
26
|
## TODO
|
|
21
27
|
|
|
22
28
|
- Collections support
|
|
23
|
-
-
|
|
29
|
+
- Test Fido, Duo MFA support
|
|
24
30
|
- Handle more fields editing
|
|
25
31
|
- Handle creating different cipher types
|
|
26
32
|
|
|
27
33
|
## Acknowledgments
|
|
28
|
-
|
|
29
|
-
- [Bitwarden](https://github.com/bitwarden)
|
|
34
|
+
- [Bitwarden whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper)
|
|
35
|
+
- [Bitwarden github](https://github.com/bitwarden)
|
|
30
36
|
- [BitwardenDecrypt](https://github.com/GurpreetKang/BitwardenDecrypt)
|
|
31
37
|
|
|
32
38
|
This project is not associated with [Bitwarden](https://github.com/bitwarden) or [Bitwarden, Inc.](https://bitwarden.com/)
|