pwm-cli
Version:
A secure command-line password manager
389 lines (340 loc) ⢠12.1 kB
JavaScript
#!/usr/bin/env node
const scryptJs = require('scrypt-js');
const CryptoJS = require("crypto-js");
const fs = require('fs');
const crypto = require("crypto");
const zlib = require('zlib');
const cryptico = require("cryptico");
const jsonfile = require('jsonfile');
const inquirer = require('inquirer');
const chalk = require('chalk');
const figlet = require('figlet');
const clipboardy = require('clipboardy');
// Password types with detailed descriptions
const PASSWORD_TYPES = [
{
name: `Maximum Security ${chalk.gray('(e.g., "P@x9K#m2$vL5")')}`,
value: 'maximum',
description: 'Uses uppercase, lowercase, numbers, and special characters'
},
{
name: `Long Password ${chalk.gray('(e.g., "Kj9nMp4vL8x2")')}`,
value: 'long',
description: 'Uses uppercase, lowercase, and numbers, no special characters'
},
{
name: `Medium Length ${chalk.gray('(e.g., "Nx7kP4mJ")')}`,
value: 'medium',
description: '8 characters with mixed case and numbers'
},
{
name: `Basic Password ${chalk.gray('(e.g., "a2n9m4k8")')}`,
value: 'basic',
description: 'Simple combination of lowercase letters and numbers'
},
{
name: `Short Password ${chalk.gray('(e.g., "Kx9n")')}`,
value: 'short',
description: '4 characters, mixed case and one number'
},
{
name: `PIN ${chalk.gray('(e.g., "7294")')}`,
value: 'pin',
description: '4-digit number, useful for numeric-only requirements'
},
{
name: `Memorable Name ${chalk.gray('(e.g., "ralphsmith")')}`,
value: 'name',
description: 'Lowercase letters only, resembles a name'
},
{
name: `Passphrase ${chalk.gray('(e.g., "correct horse battery staple")')}`,
value: 'phrase',
description: 'Multiple random words combined, very secure and memorable'
}
];
// Display welcome banner
console.log(chalk.cyan(figlet.textSync('PWM CLI', { horizontalLayout: 'full' })));
console.log(chalk.yellow('\nStateless Password Manager - Generates passwords on-the-fly, stores nothing!\n'));
console.log(chalk.gray('⢠Your passwords are generated using cryptographic algorithms'));
console.log(chalk.gray('⢠Same inputs always produce the same password'));
console.log(chalk.gray('⢠No passwords are ever stored anywhere\n'));
async function create_key(master_password, name) {
const key = Buffer.from(master_password);
const salt = Buffer.from("com.cydteam.password" + name + name.length);
// scrypt parameters
const N = 32768;
const r = 8;
const p = 2;
const dkLen = 64;
try {
const result = await scryptJs.scrypt(
key,
salt,
N,
r,
p,
dkLen
);
return Buffer.from(result).toString('hex');
} catch (err) {
console.error(chalk.red('Error in key generation:', err));
throw err;
}
}
function create_template(key, site_name, counter = 0) {
var message = site_name + site_name.length + counter + "com.cydteam.password";
var hash = CryptoJS.HmacSHA256(message, key);
//template = template.toString(CryptoJS.enc.Hex);
let seed = new Uint8Array(hash.words.length * 4 /*sizeof(int32)*/ );
let seedView = new DataView(seed.buffer, seed.byteOffset, seed.byteLength);
// Loop over hash.words which are INT32
for (let i = 0; i < hash.words.length; i++) {
// Set seed[i*4,i*4+4] to hash.words[i] INT32 in big-endian form
seedView.setInt32(i * 4 /*sizeof(int32)*/ , hash.words[i], false /*big-endian*/ );
}
return seed;
}
/*
C = BCDFGHJKLMNPQRSTVWXYZ
v = aeiou
V = AEIOU
c = bcdfghjklmnpqrstvwxyz
n = 0123456789
o = @&%?,=[]_:-+*$#!'^~;()/.
x = AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()
a = AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz
*/
templates = {
maximum: [
"anoxxxxxxxxxxxxxxxxx",
"axxxxxxxxxxxxxxxxxno",
"xoxxxxxxxxxxxxxxxxxo"
],
long: [
"CvcvnoCvcvCvcv",
"CvcvCvcvnoCvcv",
"CvcvCvcvCvcvno",
"CvccnoCvcvCvcv",
"CvccCvcvnoCvcv",
"CvccCvcvCvcvno",
"CvcvnoCvccCvcv",
"CvcvCvccnoCvcv",
"CvcvCvccCvcvno",
"CvcvnoCvcvCvcc",
"CvcvCvcvnoCvcc",
"CvcvCvcvCvccno",
"CvccnoCvccCvcv",
"CvccCvccnoCvcv",
"CvccCvccCvcvno",
"CvcvnoCvccCvcc",
"CvcvCvccnoCvcc",
"CvcvCvccCvccno",
"CvccnoCvcvCvcc",
"CvccCvcvnoCvcc",
"CvccCvcvCvccno"
],
medium: [
"CvcnoCvc",
"CvcCvcno"
],
basic: [
"aaanaaan",
"aannaaan",
"aaannaaa"
],
short: [
"Cvcn"
],
pin: [
"nnnn"
],
name: [
"cvccvcvcv"
],
phrase: [
"cvcc cvc cvccvcv cvc",
"cvc cvccvcvcv cvcv",
"cv cvccv cvc cvcvccv"
]
};
passchars = {
V: "AEIOU",
C: "BCDFGHJKLMNPQRSTVWXYZ",
v: "aeiou",
c: "bcdfghjklmnpqrstvwxyz",
A: "AEIOUBCDFGHJKLMNPQRSTVWXYZ",
a: "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz",
n: "0123456789",
o: "@&%?,=[]_:-+*$#!'^~;()/.",
x: "AEIOUaeiouBCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz0123456789!@#$%^&*()",
" ": " "
};
function create_password(seed, type) {
switch (type) {
case "maximum":
template = templates.maximum[seed[0] % templates.maximum.length];
break;
case "pin":
template = templates.pin[seed[0] % templates.pin.length];
break;
case "name":
template = templates.name[seed[0] % templates.name.length];
break;
case "phrase":
template = templates.phrase[seed[0] % templates.phrase.length];
break;
case "short":
template = templates.short[seed[0] % templates.short.length];
break;
case "basic":
template = templates.basic[seed[0] % templates.basic.length];
break;
case "long":
template = templates.long[seed[0] % templates.long.length];
break;
case "medium":
template = templates.medium[seed[0] % templates.medium.length];
break;
case "phrase":
template = templates.phrase[seed[0] % templates.phrase.length];
break;
}
var password = template.split("").map(function(c, i) {
// Use passchars to map the template string (e.g. xxx...xxx)
// to characters (e.g. c -> bcdfghjklmnpqrstvwxyz)
let chars = passchars[c];
// Select the character using seed[i + 1]
return chars[seed[i + 1] % chars.length];
}).join("");
return password;
}
function create_passphrase(seed) {
var array = fs.readFileSync('10000words.txt').toString().split("\n");
var phrase = "";
for (var i = 0; i < 5; i++) {
phrase += array[seed[i + 1] % array.length];
phrase += " ";
}
return phrase;
}
function aes_encrypt(seed, RSAPublickey) {
var asymmetric_key = create_password(seed, "phrase");
console.log('The symm key used while encrypting is:', asymmetric_key)
key_writer(asymmetric_key);
const cipher = crypto.createCipher('aes-256-ctr', asymmetric_key);
const encInput = fs.createReadStream('test.txt');
const encOutput = fs.createWriteStream('test.encrypted');
encInput.pipe(cipher).pipe(encOutput).on('close', function() {
console.log('Encryption was done!')
aes_decrypt(seed, RSAPrivatekey, RSAPublickey); //decrypt is called here!!
});
}
function key_writer(asymmetric_key) {
var EncryptionResult = cryptico.encrypt(asymmetric_key, RSAPublickey);
var file = 'key.json'
jsonfile.writeFile(file, EncryptionResult, function(err) {
if (err)
console.error(err)
})
}
function create_rsa_keys(seed, flag) {
var PassPhrase = create_passphrase(seed);
var Bits = 1024;
var RSAPrivatekey = cryptico.generateRSAKey(PassPhrase, Bits);
var RSAPublickey = cryptico.publicKeyString(RSAPrivatekey);
if (flag == 0)
return RSAPublickey;
else
return RSAPrivatekey;
}
function aes_decrypt(seed, RSAPrivatekey, RSAPublickey) {
var file = 'key.json'
var key_from_file = jsonfile.readFileSync(file);
var DecryptionResult = cryptico.decrypt(key_from_file.cipher, RSAPrivatekey);
console.log('The symm key for AES decryption is:', DecryptionResult.plaintext);
var key = DecryptionResult.plaintext
const decipher = crypto.createDecipher('aes-256-ctr', key);
const decInput = fs.createReadStream('test.encrypted');
const decOutput = fs.createWriteStream('test.decrypted');
decInput.pipe(decipher).pipe(decOutput).on('close', function() {
console.log('Decryption was done!')
});
}
async function promptUser() {
try {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: chalk.green('Enter your identifier name:'),
prefix: 'š',
suffix: chalk.gray(' (this helps make your passwords unique)'),
validate: input => input.length > 0 ? true : 'Name cannot be empty'
},
{
type: 'password',
name: 'master_password',
message: chalk.green('Enter your master password:'),
prefix: 'š',
suffix: chalk.gray(' (minimum 8 characters)'),
mask: '*',
validate: input => input.length >= 8 ? true : 'Password must be at least 8 characters'
},
{
type: 'input',
name: 'site_name',
message: chalk.green('Enter the site or service name:'),
prefix: 'š',
suffix: chalk.gray(' (e.g., "github.com", "netflix")'),
validate: input => input.length > 0 ? true : 'Site name cannot be empty'
},
{
type: 'list',
name: 'type',
message: chalk.green('Choose password type:'),
prefix: 'š',
choices: PASSWORD_TYPES.map(type => ({
name: `${type.name}\n ${chalk.gray(type.description)}`,
value: type.value,
short: type.name.split(' ')[0]
})),
pageSize: 12
}
]);
console.log(chalk.cyan('\nāļø Generating secure password...\n'));
const key = await create_key(answers.master_password, answers.name);
const seed = create_template(key, answers.site_name, 1);
let password;
if(answers.type === "phrase") {
password = create_passphrase(seed);
} else {
password = create_password(seed, answers.type);
}
console.log(chalk.green('Generated Password: ') + chalk.bold.white(password));
console.log(chalk.gray('\nThis password will always be generated for these inputs'));
// Prompt for clipboard copy
const { shouldCopy } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldCopy',
message: chalk.yellow('Copy password to clipboard?'),
prefix: 'š',
default: true
}
]);
if (shouldCopy) {
try {
await clipboardy.write(password);
console.log(chalk.green('\nā Password copied to clipboard'));
} catch (error) {
console.log(chalk.red('\nā Failed to copy to clipboard:', error.message));
}
}
} catch (err) {
console.error(chalk.red('Error:', err.message));
process.exit(1);
}
}
// Start the application
promptUser();