pass-manager-cli 1.0.1 → 1.0.3
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/README.md +75 -6
- package/index.js +1 -1
- package/package.json +8 -5
- package/src/commands/passwords.js +12 -5
- package/src/commands/serve.js +33 -0
- package/src/config.js +15 -0
- package/src/lib/commands.js +2 -2
- package/src/lib/storage.js +102 -28
- package/src/services/passwordService.js +73 -18
- package/src/utils/crypto.js +73 -0
- package/src/utils/errors.js +24 -0
- package/src/utils/prompts.js +26 -0
- package/tests/crypto.test.js +47 -0
- package/docs/API_CONTRACT.md +0 -99
- package/mise.toml +0 -2
- package/src/commands/host.js +0 -9
- package/src/lib/api.js +0 -28
- package/src/services/hostService.js +0 -10
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Password Manager CLI (PMC)
|
|
2
2
|
|
|
3
|
-
A command-line interface (CLI) tool for managing your passwords efficiently.
|
|
3
|
+
A secure command-line interface (CLI) tool for managing your passwords efficiently.
|
|
4
|
+
|
|
5
|
+
[Github Repo Link](https://github.com/Ajinkyap331/password-manager-cli)
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
+
- **End-to-End Encryption**: All passwords are encrypted with **AES-256-GCM** using a Master Password.
|
|
10
|
+
- **System Keychain Integration**: Securely store your Master Password in your OS keychain (macOS, Windows, or Linux) for seamless access.
|
|
11
|
+
- **Interactive Mode**: Securely add passwords via interactive prompts to keep them out of your terminal history.
|
|
9
12
|
- **Clipboard Integration**: Copy passwords directly to your clipboard for quick use.
|
|
10
|
-
- **
|
|
13
|
+
- **Standalone Local Storage**: Your vault is stored locally in `~/.config/pmc/passwords.json`.
|
|
11
14
|
|
|
12
15
|
## Installation
|
|
13
16
|
|
|
@@ -23,6 +26,72 @@ just setup
|
|
|
23
26
|
pmc --help
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
## User Guide
|
|
30
|
+
|
|
31
|
+
### 1. Setup & Adding Passwords
|
|
32
|
+
The safest way to add a password is using the interactive mode. This prevents your password from being saved in your terminal history.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pmc add
|
|
36
|
+
```
|
|
37
|
+
*You will be prompted for the URL, Username, and Password.*
|
|
38
|
+
|
|
39
|
+
### 2. Listing Passwords
|
|
40
|
+
To see all your stored accounts (passwords remain hidden):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pmc list
|
|
44
|
+
```
|
|
45
|
+
*Note the **index** number of the entry you want to use.*
|
|
46
|
+
|
|
47
|
+
### 3. Copying to Clipboard
|
|
48
|
+
To use a password, copy it directly to your clipboard using its index:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pmc copy <index>
|
|
52
|
+
```
|
|
53
|
+
*Example: `pmc copy 0`*
|
|
54
|
+
|
|
55
|
+
### 4. Updating an Entry
|
|
56
|
+
If you change a password or username:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pmc update <index>
|
|
60
|
+
```
|
|
61
|
+
*This will prompt you with the current values, which you can edit or keep.*
|
|
62
|
+
|
|
63
|
+
### 5. Deleting an Entry
|
|
64
|
+
To remove a password permanently:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pmc delete <index>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 6. Serving via HTTP
|
|
71
|
+
Start a local HTTP server to interact with other applications (e.g., UI or browser extensions).
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pmc serve
|
|
75
|
+
```
|
|
76
|
+
*Starts a server at `http://localhost:7474`. You can query passwords via `GET /password?url=<url>`.*
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Security Tip: The Master Password
|
|
81
|
+
The first time you use PMC, you'll create a **Master Password**.
|
|
82
|
+
- **Keychain:** We recommend saying "Yes" when asked to save it to the System Keychain. This allows you to use PMC without typing your Master Password every time.
|
|
83
|
+
- **Lost Password:** If you lose your Master Password, **your data cannot be recovered**. Keep it safe!
|
|
84
|
+
|
|
85
|
+
## Contribution
|
|
86
|
+
|
|
87
|
+
Contributions are welcome! If you'd like to improve PMC, please follow these steps:
|
|
88
|
+
|
|
89
|
+
1. **[Fork the repository](https://github.com/Ajinkyap331/password-manager-cli)** on GitHub.
|
|
90
|
+
2. **Clone your fork** locally: `git clone https://github.com/YOUR_USERNAME/password-manager-cli.git`.
|
|
91
|
+
3. **Create a new branch** for your feature or bugfix: `git checkout -b feature-name`.
|
|
92
|
+
4. **Make your changes** and ensure everything works as expected.
|
|
93
|
+
5. **Commit your changes**: `git commit -m "Add feature name"`.
|
|
94
|
+
6. **Push to your branch**: `git push origin feature-name`.
|
|
95
|
+
7. **Create a Pull Request** on the original repository.
|
|
27
96
|
|
|
28
|
-
|
|
97
|
+
Please ensure your code follows the existing style and structure of the project.
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pass-manager-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
|
-
"scripts": {
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test tests/*.test.js"
|
|
9
|
+
},
|
|
8
10
|
"keywords": [],
|
|
9
11
|
"author": "",
|
|
10
12
|
"license": "ISC",
|
|
@@ -12,10 +14,11 @@
|
|
|
12
14
|
"pmc": "./index.js"
|
|
13
15
|
},
|
|
14
16
|
"dependencies": {
|
|
15
|
-
"
|
|
17
|
+
"@inquirer/prompts": "^8.1.0",
|
|
16
18
|
"clipboardy": "^5.0.2",
|
|
17
19
|
"commander": "^14.0.2",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
+
"express": "^5.2.1",
|
|
21
|
+
"keytar": "^7.9.0",
|
|
22
|
+
"lowdb": "^7.0.1"
|
|
20
23
|
}
|
|
21
24
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
addPassword,
|
|
3
2
|
listPasswords,
|
|
3
|
+
addPassword,
|
|
4
4
|
copyPassword,
|
|
5
5
|
deletePassword,
|
|
6
|
+
updatePassword,
|
|
6
7
|
} from "../services/passwordService.js";
|
|
7
8
|
|
|
8
9
|
export const passwordCommands = (program) => {
|
|
@@ -14,9 +15,9 @@ export const passwordCommands = (program) => {
|
|
|
14
15
|
program
|
|
15
16
|
.command("add")
|
|
16
17
|
.description("add a password")
|
|
17
|
-
.argument("url", "url")
|
|
18
|
-
.argument("username", "username")
|
|
19
|
-
.argument("password", "password")
|
|
18
|
+
.argument("[url]", "url")
|
|
19
|
+
.argument("[username]", "username")
|
|
20
|
+
.argument("[password]", "password")
|
|
20
21
|
.action(addPassword);
|
|
21
22
|
|
|
22
23
|
program
|
|
@@ -30,4 +31,10 @@ export const passwordCommands = (program) => {
|
|
|
30
31
|
.description("Delete Password")
|
|
31
32
|
.argument("index", "index")
|
|
32
33
|
.action(deletePassword);
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command("update")
|
|
37
|
+
.description("Update password entry")
|
|
38
|
+
.argument("index", "index")
|
|
39
|
+
.action(updatePassword);
|
|
40
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { getPasswords } from "../lib/storage.js";
|
|
3
|
+
export const app = express();
|
|
4
|
+
const PORT = 7474;
|
|
5
|
+
|
|
6
|
+
export const serverCommands = (program) => {
|
|
7
|
+
program
|
|
8
|
+
.command("serve")
|
|
9
|
+
.description("Create a server to interact with UI")
|
|
10
|
+
.action(startServer);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
app.get("/password", async (req, res) => {
|
|
14
|
+
const url = req.query.url;
|
|
15
|
+
console.log(url);
|
|
16
|
+
try {
|
|
17
|
+
const allPasswords = await getPasswords();
|
|
18
|
+
const passwords = allPasswords.filter((password) => password.url === url);
|
|
19
|
+
res.send(passwords);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
res.status(500).send({ error: error.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const startServer = async () => {
|
|
26
|
+
try {
|
|
27
|
+
app.listen(PORT, "127.0.0.1", () => {
|
|
28
|
+
console.log(`Server running at http://127.0.0.1:${PORT}/`);
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("Failed to start server:", error.message);
|
|
32
|
+
}
|
|
33
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export const SERVICE_NAME = "PMC-CLI";
|
|
5
|
+
export const ACCOUNT_NAME = "MasterPassword";
|
|
6
|
+
|
|
7
|
+
export const HOME_DIRECTORY = os.homedir();
|
|
8
|
+
export const STORAGE_DIRECTORY = path.join(
|
|
9
|
+
HOME_DIRECTORY,
|
|
10
|
+
".config",
|
|
11
|
+
"pmc",
|
|
12
|
+
"passwords.json"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_DATA = { vault: null };
|
package/src/lib/commands.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { passwordCommands } from "../commands/passwords.js";
|
|
2
|
-
import {
|
|
2
|
+
import { serverCommands } from "../commands/serve.js";
|
|
3
3
|
|
|
4
4
|
export const init = (program) => {
|
|
5
5
|
passwordCommands(program);
|
|
6
|
-
|
|
6
|
+
serverCommands(program);
|
|
7
7
|
program.parse();
|
|
8
8
|
};
|
package/src/lib/storage.js
CHANGED
|
@@ -1,39 +1,113 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { password as passwordPrompt, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { vaultEncryption } from "../utils/crypto.js";
|
|
3
|
+
import keytar from "keytar";
|
|
4
|
+
import {
|
|
5
|
+
getMasterPasswordPrompt,
|
|
6
|
+
saveToKeychainPrompt,
|
|
7
|
+
} from "../utils/prompts.js";
|
|
8
|
+
import { JSONFilePreset } from "lowdb/node";
|
|
9
|
+
import {
|
|
10
|
+
SERVICE_NAME,
|
|
11
|
+
ACCOUNT_NAME,
|
|
12
|
+
STORAGE_DIRECTORY,
|
|
13
|
+
DEFAULT_DATA,
|
|
14
|
+
} from "../config.js";
|
|
15
|
+
import { DecryptionError } from "../utils/errors.js";
|
|
4
16
|
|
|
5
|
-
|
|
17
|
+
const getMasterPassword = async (isNew = false) => {
|
|
18
|
+
if (!isNew) {
|
|
19
|
+
const storedPassword = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
20
|
+
if (storedPassword) {
|
|
21
|
+
return storedPassword;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
6
24
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
await
|
|
13
|
-
|
|
14
|
-
logging: false,
|
|
15
|
-
ttl: false,
|
|
16
|
-
});
|
|
17
|
-
isInitialized = true;
|
|
25
|
+
const masterPassword = await passwordPrompt(getMasterPasswordPrompt(isNew));
|
|
26
|
+
|
|
27
|
+
const shouldSave = await confirm(saveToKeychainPrompt);
|
|
28
|
+
|
|
29
|
+
if (shouldSave) {
|
|
30
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, masterPassword);
|
|
31
|
+
console.log("Master Password saved to System Keychain.");
|
|
18
32
|
}
|
|
33
|
+
|
|
34
|
+
return masterPassword;
|
|
19
35
|
};
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
await
|
|
23
|
-
return
|
|
37
|
+
const initStorage = async () => {
|
|
38
|
+
const db = await JSONFilePreset(STORAGE_DIRECTORY, DEFAULT_DATA);
|
|
39
|
+
return db;
|
|
24
40
|
};
|
|
25
41
|
|
|
26
|
-
export const
|
|
27
|
-
await initStorage();
|
|
28
|
-
|
|
42
|
+
export const addItem = async (newPasswordEntry) => {
|
|
43
|
+
const db = await initStorage();
|
|
44
|
+
const isNew = !db.data.vault;
|
|
45
|
+
|
|
46
|
+
const masterPassword = await getMasterPassword(isNew);
|
|
47
|
+
|
|
48
|
+
let passwords = [];
|
|
49
|
+
if (!isNew) {
|
|
50
|
+
passwords = await vaultEncryption.decrypt(db.data.vault, masterPassword);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
passwords.push(newPasswordEntry);
|
|
54
|
+
|
|
55
|
+
const encryptedVault = await vaultEncryption.encrypt(passwords, masterPassword);
|
|
56
|
+
await db.update((data) => {
|
|
57
|
+
data.vault = encryptedVault;
|
|
58
|
+
});
|
|
29
59
|
};
|
|
30
60
|
|
|
31
|
-
export const
|
|
32
|
-
await initStorage();
|
|
33
|
-
|
|
61
|
+
export const getPasswords = async () => {
|
|
62
|
+
const db = await initStorage();
|
|
63
|
+
if (!db.data.vault) {
|
|
64
|
+
console.log("Vault is empty.");
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const masterPassword = await getMasterPassword();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return await vaultEncryption.decrypt(db.data.vault, masterPassword);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof DecryptionError) {
|
|
74
|
+
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
75
|
+
throw new DecryptionError(
|
|
76
|
+
"Decryption failed. The master password may be incorrect or the vault is corrupted. Stored keychain password has been cleared."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
34
81
|
};
|
|
35
82
|
|
|
36
|
-
export const
|
|
37
|
-
await initStorage();
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
export const clear = async (indexToRemove) => {
|
|
84
|
+
const db = await initStorage();
|
|
85
|
+
if (!db.data.vault) return;
|
|
86
|
+
|
|
87
|
+
const masterPassword = await getMasterPassword();
|
|
88
|
+
let passwords = await vaultEncryption.decrypt(db.data.vault, masterPassword);
|
|
89
|
+
|
|
90
|
+
if (indexToRemove >= 0 && indexToRemove < passwords.length) {
|
|
91
|
+
passwords.splice(indexToRemove, 1);
|
|
92
|
+
const encryptedVault = await vaultEncryption.encrypt(passwords, masterPassword);
|
|
93
|
+
await db.update((data) => {
|
|
94
|
+
data.vault = encryptedVault;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const updateItem = async (indexToUpdate, updatedEntry) => {
|
|
100
|
+
const db = await initStorage();
|
|
101
|
+
if (!db.data.vault) return;
|
|
102
|
+
|
|
103
|
+
const masterPassword = await getMasterPassword();
|
|
104
|
+
let passwords = await vaultEncryption.decrypt(db.data.vault, masterPassword);
|
|
105
|
+
|
|
106
|
+
if (indexToUpdate >= 0 && indexToUpdate < passwords.length) {
|
|
107
|
+
passwords[indexToUpdate] = { ...passwords[indexToUpdate], ...updatedEntry };
|
|
108
|
+
const encryptedVault = await vaultEncryption.encrypt(passwords, masterPassword);
|
|
109
|
+
await db.update((data) => {
|
|
110
|
+
data.vault = encryptedVault;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -1,37 +1,59 @@
|
|
|
1
|
+
import { input, password as passwordPrompt } from "@inquirer/prompts";
|
|
1
2
|
import {
|
|
2
3
|
getPasswords,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
addItem,
|
|
5
|
+
clear,
|
|
6
|
+
updateItem,
|
|
7
|
+
} from "../lib/storage.js";
|
|
6
8
|
import { copyToClipboard } from "../utils/clipboard.js";
|
|
9
|
+
import {
|
|
10
|
+
urlPrompt,
|
|
11
|
+
usernamePrompt,
|
|
12
|
+
passwordPromptConfig,
|
|
13
|
+
getUpdateUsernamePrompt,
|
|
14
|
+
updatePasswordPromptConfig,
|
|
15
|
+
} from "../utils/prompts.js";
|
|
7
16
|
|
|
8
|
-
export const
|
|
17
|
+
export const listPasswords = async () => {
|
|
9
18
|
try {
|
|
10
|
-
await
|
|
11
|
-
console.
|
|
19
|
+
const passwords = await getPasswords();
|
|
20
|
+
console.table(passwords, ["url", "username"]);
|
|
12
21
|
} catch (error) {
|
|
13
|
-
console.error("Failed to
|
|
22
|
+
console.error("Failed to list passwords:", error.message);
|
|
14
23
|
}
|
|
15
24
|
};
|
|
16
25
|
|
|
17
|
-
export const
|
|
26
|
+
export const addPassword = async (url, username, password) => {
|
|
18
27
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
28
|
+
const finalUrl = url || (await input(urlPrompt));
|
|
29
|
+
const finalUsername = username || (await input(usernamePrompt));
|
|
30
|
+
const finalPassword =
|
|
31
|
+
password || (await passwordPrompt(passwordPromptConfig));
|
|
32
|
+
|
|
33
|
+
const newPasswordEntry = {
|
|
34
|
+
url: finalUrl,
|
|
35
|
+
username: finalUsername,
|
|
36
|
+
password: finalPassword,
|
|
37
|
+
};
|
|
38
|
+
await addItem(newPasswordEntry);
|
|
39
|
+
console.log("Password added successfully.");
|
|
21
40
|
} catch (error) {
|
|
22
|
-
console.error("Failed to
|
|
41
|
+
console.error("Failed to add password:", error.message);
|
|
23
42
|
}
|
|
24
43
|
};
|
|
25
44
|
|
|
26
45
|
export const copyPassword = async (index) => {
|
|
27
46
|
try {
|
|
28
47
|
const passwords = await getPasswords();
|
|
48
|
+
const i = parseInt(index, 10);
|
|
29
49
|
|
|
30
|
-
if (
|
|
31
|
-
const password = passwords[
|
|
50
|
+
if (!isNaN(i) && i >= 0 && i < passwords.length) {
|
|
51
|
+
const password = passwords[i].password;
|
|
32
52
|
const success = await copyToClipboard(password);
|
|
33
53
|
if (success) console.log("Password copied to clipboard.");
|
|
34
|
-
} else
|
|
54
|
+
} else {
|
|
55
|
+
console.error("Invalid index provided.");
|
|
56
|
+
}
|
|
35
57
|
} catch (error) {
|
|
36
58
|
console.error("Failed to copy password:", error.message);
|
|
37
59
|
}
|
|
@@ -40,13 +62,46 @@ export const copyPassword = async (index) => {
|
|
|
40
62
|
export const deletePassword = async (index) => {
|
|
41
63
|
try {
|
|
42
64
|
const passwords = await getPasswords();
|
|
65
|
+
const i = parseInt(index, 10);
|
|
43
66
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
await deletePasswordById(passwordId);
|
|
67
|
+
if (!isNaN(i) && i >= 0 && i < passwords.length) {
|
|
68
|
+
await clear(i);
|
|
47
69
|
console.log("Password deleted successfully.");
|
|
48
|
-
} else
|
|
70
|
+
} else {
|
|
71
|
+
console.error("Invalid index provided.");
|
|
72
|
+
}
|
|
49
73
|
} catch (error) {
|
|
50
74
|
console.error("Failed to delete password:", error.message);
|
|
51
75
|
}
|
|
52
76
|
};
|
|
77
|
+
|
|
78
|
+
export const updatePassword = async (index) => {
|
|
79
|
+
try {
|
|
80
|
+
const passwords = await getPasswords();
|
|
81
|
+
const i = parseInt(index, 10);
|
|
82
|
+
|
|
83
|
+
if (!isNaN(i) && i >= 0 && i < passwords.length) {
|
|
84
|
+
const current = passwords[i];
|
|
85
|
+
const updates = {};
|
|
86
|
+
|
|
87
|
+
const newUsername = await input(
|
|
88
|
+
getUpdateUsernamePrompt(current.username)
|
|
89
|
+
);
|
|
90
|
+
if (newUsername) updates.username = newUsername;
|
|
91
|
+
|
|
92
|
+
const newPassword = await passwordPrompt(updatePasswordPromptConfig);
|
|
93
|
+
if (newPassword) updates.password = newPassword;
|
|
94
|
+
|
|
95
|
+
if (Object.keys(updates).length > 0) {
|
|
96
|
+
await updateItem(i, updates);
|
|
97
|
+
console.log("Password entry updated successfully.");
|
|
98
|
+
} else {
|
|
99
|
+
console.log("No changes made.");
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.error("Invalid index provided.");
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Failed to update password:", error.message);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { DecryptionError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
const pbkdf2 = promisify(crypto.pbkdf2);
|
|
6
|
+
|
|
7
|
+
export class VaultEncryption {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.ALGORITHM = "aes-256-gcm";
|
|
10
|
+
this.KEY_LENGTH = 32;
|
|
11
|
+
this.SALT_LENGTH = 16;
|
|
12
|
+
this.IV_LENGTH = 12;
|
|
13
|
+
this.ITERATIONS = 100000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async encrypt(data, password) {
|
|
17
|
+
const salt = crypto.randomBytes(this.SALT_LENGTH);
|
|
18
|
+
const iv = crypto.randomBytes(this.IV_LENGTH);
|
|
19
|
+
|
|
20
|
+
const key = await pbkdf2(
|
|
21
|
+
password,
|
|
22
|
+
salt,
|
|
23
|
+
this.ITERATIONS,
|
|
24
|
+
this.KEY_LENGTH,
|
|
25
|
+
"sha256"
|
|
26
|
+
);
|
|
27
|
+
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv);
|
|
28
|
+
|
|
29
|
+
const encrypted = Buffer.concat([
|
|
30
|
+
cipher.update(JSON.stringify(data), "utf8"),
|
|
31
|
+
cipher.final(),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const authTag = cipher.getAuthTag();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
salt: salt.toString("hex"),
|
|
38
|
+
iv: iv.toString("hex"),
|
|
39
|
+
authTag: authTag.toString("hex"),
|
|
40
|
+
encryptedData: encrypted.toString("hex"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async decrypt(vault, password) {
|
|
45
|
+
const salt = Buffer.from(vault.salt, "hex");
|
|
46
|
+
const iv = Buffer.from(vault.iv, "hex");
|
|
47
|
+
const authTag = Buffer.from(vault.authTag, "hex");
|
|
48
|
+
const encryptedData = Buffer.from(vault.encryptedData, "hex");
|
|
49
|
+
|
|
50
|
+
const key = await pbkdf2(
|
|
51
|
+
password,
|
|
52
|
+
salt,
|
|
53
|
+
this.ITERATIONS,
|
|
54
|
+
this.KEY_LENGTH,
|
|
55
|
+
"sha256"
|
|
56
|
+
);
|
|
57
|
+
const decipher = crypto.createDecipheriv(this.ALGORITHM, key, iv);
|
|
58
|
+
|
|
59
|
+
decipher.setAuthTag(authTag);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const decrypted = Buffer.concat([
|
|
63
|
+
decipher.update(encryptedData),
|
|
64
|
+
decipher.final(),
|
|
65
|
+
]);
|
|
66
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new DecryptionError("Invalid master password or corrupted data.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const vaultEncryption = new VaultEncryption();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = this.constructor.name;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class DecryptionError extends AppError {
|
|
9
|
+
constructor(message = "Decryption failed.") {
|
|
10
|
+
super(message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class KeychainError extends AppError {
|
|
15
|
+
constructor(message = "Keychain operation failed.") {
|
|
16
|
+
super(message);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class StorageError extends AppError {
|
|
21
|
+
constructor(message = "Storage operation failed.") {
|
|
22
|
+
super(message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const urlPrompt = { message: "Enter URL (e.g., google.com):" };
|
|
2
|
+
|
|
3
|
+
export const usernamePrompt = { message: "Enter Username:" };
|
|
4
|
+
|
|
5
|
+
export const passwordPromptConfig = { message: "Enter Password:", mask: "*" };
|
|
6
|
+
|
|
7
|
+
export const getUpdateUsernamePrompt = (currentUsername) => ({
|
|
8
|
+
message: `Enter Username (leave empty to keep: ${currentUsername}):`,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const updatePasswordPromptConfig = {
|
|
12
|
+
message: "Enter New Password (leave empty to keep current):",
|
|
13
|
+
mask: "*",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getMasterPasswordPrompt = (isNew) => ({
|
|
17
|
+
message: isNew
|
|
18
|
+
? "Create a new Master Password for your vault:"
|
|
19
|
+
: "Enter your Master Password to unlock the vault:",
|
|
20
|
+
mask: "*",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const saveToKeychainPrompt = {
|
|
24
|
+
message: "Save Master Password to System Keychain?",
|
|
25
|
+
default: true,
|
|
26
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { vaultEncryption } from "../src/utils/crypto.js";
|
|
2
|
+
import { DecryptionError } from "../src/utils/errors.js";
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
test("VaultEncryption - encrypt and decrypt", async (t) => {
|
|
7
|
+
const data = { secret: "my-password-123" };
|
|
8
|
+
const password = "master-password";
|
|
9
|
+
|
|
10
|
+
await t.test("should successfully encrypt and then decrypt data", async () => {
|
|
11
|
+
const encrypted = await vaultEncryption.encrypt(data, password);
|
|
12
|
+
|
|
13
|
+
assert.ok(encrypted.salt, "should have salt");
|
|
14
|
+
assert.ok(encrypted.iv, "should have iv");
|
|
15
|
+
assert.ok(encrypted.authTag, "should have authTag");
|
|
16
|
+
assert.ok(encrypted.encryptedData, "should have encryptedData");
|
|
17
|
+
|
|
18
|
+
const decrypted = await vaultEncryption.decrypt(encrypted, password);
|
|
19
|
+
assert.deepStrictEqual(decrypted, data, "decrypted data should match original data");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await t.test("should throw DecryptionError with incorrect password", async () => {
|
|
23
|
+
const encrypted = await vaultEncryption.encrypt(data, password);
|
|
24
|
+
|
|
25
|
+
await assert.rejects(
|
|
26
|
+
async () => {
|
|
27
|
+
await vaultEncryption.decrypt(encrypted, "wrong-password");
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "DecryptionError",
|
|
31
|
+
message: "Invalid master password or corrupted data.",
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await t.test("should throw error with corrupted data", async () => {
|
|
37
|
+
const encrypted = await vaultEncryption.encrypt(data, password);
|
|
38
|
+
const corruptedVault = { ...encrypted, encryptedData: "corrupted" };
|
|
39
|
+
|
|
40
|
+
await assert.rejects(
|
|
41
|
+
async () => {
|
|
42
|
+
await vaultEncryption.decrypt(corruptedVault, password);
|
|
43
|
+
},
|
|
44
|
+
DecryptionError
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
package/docs/API_CONTRACT.md
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# Backend API Contract for Password Manager CLI
|
|
2
|
-
|
|
3
|
-
This document outlines the API endpoints, methods, and data structures expected from the backend service that the Password Manager CLI (PMC) interacts with.
|
|
4
|
-
|
|
5
|
-
**Base URL Structure:** `http://<hostIP>:5421/`
|
|
6
|
-
|
|
7
|
-
The `<hostIP>` is configured using the `pmc host link <IP_ADDRESS>` command.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. Passwords API
|
|
12
|
-
|
|
13
|
-
This API is responsible for managing password entries.
|
|
14
|
-
|
|
15
|
-
### a. `GET /passwords` - Retrieve All Passwords
|
|
16
|
-
|
|
17
|
-
Retrieves a list of all password entries stored in the backend.
|
|
18
|
-
|
|
19
|
-
* **Method:** `GET`
|
|
20
|
-
* **Path:** `/passwords`
|
|
21
|
-
* **Authentication:** (Assumed to be handled by backend, not explicitly defined in current CLI)
|
|
22
|
-
* **Request Headers:** (None explicitly sent by CLI for now, could include Authorization in future)
|
|
23
|
-
* **Request Body:** (None)
|
|
24
|
-
* **Response:** `200 OK` - A JSON array of password objects.
|
|
25
|
-
|
|
26
|
-
```json
|
|
27
|
-
[
|
|
28
|
-
{
|
|
29
|
-
"id": "uuid-v4-string-1",
|
|
30
|
-
"url": "example.com",
|
|
31
|
-
"username": "user@example.com",
|
|
32
|
-
"password": "secure_password_1"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"id": "uuid-v4-string-2",
|
|
36
|
-
"url": "another.com",
|
|
37
|
-
"username": "another_user",
|
|
38
|
-
"password": "secure_password_2"
|
|
39
|
-
},
|
|
40
|
-
// ... more password objects
|
|
41
|
-
]
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### b. `POST /passwords` - Add a New Password
|
|
45
|
-
|
|
46
|
-
Adds a new password entry to the backend.
|
|
47
|
-
|
|
48
|
-
* **Method:** `POST`
|
|
49
|
-
* **Path:** `/passwords`
|
|
50
|
-
* **Authentication:** (Assumed)
|
|
51
|
-
* **Request Headers:**
|
|
52
|
-
* `Content-Type: application/json`
|
|
53
|
-
* **Request Body:** A JSON object containing the new password's details.
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"url": "new-site.com",
|
|
58
|
-
"username": "new_user",
|
|
59
|
-
"password": "new_secure_password"
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
* **Response:** `200 OK` or `201 Created` - The newly created password object, including its generated `id`.
|
|
63
|
-
|
|
64
|
-
```json
|
|
65
|
-
{
|
|
66
|
-
"id": "uuid-v4-string-3",
|
|
67
|
-
"url": "new-site.com",
|
|
68
|
-
"username": "new_user",
|
|
69
|
-
"password": "new_secure_password"
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### c. `DELETE /passwords/:id` - Delete a Password by ID
|
|
74
|
-
|
|
75
|
-
Deletes a specific password entry from the backend using its unique ID.
|
|
76
|
-
|
|
77
|
-
* **Method:** `DELETE`
|
|
78
|
-
* **Path:** `/passwords/{id}` (where `{id}` is the unique identifier of the password)
|
|
79
|
-
* **Authentication:** (Assumed)
|
|
80
|
-
* **Request Headers:** (None)
|
|
81
|
-
* **Request Body:** (None)
|
|
82
|
-
* **Response:** `200 OK` (or `204 No Content`) - Indicates successful deletion. No content is typically returned for 204.
|
|
83
|
-
|
|
84
|
-
```json
|
|
85
|
-
// Example for 200 OK
|
|
86
|
-
{
|
|
87
|
-
"message": "Password deleted successfully"
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
or
|
|
91
|
-
(No Content for 204)
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## 2. Host Configuration
|
|
96
|
-
|
|
97
|
-
The `pmc host link <IP_ADDRESS>` command primarily configures the local CLI's storage to remember the backend server's IP address. There is no explicit "Host API" endpoint for this action in the backend. The `hostIP` is used by the CLI to construct the base URL for the Passwords API.
|
|
98
|
-
|
|
99
|
-
---
|
package/mise.toml
DELETED
package/src/commands/host.js
DELETED
package/src/lib/api.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
import { getHostIP } from "./storage.js";
|
|
3
|
-
|
|
4
|
-
const getBaseUrl = async () => {
|
|
5
|
-
const hostIP = await getHostIP();
|
|
6
|
-
if (!hostIP) {
|
|
7
|
-
throw new Error("Host IP not set. Please link a password manager first.");
|
|
8
|
-
}
|
|
9
|
-
return `http://${hostIP}:5421/passwords`;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const getPasswords = async () => {
|
|
13
|
-
const url = await getBaseUrl();
|
|
14
|
-
const response = await axios.get(url);
|
|
15
|
-
return response.data;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const createPassword = async (url, username, password) => {
|
|
19
|
-
const baseUrl = await getBaseUrl();
|
|
20
|
-
const response = await axios.post(baseUrl, { url, username, password });
|
|
21
|
-
return response.data;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const deletePasswordById = async (id) => {
|
|
25
|
-
const baseUrl = await getBaseUrl();
|
|
26
|
-
const response = await axios.delete(`${baseUrl}/${id}`);
|
|
27
|
-
return response.data;
|
|
28
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { setItem } from "../lib/storage.js";
|
|
2
|
-
|
|
3
|
-
export const linkManager = async (ip) => {
|
|
4
|
-
try {
|
|
5
|
-
await setItem("host", ip);
|
|
6
|
-
console.log(`Host IP ${ip} linked successfully.`);
|
|
7
|
-
} catch (error) {
|
|
8
|
-
console.error("Failed to link host IP:", error.message);
|
|
9
|
-
}
|
|
10
|
-
};
|