UNPKG

pwm-cli

Version:

A secure command-line password manager

389 lines (340 loc) • 12.1 kB
#!/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();