pass-manager-cli 1.0.0 → 1.0.2

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 CHANGED
@@ -1,13 +1,14 @@
1
1
  # Password Manager CLI (PMC)
2
2
 
3
- A command-line interface (CLI) tool for managing your passwords efficiently. This tool allows you to add, list, copy, and delete passwords, as well as link to your password manager's host.
3
+ A secure command-line interface (CLI) tool for managing your passwords efficiently.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Secure Password Management**: Add, list, copy, and delete passwords.
8
- - **Host Configuration**: Easily link your CLI to a password manager backend by specifying its host IP.
7
+ - **End-to-End Encryption**: All passwords are encrypted with **AES-256-GCM** using a Master Password.
8
+ - **System Keychain Integration**: Securely store your Master Password in your OS keychain (macOS, Windows, or Linux) for seamless access.
9
+ - **Interactive Mode**: Securely add passwords via interactive prompts to keep them out of your terminal history.
9
10
  - **Clipboard Integration**: Copy passwords directly to your clipboard for quick use.
10
- - **Modular Structure**: Built with a clear separation of concerns for easy maintenance and extensibility.
11
+ - **Standalone Local Storage**: Your vault is stored locally in `~/.config/pmc/passwords.json`.
11
12
 
12
13
  ## Installation
13
14
 
@@ -23,6 +24,64 @@ just setup
23
24
  pmc --help
24
25
  ```
25
26
 
26
- ### API Contract
27
+ ## User Guide
27
28
 
28
- For details on the backend API endpoints and data structures that this CLI interacts with, please refer to the [API Contract Documentation](./docs/API_CONTRACT.md).
29
+ ### 1. Setup & Adding Passwords
30
+ The safest way to add a password is using the interactive mode. This prevents your password from being saved in your terminal history.
31
+
32
+ ```bash
33
+ pmc add
34
+ ```
35
+ *You will be prompted for the URL, Username, and Password.*
36
+
37
+ ### 2. Listing Passwords
38
+ To see all your stored accounts (passwords remain hidden):
39
+
40
+ ```bash
41
+ pmc list
42
+ ```
43
+ *Note the **index** number of the entry you want to use.*
44
+
45
+ ### 3. Copying to Clipboard
46
+ To use a password, copy it directly to your clipboard using its index:
47
+
48
+ ```bash
49
+ pmc copy <index>
50
+ ```
51
+ *Example: `pmc copy 0`*
52
+
53
+ ### 4. Updating an Entry
54
+ If you change a password or username:
55
+
56
+ ```bash
57
+ pmc update <index>
58
+ ```
59
+ *This will prompt you with the current values, which you can edit or keep.*
60
+
61
+ ### 5. Deleting an Entry
62
+ To remove a password permanently:
63
+
64
+ ```bash
65
+ pmc delete <index>
66
+ ```
67
+
68
+ ---
69
+
70
+ ### Security Tip: The Master Password
71
+ The first time you use PMC, you'll create a **Master Password**.
72
+ - **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.
73
+ - **Lost Password:** If you lose your Master Password, **your data cannot be recovered**. Keep it safe!
74
+
75
+ ## Contribution
76
+
77
+ Contributions are welcome! If you'd like to improve PMC, please follow these steps:
78
+
79
+ 1. **Fork the repository** on GitHub.
80
+ 2. **Clone your fork** locally: `git clone https://github.com/YOUR_USERNAME/password-manager-cli.git`.
81
+ 3. **Create a new branch** for your feature or bugfix: `git checkout -b feature-name`.
82
+ 4. **Make your changes** and ensure everything works as expected.
83
+ 5. **Commit your changes**: `git commit -m "Add feature name"`.
84
+ 6. **Push to your branch**: `git push origin feature-name`.
85
+ 7. **Create a Pull Request** on the original repository.
86
+
87
+ Please ensure your code follows the existing style and structure of the project.
package/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { init } from "./src/lib/commands.js";
5
+ import { getPasswordByUrl } from "./src/lib/api.js";
5
6
 
6
7
  const program = new Command();
7
8
 
@@ -10,4 +11,4 @@ program
10
11
  .description("CLI for Password Manager")
11
12
  .version("1.0.0");
12
13
 
13
- init(program);
14
+ init(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pass-manager-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -12,10 +12,11 @@
12
12
  "pmc": "./index.js"
13
13
  },
14
14
  "dependencies": {
15
- "axios": "^1.13.2",
15
+ "@inquirer/prompts": "^8.1.0",
16
16
  "clipboardy": "^5.0.2",
17
17
  "commander": "^14.0.2",
18
- "inquirer": "^13.1.0",
19
- "node-persist": "^4.0.4"
18
+ "express": "^5.2.1",
19
+ "keytar": "^7.9.0",
20
+ "lowdb": "^7.0.1"
20
21
  }
21
22
  }
@@ -3,6 +3,7 @@ import {
3
3
  listPasswords,
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((index) => updatePassword(index));
40
+ };
@@ -0,0 +1,24 @@
1
+ import express from "express";
2
+ import { getPasswordByUrl } from "../lib/api.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
+ const passwords = await getPasswordByUrl(url);
17
+ res.send(passwords);
18
+ });
19
+
20
+ const startServer = () => {
21
+ app.listen(PORT, () => {
22
+ console.log(`Server running at http://localhost:${PORT}/`);
23
+ });
24
+ };
package/src/lib/api.js CHANGED
@@ -1,28 +1,25 @@
1
- import axios from "axios";
2
- import { getHostIP } from "./storage.js";
1
+ import { addItem, clear, getPasswords, updateItem } from "./storage.js";
3
2
 
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
- };
3
+ export const getAllPasswords = async () => await getPasswords();
11
4
 
12
- export const getPasswords = async () => {
13
- const url = await getBaseUrl();
14
- const response = await axios.get(url);
15
- return response.data;
16
- };
5
+ export const getPasswordByUrl = async (url) =>
6
+ await getPasswords().then((passwords) =>
7
+ passwords.filter((password) => password.url === url)
8
+ );
17
9
 
18
10
  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;
11
+ const passwordMapping = {
12
+ url: url,
13
+ username: username,
14
+ password: password,
15
+ };
16
+ await addItem(passwordMapping);
22
17
  };
23
18
 
24
- export const deletePasswordById = async (id) => {
25
- const baseUrl = await getBaseUrl();
26
- const response = await axios.delete(`${baseUrl}/${id}`);
27
- return response.data;
28
- };
19
+ export const deletePasswordByIndex = async (index) => {
20
+ await clear(index);
21
+ };
22
+
23
+ export const updatePasswordByIndex = async (index, updates) => {
24
+ await updateItem(index, updates);
25
+ };
@@ -1,8 +1,8 @@
1
1
  import { passwordCommands } from "../commands/passwords.js";
2
- import { hostCommands } from "../commands/host.js";
2
+ import { serverCommands } from "../commands/serve.js";
3
3
 
4
4
  export const init = (program) => {
5
5
  passwordCommands(program);
6
- hostCommands(program);
6
+ serverCommands(program);
7
7
  program.parse();
8
8
  };
@@ -1,39 +1,123 @@
1
- import nodePersist from "node-persist";
2
1
  import os from "os";
3
2
  import path from "path";
3
+ import { JSONFilePreset } from "lowdb/node";
4
+ import { password as passwordPrompt, confirm } from "@inquirer/prompts";
5
+ import { encrypt, decrypt } from "../utils/crypto.js";
6
+ import keytar from "keytar";
4
7
 
5
- let isInitialized = false;
8
+ const SERVICE_NAME = "PMC-CLI";
9
+ const ACCOUNT_NAME = "MasterPassword";
6
10
 
7
- const initStorage = async () => {
8
- if (!isInitialized) {
9
- const homeDirectory = os.homedir();
10
- const storageDirectory = path.join(homeDirectory, ".config", "pmc");
11
-
12
- await nodePersist.init({
13
- dir: storageDirectory,
14
- logging: false,
15
- ttl: false,
16
- });
17
- isInitialized = true;
11
+ const getMasterPassword = async (isNew = false) => {
12
+ if (!isNew) {
13
+ const storedPassword = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
14
+ if (storedPassword) {
15
+ return storedPassword;
16
+ }
17
+ }
18
+
19
+ const masterPassword = await passwordPrompt({
20
+ message: isNew
21
+ ? "Create a new Master Password for your vault:"
22
+ : "Enter your Master Password to unlock the vault:",
23
+ mask: "*",
24
+ });
25
+
26
+ const shouldSave = await confirm({
27
+ message: "Save Master Password to System Keychain?",
28
+ default: true,
29
+ });
30
+
31
+ if (shouldSave) {
32
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, masterPassword);
33
+ console.log("Master Password saved to System Keychain.");
18
34
  }
35
+
36
+ return masterPassword;
19
37
  };
20
38
 
21
- export const setItem = async (key, value) => {
22
- await initStorage();
23
- return nodePersist.setItem(key, value);
39
+ const initStorage = async () => {
40
+ const homeDirectory = os.homedir();
41
+ const storageDirectory = path.join(
42
+ homeDirectory,
43
+ ".config",
44
+ "pmc",
45
+ "passwords.json"
46
+ );
47
+
48
+ const defaultData = { vault: null };
49
+ const db = await JSONFilePreset(storageDirectory, defaultData);
50
+
51
+ return db;
52
+ };
53
+
54
+ export const addItem = async (newPasswordEntry) => {
55
+ const db = await initStorage();
56
+ const isNew = !db.data.vault;
57
+
58
+ const masterPassword = await getMasterPassword(isNew);
59
+
60
+ let passwords = [];
61
+ if (!isNew) {
62
+ passwords = await decrypt(db.data.vault, masterPassword);
63
+ }
64
+
65
+ passwords.push(newPasswordEntry);
66
+
67
+ const encryptedVault = await encrypt(passwords, masterPassword);
68
+ await db.update((data) => {
69
+ data.vault = encryptedVault;
70
+ });
24
71
  };
25
72
 
26
- export const getItem = async (key) => {
27
- await initStorage();
28
- return nodePersist.getItem(key);
73
+ export const getPasswords = async () => {
74
+ const db = await initStorage();
75
+ if (!db.data.vault) {
76
+ console.log("Vault is empty.");
77
+ return [];
78
+ }
79
+
80
+ const masterPassword = await getMasterPassword();
81
+
82
+ try {
83
+ return await decrypt(db.data.vault, masterPassword);
84
+ } catch (error) {
85
+ console.error("Decryption failed. " + error.message);
86
+ // If decryption fails, maybe the keychain password is wrong/stale?
87
+ // Optionally delete it so the user can re-enter.
88
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
89
+ process.exit(1);
90
+ }
29
91
  };
30
92
 
31
- export const clear = async () => {
32
- await initStorage();
33
- return nodePersist.clear();
93
+ export const clear = async (indexToRemove) => {
94
+ const db = await initStorage();
95
+ if (!db.data.vault) return;
96
+
97
+ const masterPassword = await getMasterPassword();
98
+ let passwords = await decrypt(db.data.vault, masterPassword);
99
+
100
+ if (indexToRemove >= 0 && indexToRemove < passwords.length) {
101
+ passwords.splice(indexToRemove, 1);
102
+ const encryptedVault = await encrypt(passwords, masterPassword);
103
+ await db.update((data) => {
104
+ data.vault = encryptedVault;
105
+ });
106
+ }
34
107
  };
35
108
 
36
- export const getHostIP = async () => {
37
- await initStorage();
38
- return nodePersist.getItem("host");
39
- };
109
+ export const updateItem = async (indexToUpdate, updatedEntry) => {
110
+ const db = await initStorage();
111
+ if (!db.data.vault) return;
112
+
113
+ const masterPassword = await getMasterPassword();
114
+ let passwords = await decrypt(db.data.vault, masterPassword);
115
+
116
+ if (indexToUpdate >= 0 && indexToUpdate < passwords.length) {
117
+ passwords[indexToUpdate] = { ...passwords[indexToUpdate], ...updatedEntry };
118
+ const encryptedVault = await encrypt(passwords, masterPassword);
119
+ await db.update((data) => {
120
+ data.vault = encryptedVault;
121
+ });
122
+ }
123
+ };
@@ -1,13 +1,24 @@
1
1
  import {
2
- getPasswords,
2
+ getAllPasswords,
3
3
  createPassword,
4
- deletePasswordById,
4
+ deletePasswordByIndex,
5
+ updatePasswordByIndex,
5
6
  } from "../lib/api.js";
6
7
  import { copyToClipboard } from "../utils/clipboard.js";
8
+ import { input, password as passwordPrompt } from "@inquirer/prompts";
7
9
 
8
10
  export const addPassword = async (url, username, password) => {
9
11
  try {
10
- await createPassword(url, username, password);
12
+ const finalUrl =
13
+ url ||
14
+ (await input({ message: "Enter URL (e.g., google.com):" }));
15
+ const finalUsername =
16
+ username || (await input({ message: "Enter Username:" }));
17
+ const finalPassword =
18
+ password ||
19
+ (await passwordPrompt({ message: "Enter Password:", mask: "*" }));
20
+
21
+ await createPassword(finalUrl, finalUsername, finalPassword);
11
22
  console.log("Password added successfully.");
12
23
  } catch (error) {
13
24
  console.error("Failed to add password:", error.message);
@@ -16,7 +27,7 @@ export const addPassword = async (url, username, password) => {
16
27
 
17
28
  export const listPasswords = async () => {
18
29
  try {
19
- const passwords = await getPasswords();
30
+ const passwords = await getAllPasswords();
20
31
  console.table(passwords, ["url", "username"]);
21
32
  } catch (error) {
22
33
  console.error("Failed to list passwords:", error.message);
@@ -25,7 +36,7 @@ export const listPasswords = async () => {
25
36
 
26
37
  export const copyPassword = async (index) => {
27
38
  try {
28
- const passwords = await getPasswords();
39
+ const passwords = await getAllPasswords();
29
40
 
30
41
  if (index >= 0 && index < passwords.length) {
31
42
  const password = passwords[index].password;
@@ -39,14 +50,45 @@ export const copyPassword = async (index) => {
39
50
 
40
51
  export const deletePassword = async (index) => {
41
52
  try {
42
- const passwords = await getPasswords();
53
+ const passwords = await getAllPasswords();
43
54
 
44
55
  if (index >= 0 && index < passwords.length) {
45
- const passwordId = passwords[index].id;
46
- await deletePasswordById(passwordId);
56
+ await deletePasswordByIndex(index);
57
+
47
58
  console.log("Password deleted successfully.");
48
59
  } else console.error("Invalid index provided.");
49
60
  } catch (error) {
50
61
  console.error("Failed to delete password:", error.message);
51
62
  }
52
63
  };
64
+
65
+ export const updatePassword = async (index) => {
66
+ try {
67
+ const passwords = await getAllPasswords();
68
+
69
+ if (index >= 0 && index < passwords.length) {
70
+ const current = passwords[index];
71
+ const updates = {};
72
+
73
+ const newUsername = await input({
74
+ message: `Enter Username (leave empty to keep: ${current.username}):`,
75
+ });
76
+ if (newUsername) updates.username = newUsername;
77
+
78
+ const newPassword = await passwordPrompt({
79
+ message: "Enter New Password (leave empty to keep current):",
80
+ mask: "*",
81
+ });
82
+ if (newPassword) updates.password = newPassword;
83
+
84
+ if (Object.keys(updates).length > 0) {
85
+ await updatePasswordByIndex(index, updates);
86
+ console.log("Password entry updated successfully.");
87
+ } else {
88
+ console.log("No changes made.");
89
+ }
90
+ } else console.error("Invalid index provided.");
91
+ } catch (error) {
92
+ console.error("Failed to update password:", error.message);
93
+ }
94
+ };
@@ -0,0 +1,54 @@
1
+ import crypto from "crypto";
2
+ import { promisify } from "util";
3
+
4
+ const pbkdf2 = promisify(crypto.pbkdf2);
5
+
6
+ const ALGORITHM = "aes-256-gcm";
7
+ const KEY_LENGTH = 32;
8
+ const SALT_LENGTH = 16;
9
+ const IV_LENGTH = 12;
10
+ const ITERATIONS = 100000;
11
+
12
+ export const encrypt = async (data, password) => {
13
+ const salt = crypto.randomBytes(SALT_LENGTH);
14
+ const iv = crypto.randomBytes(IV_LENGTH);
15
+
16
+ const key = await pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, "sha256");
17
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
18
+
19
+ const encrypted = Buffer.concat([
20
+ cipher.update(JSON.stringify(data), "utf8"),
21
+ cipher.final(),
22
+ ]);
23
+
24
+ const authTag = cipher.getAuthTag();
25
+
26
+ return {
27
+ salt: salt.toString("hex"),
28
+ iv: iv.toString("hex"),
29
+ authTag: authTag.toString("hex"),
30
+ encryptedData: encrypted.toString("hex"),
31
+ };
32
+ };
33
+
34
+ export const decrypt = async (vault, password) => {
35
+ const salt = Buffer.from(vault.salt, "hex");
36
+ const iv = Buffer.from(vault.iv, "hex");
37
+ const authTag = Buffer.from(vault.authTag, "hex");
38
+ const encryptedData = Buffer.from(vault.encryptedData, "hex");
39
+
40
+ const key = await pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, "sha256");
41
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
42
+
43
+ decipher.setAuthTag(authTag);
44
+
45
+ try {
46
+ const decrypted = Buffer.concat([
47
+ decipher.update(encryptedData),
48
+ decipher.final(),
49
+ ]);
50
+ return JSON.parse(decrypted.toString("utf8"));
51
+ } catch (error) {
52
+ throw new Error("Invalid master password or corrupted data.");
53
+ }
54
+ };
@@ -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
- ---
@@ -1,9 +0,0 @@
1
- import { linkManager } from "../services/hostService.js";
2
-
3
- export const hostCommands = (program) => {
4
- program
5
- .command("link")
6
- .description("link a password manager")
7
- .argument("ip", "ip")
8
- .action(linkManager);
9
- };
@@ -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
- };