user2226755
user2226755

Reputation: 13157

How to hide password in the nodejs console?

I want to hide password input. I see many answers in stackoverflow but I can't verify value if I press backspace. The condition return false.

I tried several solution to overwrite the function but I got an issue with buffer if I press backspace, I got invisible character \b.

I press : "A", backspace, "B", I have in my buffer this : "\u0041\u0008\u0042" (toString() = 'A\bB') and not "B".

I have :

var readline = require('readline');

var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

rl.question("password : ", function(password) {
    console.log("Your password : " + password);
});

Upvotes: 60

Views: 48189

Answers (16)

SkySpiral7
SkySpiral7

Reputation: 412

This is my comprehensive solution that will run in node without anything from npm. It uses promises and has an option for disabling the echo back when typing. This works in node 20.11.1 and doesn't use the deprecated/removed things other answers have and has no race condition. When it is silent no replacement symbols are shown but it does support backspace and pasting.

'use strict';
const readline = require('readline');
const stream = require('stream');

const prompt = (query, promptOptions = {}) =>
  new Promise((resolve) => {
    const mutableStdout = new stream.Writable({
      write: (chunk, encoding, callback) => {
        //don't print anything typed when silent
        if (!promptOptions.silent) {
          process.stdout.write(chunk, encoding);
        }
        callback();
      },
    });
    const rl = readline.createInterface({
      input: process.stdin,
      output: mutableStdout,
      terminal: promptOptions.silent,
    });

    //have to print here so that query is shown even when stdin is silent
    process.stdout.write(query);
    rl.question('', (answer) => {
      if (promptOptions.silent) {
        rl.history = rl.history.slice(1);
        //enter will submit without printing. this puts the output on the next line
        process.stdout.write('\n');
      }
      rl.close();
      resolve(answer);
    });
  });

async function main() {
  const user = await prompt('User Name: ');
  const pass = await prompt('Password: ', { silent: true });
  console.log(`${user}:${pass}`);
}

const shell = {
  GENERIC_FAILURE_CODE: 1,
  SUCCESS_CODE: 0,
};
main()
  .then(() => {
    process.exit(shell.SUCCESS_CODE);
  })
  .catch((error) => {
    console.error('main failed', error);
    process.exit(shell.GENERIC_FAILURE_CODE);
  });

Upvotes: 1

Big McLargeHuge
Big McLargeHuge

Reputation: 16056

This might be the easiest solution:

import { createInterface } from 'node:readline/promises';

const rl = createInterface({
    input: process.stdin,
    output: null,
    terminal: true,
});

const password = await rl.question();
rl.close();

This answer is close to the same but more complicated.

Upvotes: 2

guybedford
guybedford

Reputation: 1422

This can be handled with readline by intercepting the output through a muted stream, as is done in the read project on npm (https://github.com/isaacs/read/blob/v1.0.7/lib/read.js):

var readline = require('readline');
var Writable = require('stream').Writable;

var mutableStdout = new Writable({
  write: function(chunk, encoding, callback) {
    if (!this.muted)
      process.stdout.write(chunk, encoding);
    callback();
  }
});

mutableStdout.muted = false;

var rl = readline.createInterface({
  input: process.stdin,
  output: mutableStdout,
  terminal: true
});

rl.question('Password: ', function(password) {
  console.log('\nPassword is ' + password);
  rl.close();
});

mutableStdout.muted = true;

Upvotes: 65

Ohad Schneider
Ohad Schneider

Reputation: 38112

The accepted answer suggests we imitate the read package (https://www.npmjs.com/package/read), but why not just use it? It has almost 5M weekly downloads, regularly updated, and among the contributors you have the guy behind npm itself...

import { read } from 'read';

const password = await read({
  prompt: "password: ",
  silent: true,
  replace: "*"
};

console.log("Your password: " + password);

Upvotes: 4

Partly Cloudy
Partly Cloudy

Reputation: 6915

I liked the _writeToOutput override, but the solutions here don't seem to handle the arrow-up/history case; I've added that, and TypeScripted it. Makes use of the fact that readline sends the full query string when you navigate history, and only a single character normally.

Checking the input or using a proper library are better choices, but not always worth the effort. This ios quick-and-dirty, probably still breaks in cases, but I haven't found them (TM):

let echo = '*';  // or '' if you prefer
let first: string|undefined;

(this.readline as any)._writeToOutput = (c: string) => {
  if (first==undefined || c.length!=1) {
    if (first==undefined) first = c;
    if (c.startsWith(first)) {
      // rewriting prompt
      (this.readline as any).output?.write(first);
      c = c.slice(first.length);
    } else if (!c.trim()) {
      // user pressed enter, show the enter
      (this.readline as any).output?.write(c);
      c = '';
    }
  }
  for (let i = 0; i < c.length; i++) {
    // all other input, and bits after the prompt, use echo char
    (this.readline as any).output?.write(echo);
  }
};

And after completion, don't forget this from https://stackoverflow.com/a/24037546:

(this.readline as any).history = (this.readline as any).history?.slice(1);

Upvotes: 0

Roman Rhrn Nesterov
Roman Rhrn Nesterov

Reputation: 3673

Unix like style. No symbols on put/paste at all.

rl.input.on('keypress', (c) => {
  if (c.charCodeAt() === 127) {
    const len = rl.line.length
    readline.moveCursor(rl.output, -len, 0)
    readline.clearLine(rl.output, 1)
    return
  }
  readline.moveCursor(rl.output, -1, 0)
  readline.clearLine(rl.output, 1)
})

Upvotes: 0

n8jadams
n8jadams

Reputation: 1164

Here's my own take on this, cherry picking from the other answers here. When the user types, there is no output. This is to prevent any data leaks.

const readline = require("readline");
    
const hiddenQuestion = (query) =>
    new Promise((resolve) => {
        console.log(query);
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
        });
        rl._writeToOutput = () => {};
        const stdin = process.openStdin();
        process.stdin.on("data", (char) => {
            char = char.toString("utf-8");
            switch (char) {
                case "\n":
                case "\r":
                case "\u0004":
                    // Finished writing their response
                    stdin.pause();
                    break;
                // You might make this case optional, (Ctrl-C)
                case "\u0003":
                    // Ctrl-C
                    process.exit(0);
                default:
                    process.stdout.clearLine();
                    readline.cursorTo(process.stdout, 0);
                    break;
            }
        });
        rl.question("", (value) => {
            rl.history = rl.history.slice(1);
            rl.close();
            resolve(value);
        });
    });

// Usage example:
void (async () => {
    const password = await hiddenQuestion("What is your password?");
    // do what you want with the password...
})();

Upvotes: 0

Kabir Sarin
Kabir Sarin

Reputation: 18526

A promisified typescript native version:

This will also handle multiple question calls (as @jeffrey-woo pointed out). I chose not to replace input with *, as it didn't feel very unix-y, and I found it to be glitchy sometimes if typing too fast anway.

import readline from 'readline';

export const question = (question: string, options: { hidden?: boolean } = {}) =>
  new Promise<string>((resolve, reject) => {
    const input = process.stdin;
    const output = process.stdout;

    type Rl = readline.Interface & { history: string[] };
    const rl = readline.createInterface({ input, output }) as Rl;

    if (options.hidden) {
      const onDataHandler = (charBuff: Buffer) => {
        const char = charBuff + '';
        switch (char) {
          case '\n':
          case '\r':
          case '\u0004':
            input.removeListener('data', onDataHandler);
            break;
          default:
            output.clearLine(0);
            readline.cursorTo(output, 0);
            output.write(question);
            break;
        }
      };
      input.on('data', onDataHandler);
    }

    rl.question(question, (answer) => {
      if (options.hidden) rl.history = rl.history.slice(1);
      rl.close();
      resolve(answer);
    });
  });

Usage:

(async () => {
  const hiddenValue = await question('This will be hidden', { hidden: true });
  const visibleValue = await question('This will be visible');
  console.log('hidden value', hiddenValue);
  console.log('visible value', visibleValue);
});

Upvotes: 3

Hafthor
Hafthor

Reputation: 16906

Here's my solution which doesn't require any external libraries (besides readline) or a lot of code.

// turns off echo, but also doesn't process backspaces
// also captures ctrl+c, ctrl+d
process.stdin.setRawMode(true); 

const rl = require('readline').createInterface({input: process.stdin});
rl.on('close', function() { process.exit(0); }); // on ctrl+c, doesn't work? :(
rl.on('line', function(line) {
    if (/\u0003\.test(line)/) process.exit(0); // on ctrl+c, but after return :(
    // process backspaces
    while (/\u007f/.test(line)) {
        line = line.replace(/[^\u007f]\u007f/, '').replace(/^\u007f+/, '');
    }

    // do whatever with line
});

Upvotes: 1

grandinero
grandinero

Reputation: 1225

Another method using readline:

var readline = require("readline"),
    rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });

rl.input.on("keypress", function (c, k) {
  // get the number of characters entered so far:
  var len = rl.line.length;
  // move cursor back to the beginning of the input:
  readline.moveCursor(rl.output, -len, 0);
  // clear everything to the right of the cursor:
  readline.clearLine(rl.output, 1);
  // replace the original input with asterisks:
  for (var i = 0; i < len; i++) {
    rl.output.write("*");
  }
});

rl.question("Enter your password: ", function (pw) {
  // pw == the user's input:
  console.log(pw);
  rl.close();
});

Upvotes: 10

phatmann
phatmann

Reputation: 18493

You can use the prompt module, as suggested here.

const prompt = require('prompt');

const properties = [
    {
        name: 'username', 
        validator: /^[a-zA-Z\s\-]+$/,
        warning: 'Username must be only letters, spaces, or dashes'
    },
    {
        name: 'password',
        hidden: true
    }
];

prompt.start();

prompt.get(properties, function (err, result) {
    if (err) { return onErr(err); }
    console.log('Command-line input received:');
    console.log('  Username: ' + result.username);
    console.log('  Password: ' + result.password);
});

function onErr(err) {
    console.log(err);
    return 1;
}

Upvotes: 2

user2226755
user2226755

Reputation: 13157

Overwrite _writeToOutput of application's readline interface : https://github.com/nodejs/node/blob/v9.5.0/lib/readline.js#L291

To hide your password input, you can use :

FIRST SOLUTION : "password : [=-]"

This solution has animation when you press a touch :

password : [-=]
password : [=-]

The code :

var readline = require('readline');

var rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.stdoutMuted = true;

rl.query = "Password : ";
rl.question(rl.query, function(password) {
  console.log('\nPassword is ' + password);
  rl.close();
});

rl._writeToOutput = function _writeToOutput(stringToWrite) {
  if (rl.stdoutMuted)
    rl.output.write("\x1B[2K\x1B[200D"+rl.query+"["+((rl.line.length%2==1)?"=-":"-=")+"]");
  else
    rl.output.write(stringToWrite);
};

This sequence "\x1B[2K\x1BD" uses two escapes sequences :

  • Esc [2K : clear entire line.
  • Esc D : move/scroll window up one line.

To learn more, read this : http://ascii-table.com/ansi-escape-sequences-vt-100.php

SECOND SOLUTION : "password : ****"

var readline = require('readline');

var rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.stdoutMuted = true;

rl.question('Password: ', function(password) {
  console.log('\nPassword is ' + password);
  rl.close();
});

rl._writeToOutput = function _writeToOutput(stringToWrite) {
  if (rl.stdoutMuted)
    rl.output.write("*");
  else
    rl.output.write(stringToWrite);
};

You can clear history with :

rl.history = rl.history.slice(1);

Upvotes: 56

sdgfsdh
sdgfsdh

Reputation: 37045

My solution, scraped together from various bits online:

import readline from 'readline';

export const hiddenQuestion = query => new Promise((resolve, reject) => {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });
  const stdin = process.openStdin();
  process.stdin.on('data', char => {
    char = char + '';
    switch (char) {
      case '\n':
      case '\r':
      case '\u0004':
        stdin.pause();
        break;
      default:
        process.stdout.clearLine();
        readline.cursorTo(process.stdout, 0);
        process.stdout.write(query + Array(rl.line.length + 1).join('*'));
        break;
    }
  });
  rl.question(query, value => {
    rl.history = rl.history.slice(1);
    resolve(value);
  });
});

Usage is like this:

// import { hiddenQuestion } from './hidden-question.js';

const main = async () => {
  console.log('Enter your password and I will tell you your password! ');
  const password = await hiddenQuestion('> ');
  console.log('Your password is "' + password + '". ');
};

main().catch(error => console.error(error));

Upvotes: 8

Alex Glukhovtsev
Alex Glukhovtsev

Reputation: 2561

Also one can use tty.ReadStream
changing mode of process.stdin
to disable echoing input characters.

let read_Line_Str = "";
let credentials_Obj = {};
process.stdin.setEncoding('utf8');
process.stdin.setRawMode( true );
process.stdout.write( "Enter password:" ); 
process.stdin.on( 'readable', () => {
  const chunk = process.stdin.read();
  if ( chunk !== null ) {
    read_Line_Str += chunk;
    if( 
      chunk == "\n" ||
      chunk == "\r" ||
      chunk == "\u0004"
    ){
      process.stdout.write( "\n" );
      process.stdin.setRawMode( false );
      process.stdin.emit('end'); /// <- this invokes on.end
    }else{
      // providing visual feedback
      process.stdout.write( "*" );  
    }  
  }else{
    //console.log( "readable data chunk is null|empty" );
  }
} );
process.stdin.on( 'end', () => {
  credentials_Obj.user = process.env.USER;
  credentials_Obj.host = 'localhost';
  credentials_Obj.database = process.env.USER;
  credentials_Obj.password = read_Line_Str.trim();
  credentials_Obj.port = 5432;
  //
  connect_To_DB( credentials_Obj );
} );

Upvotes: 3

estaples
estaples

Reputation: 982

You can use the readline-sync module instead of node's readline.

Password-hiding functionality is built in via it's "hideEchoBack" option.

https://www.npmjs.com/package/readline-sync

Upvotes: 17

Jeffrey Woo
Jeffrey Woo

Reputation: 51

Wanted to add to the marked solution#2.

When we detect the line-ends, I believe we should remove the event handler instead of just stdin.pause(). This can be an issue if you are waiting on rl.question/rl.prompt elsewhere. In those cases, if stdin.pause() was used, it would just exit the program without giving any errors and can be quite annoying to debug.

function hidden(query, callback) {
    var stdin = process.openStdin();
    var onDataHandler = function(char) {
        char = char + "";
        switch (char) {
          case "\n": case "\r": case "\u0004":
            // Remove this handler
            stdin.removeListener("data",onDataHandler); 
            break;//stdin.pause(); break;
          default:
            process.stdout.write("\033[2K\033[200D" + query + Array(rl.line.length+1).join("*"));
          break;
        }
    }
    process.stdin.on("data", onDataHandler);

    rl.question(query, function(value) {
      rl.history = rl.history.slice(1);
      callback(value);
    });
}

Upvotes: 5

Related Questions