npm-cred
Version:
A secure CLI tool for managing multiple credential vaults
272 lines (234 loc) • 8.18 kB
JavaScript
import {
displayErrorMessage,
displaySuccessMessage,
displayWarningMessage,
} from "./messages.js";
import { encrypt, decrypt } from "./crypto.js";
class Vault {
constructor(name, password, isUnlocked = false, initialEncryptedData = null) {
if (!name) {
displayErrorMessage("Name is required");
return;
}
if (name.length < 5) {
displayErrorMessage("Vault name must be at least 5 characters long");
return;
}
this.name = name;
this.isUnlocked = isUnlocked;
this.credentials = [];
if (password) {
if (password.length < 5) {
displayErrorMessage(
"Vault password must be at least 5 characters long"
);
return;
}
this.encryptedData = initialEncryptedData || encrypt([], password); // Use provided encrypted data or create new
this.password = password; // Store password for locking/unlocking
} else {
this.encryptedData = null; // No password means no encryption
}
}
unlock(name, password) {
if (!name) {
displayErrorMessage("Vault name is required");
return false;
}
if (name.length < 5) {
displayErrorMessage("Vault name must be at least 5 characters long");
return false;
}
if (name === this.name) {
if (this.isUnlocked) {
displayWarningMessage(`Vault ${this.name} is already unlocked`);
return true; // Return true if already unlocked
}
if (!password) {
displayErrorMessage("Password is required to unlock the vault");
return false;
}
if (password.length < 5) {
displayErrorMessage(
"Vault password must be at least 5 characters long"
);
return false;
}
try {
// Decrypt credentials from encrypted data
const decryptedCredentials = decrypt(this.encryptedData, password);
// Update vault state
this.credentials = decryptedCredentials;
this.isUnlocked = true;
this.password = password;
displaySuccessMessage(`Vault ${this.name} unlocked successfully`);
return true;
} catch (error) {
displayErrorMessage("Failed to decrypt vault data");
return false;
}
} else {
displayWarningMessage(`Incorrect name or password for vault`);
return false;
}
}
lock(name) {
if (!name) {
displayErrorMessage("Vault name is required for locking");
return false;
}
if (name === this.name) {
if (!this.isUnlocked) {
displayWarningMessage(`Vault ${this.name} is already locked`);
return false;
}
// Simply set the vault as locked
this.isUnlocked = false; // Set the vault as locked
displaySuccessMessage(`Vault ${this.name} locked successfully`);
return true;
} else {
displayErrorMessage(`Incorrect name for locking the vault`);
return false;
}
}
// Add a credential to the vault
addCredential(credential) {
if (!this.isUnlocked) {
displayErrorMessage(`Vault ${this.name} needs to be unlocked first`);
return false;
}
if (!credential || !credential.key || !credential.value) {
displayErrorMessage("Both key and value are required for the credential");
return false;
}
// Check if credential with the same key already exists
const existingCredential = this.credentials.find(
(cred) => cred.key === credential.key
);
if (existingCredential) {
displayErrorMessage(
`A credential with the key "${credential.key}" already exists. Each credential name must be unique.`
);
return false;
}
// Add the new credential
this.credentials.push(credential);
// Update the encrypted data with the current password
if (this.password) {
this.encryptedData = encrypt(this.credentials, this.password);
}
displaySuccessMessage(
`Credential "${credential.key}" added successfully to vault ${this.name}`
);
return true;
}
// Get all credentials from the vault
getCredentials() {
if (!this.isUnlocked) {
displayErrorMessage(`Vault ${this.name} needs to be unlocked first`);
return null;
}
return this.credentials;
}
// Delete a credential from the vault by key
deleteCredential(key) {
if (!this.isUnlocked) {
displayErrorMessage(`Vault ${this.name} needs to be unlocked first`);
return false;
}
if (!key) {
displayErrorMessage("Credential key is required for deletion");
return false;
}
const initialLength = this.credentials.length;
this.credentials = this.credentials.filter((cred) => cred.key !== key);
if (this.credentials.length === initialLength) {
displayWarningMessage(
`No credential with key "${key}" found in vault ${this.name}`
);
return false;
}
// Update the encrypted data with the current password
if (this.password) {
this.encryptedData = encrypt(this.credentials, this.password);
}
displaySuccessMessage(
`Credential "${key}" deleted successfully from vault ${this.name}`
);
return true;
}
// For persistence
toJSON() {
return {
name: this.name,
encryptedData: this.encryptedData,
isUnlocked: this.isUnlocked, // Save the lock state
credentials: this.isUnlocked ? this.credentials : [], // Save credentials if unlocked
password: this.password, // Include password for proper vault initialization
};
}
static fromJSON(json, password = null) {
const vault = new Vault(json.name, null); // Use null for password initially
vault.encryptedData = json.encryptedData;
vault.isUnlocked = json.isUnlocked; // Set the lock state from JSON
// If vault is unlocked, restore credentials
if (vault.isUnlocked && Array.isArray(json.credentials)) {
vault.credentials = json.credentials;
}
// Always set the password if provided, regardless of unlock state
if (password) {
vault.password = password;
}
return vault;
}
// Generate a shareable ID containing encrypted vault data
generateShareId() {
if (!this.isUnlocked) {
displayErrorMessage("Vault must be unlocked to share");
return null;
}
try {
// Always re-encrypt current credentials to ensure they're included
// This ensures we have valid encrypted data even for empty credential lists
const encryptedData = encrypt(this.credentials, this.password);
const shareData = {
name: this.name,
encryptedData: encryptedData,
password: this.password,
};
// Convert to base64
return Buffer.from(JSON.stringify(shareData)).toString("base64");
} catch (error) {
displayErrorMessage("Failed to generate share ID: " + error.message);
return null;
}
}
// Create a vault from a share ID
static fromShareId(shareId) {
try {
// Decode share data
const shareData = JSON.parse(Buffer.from(shareId, "base64").toString());
// Create new vault with original name and password
const vault = new Vault(shareData.name, shareData.password);
// Set the encrypted data and verify it's valid
try {
const decryptedCredentials = decrypt(
shareData.encryptedData,
shareData.password
);
vault.encryptedData = shareData.encryptedData;
// Store the decrypted credentials in the vault
// This is the fix - we need to store the credentials after decryption
vault.credentials = decryptedCredentials;
return vault;
} catch (error) {
displayErrorMessage("Invalid vault data in share ID");
return null;
}
} catch (error) {
displayErrorMessage("Invalid share ID");
return null;
}
}
}
export default Vault;