52

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);
});
user2226755
  • 12,494
  • 5
  • 50
  • 73

12 Answers12

59

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/master/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;
guybedford
  • 1,362
  • 11
  • 8
  • 1
    Though this code works fine, I wonder if it's _proper_. You don't pass the callback to `stdout.write` but call it with success unconditionally, and suppress any errors that way. Unfortunately, passing the callback makes it stop working and requires the muting to be done with a timeout of 0 (which then smells like a race condition to me). – Marnes Sep 20 '20 at 11:58
  • Don't miss **`Enter`** key: *`if (!this.muted || ['\n', '\r\n'].includes(chunk.toString())) ...`* – Mir-Ismaili Jan 03 '22 at 01:21
52

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);
user2226755
  • 12,494
  • 5
  • 50
  • 73
  • Thanks @Subject, the code works just as what I expected. Would you please explain a little bit about what the \033[2K\033D means? –  Jun 15 '14 at 17:22
  • 1
    \033[2K\033D, uses two sequences "Esc[2K" and "EscD". First, clear entire line, second move/scroll window up one line. – user2226755 Jun 18 '14 at 12:36
  • 3
    If you are using strict mode change '\033[2K\033[200D' to '\x1B[2K\x1B[200D'. This just changes the deprecated octal escapes into equivalent hexadecimal escapes. – Jeff Thompson Jul 29 '16 at 20:24
  • Thanks. It helps, but I cannot break my script with "ctrl+c" like usual. Why? – Kenjiro Nov 12 '16 at 11:17
  • @Kenjiro You need to set "CTRL+C" trigger on "SIGINT" with readline like this : https://nodejs.org/api/readline.html#readline_event_sigint – user2226755 Nov 12 '16 at 19:05
  • @HorsSujet I tried it and added it like this post http://stackoverflow.com/questions/21864127/nodejs-process-hangs-on-exit-ctrlc but Still can't. Do you know why? – Kenjiro Nov 14 '16 at 03:04
  • @Kenjiro Do you try : `rl.on("SIGINT", function() { rl.clearLine(); return process.exit(); });` ? – user2226755 Nov 18 '16 at 20:25
  • second solution doesn't work for me. First it shows my password as I type it and then a second time. (I see the second time is intentional). I really can't figure out why either solution would work (but the first does for me) since the question is executed prior to setting up the write settings – Michael Welch Aug 25 '18 at 22:05
  • I think the issue in second example is `if (!rl.stdoutMuted)` should not have the `!` – Michael Welch Aug 25 '18 at 22:07
  • yeah, if you remove the `!` from the `if (!rl.stdoutMuted)` the second example will work. – Michael Welch Aug 25 '18 at 22:09
  • 2
    The second solution has a weird behaviour of the return key. It prints `Password: `, then if you enter 'a' and press return the total content of the first output line is `Password: **` without a line break. Any subsequent console logging will log right after that in the same line. I fixed it by testing for the newline character in `rl._writeToOutput` like so: `if (rl.stdoutMuted && stringToWrite != '\r\n' && stringToWrite != '\n' && stringToWrite != '\r')` – chrwoizi Sep 12 '18 at 13:22
  • 1
    @chrwoizi yeah this is also true when erasing the password, as the interface will re-render the whole line with the test. try ``` if (password) { rl._writeToOutput = (s) => { const v = s.split(question) if (v.length == '2') { rl.output.write(question) rl.output.write('*'.repeat(v[1].length)) } else { rl.output.write('*') } } } ``` (https://github.com/artdecocode/reloquent/blob/master/src/lib/ask.js#L23) or NPM package reloquent – zavr Oct 07 '18 at 23:20
  • 4
    Using a private (internal part) of a class/interface (here __writeToOutput for readline) wouldn't be a good idea. The types can only commit to maintain their public interface. – Alex Sed Mar 30 '22 at 06:36
  • I hate the `_writeToOutput` solution (because of what @AlexSed mentioned) but it works perfectly and it's very simple. – derpedy-doo Jul 26 '23 at 20:24
16

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

estaples
  • 932
  • 8
  • 9
  • `readline-sync` in conjuction with `ttys` allows you to have a password prompt without showing the typed chars. Thanks for the link! :) – greduan Mar 11 '16 at 05:37
10

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();
});
grandinero
  • 1,155
  • 11
  • 18
  • 1
    Illustrates good solution / simple + effective, and doesn't require installing a module or overwriting a private API. Easy to add a flag to toggle between `keypress` processing depending whether question response should be hidden or not. – Shaun Feb 01 '21 at 05:08
  • 1
    This solution doesn't work for me as each character is shown briefly before being replaced – Kian Nov 30 '21 at 14:00
  • Suggest `rl.output.write('*'.repeat(len));` instead of `for` loop. – Ben Apr 21 '22 at 21:27
  • 1
    This still writes the input to stdout - before clearing it and writing `*`, depending on OS/setup may be intercepted/errornous thus MAY LEAK the output, whereas `_writeToOutput` is ugly but intercepts it before writing. – Michael B. Aug 14 '22 at 20:52
8

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));
sdgfsdh
  • 33,689
  • 26
  • 132
  • 245
  • 1
    For a multiple prompt script, to avoid extra new lines being printed for each call to `hiddenQuestion`, you'll want to put a `rl.close()` right before resolving the promise. – n8jadams Apr 06 '22 at 19:48
  • In addition, before the promise resolves you probably want `process.stdin.removeListener("data", onChar);` (this assumes the handler has been extracted into an `onChar` function. – Izhaki Jan 24 '23 at 10:28
5

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);
    });
}
Jeffrey Woo
  • 51
  • 1
  • 1
3

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 );
} );
Alex Glukhovtsev
  • 2,491
  • 2
  • 13
  • 11
2

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);
});
Kabir Sarin
  • 18,092
  • 10
  • 50
  • 41
1

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;
}
phatmann
  • 18,161
  • 7
  • 61
  • 51
  • I was not able to make this work correctly in an `async` main function, is this implementation only viable in sequential execution? – Huge Apr 19 '21 at 09:31
0

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
});
Hafthor
  • 16,358
  • 9
  • 56
  • 65
0

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...
})();
n8jadams
  • 991
  • 1
  • 9
  • 21
0

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)
})
Roman Rhrn Nesterov
  • 3,538
  • 1
  • 28
  • 16