30

Build a Password Field for the Terminal using Nodejs

 4 years ago
source link: https://blog.bitsrc.io/build-a-password-field-for-the-terminal-using-nodejs-31cd6cfa235
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to build a reusable password field for the command line interface

eAZrEvE.png!web

You’re probably familiar with the popular password field that shows up whenever you want to push your local repo to remote using Git Bash. It prompts for a password, and whenever you type, nothing is displayed on the terminal. I was confused at first and thought maybe my terminal lost focus. I tried typing several times until I finally decided to hit the Enter key. It was then that I realize my text was registered and my anxiety quickly dissipated.

Have you ever thought about how they managed to implement that? If you wanna know what happens beneath then this article is for you.

This article will cover two topics:

  1. Building a password field for the terminal using Nodejs
  2. Sharing the Nodejs password field module/component to a component collection in bit.dev (to share with others and reuse in different projects)

Setup

Create a Node project and make it look like this:

pswd-node -
    - src
        - pswnode.js
    - package.json
    - index.js

We will be working on the pswnode.js file.

Indefinite password

The trick here is to hook into the terminal/command-line event that triggers the data event when a key is pressed. We will have the power to write to the terminal screen whatever we want.

process.stdin is used to capture inputs to the terminal, it emits the data event whenever when type on the terminal. So we will register our function handler to the process.stdin data event to call the handler whenever we type on the terminal. It is in this handler we will display anything and in this case configure our password.

// src/pswnode.js
const stdout = process.stdout
const stdin = process.stdin
const readline = require("readline")

We set the process objects to shorter variable names :) to avoid long typing.

stdout is for writing to the terminal stdin is for capturing inputs to the terminal.

// src/pswnode.js
const stdout = process.stdout
const stdin = process.stdin
const readline = require("readline")stdout.write("password:")
stdin.setRawMode(true)
stdin.resume()
stdin.setEncoding('utf-8')
let input = ''

We write "password:" to the screen, yes to notify users that they are to input their password. Next, we set raw mode to true so as not to get streams when enter is pressed. stdin.resume, this will resume the terminal in the parent process to avoid the Node app quitting. We set the keycode encoding to utf-8 so to get the key names in string, not in binary.

The input variable is what we will use to gather the password. We will concatenate each keystroke to it, to form the password.

...
const pn = (data) => {
    const c = data
    switch (c) {
        case '\u0004': // Ctrl-d
        case '\r':
        case '\n':
            return enter()
        case '\u0003': // Ctrl-c
            return ctrlc()
        default:
            // backspace
            if (c.charCodeAt(0) === 8) return backspace()
            else return newchar(c)
    }
}

We have the function handler here, this function will be called anytime we type on the terminal.

The keystroke is received in the data param. We used a switch statement to perform different actions based on the value of the param data . We have actions for when enter is pressed; the enter function is called. Action for ctrl-c, when we want to exit the ctrlc function is called. The default case checks if the data is a backspace, to call the backspace function. If not newchar is called, this will handle the general keys.

// src/pswnode.js
...
stdin.on("data", pn)function enter() {
    stdin.removeListener('data', pn)
    l("\nYour password is: " + input)
    stdin.setRawMode(false)
    stdin.pause()
}function ctrlc() {
    stdin.removeListener('data', pn)
    stdin.setRawMode(false)
    stdin.pause()
}function newchar(c) {
    input += c
}function backspace() {
    input = input.slice(0, input.length - 1)
}

Here, we register the function handler. The stdin.on(...) is used to do so. The first argument states the event you want to hook into and the second argument, the function you want to be called when the event of the first arg is fired/emitted. Here, we put data that is the event emitted when we type on the terminal and we put our pn function handler in the second arg.

We defined all the functions we saw above.

We have enter , called when the Enter key is pressed. When pressed it means the user is done entering password, so we remove the function handler from the data event, log the user's password we snagged in the input variable, then we set raw mode to false and pause.

The ctrlc function handles the ctrl-c key, it means a sudden exit. Here it removes the pn handler from the data event then we set raw mode to false and pause.

The newchar handles any other key, it accepts the key in its c param, then concatenates the param to the input variable, this builds up the password in the input variable as the user types in. Then the function exits. With this nothing is printed to the terminal :) the user won't see his/her password because we didn't write it to the stdout stream but the password will be held in the input variable.

The backspace handles the delete key, this key is pressed when the user wants to delete a character from the password. The user doesn't see it but builds it up mentally, so he knows when an incorrect password character is typed and wants to delete. We use the slice method in String to remove the last character in the string we built up in the input variable then reassign the result to the input .

When the user types

n => input = n

n => input = nn

a => input = nna

m => input = nnam

i => input = nnami

Pressing backspace, the input will be nnam .

So, our full code will be:

Now run the pswnode.js file node src/pswnode . You will see password: printed, now type in.

$ node src/pswnode
password:

After typing press enter

$ node src/pswnode
password: 
Your password is: nnamdi

Your password is: will be displayed along with the password you typed. Mine is nnamdi .

Masking with *

Now, let’s make the user feel some output.

We will display an * asterisk whenever a key is pressed. We will add stdout.write("*") in our newchar function.

// src/pswnode.js
...
function newchar(c) {
    input += c
    stdout.write("*")
}
...

Now, when we type, * is displayed for each keystroke.

$ node src/pswnode
password: ******

On enter :

$ node src/pswnode
password: ******
Your password is: nnamdi

We have a problem if we hit the backspace to delete a character. The asterisk should decrement by one but none of that is happening. We have to refactor our backspace function.

We will require the readline library:

// src/pswnode.js
const stdout = process.stdout
const stdin = process.stdin
const readline = require("readline")...

The terminal is made up of cells where data can be written. To write to a cell you must triangulate the position of the cell in an x-y plane. The readline library has methods to helps us use the x-y plane of the terminal to position our cursor. The position of the cursor determines where data will be written to next.

Now, in this backspace problem, we need to move the cursor backward and write an empty string to it. This removes the * data on the cell. But first, we need to find the x,y position of the previous cell.

The position can be determined by adding the "password:" length and the input length and subtracting the result by 1. This will give us the x position. The y- will be 0 because "password:" is displayed at the topmost level.

With the x-y position gotten, we will use readline.cursorTo(...) method to place the cursor at the previous line, then we will write empty string sing stdout and use readline.readline.moveCursor(stdout, -1, 0) to move the cursor display move backwards.

// src/pswnode.js
...
function backspace() {
    const pslen = "password:".length
    readline.cursorTo(stdout, (pslen + input.length) - 1, 0)
    stdout.write(" ")
    readline.moveCursor(stdout, -1, 0)
    input = input.slice(0, input.length - 1)
}

Now, we run it:

$ node src/pswnode
password: ******

Now if we press backspace the last * will be deleted

$ node src/pswnode
password: *****

Press backspace again:

$ node src/pswnode
password: ****

Press it again:

$ node src/pswnode
password: ***

See the * decrements by 1.

Limiting our password

What we have above is an indefinite password field that you can type on as long as you want. Now, let’s make the password limited to a number like 6 or 7.

Simple, we add an if statement check in the newchar function:

// src/pswnode.js
...
function newchar(c) {
    if (input.length != 7) {
        input += c
        stdout.write("*")
    }
}
...

We limited our password to a length of 7. Once we type and exceed 7, * will not be displayed and the keystroke will not be joined to the input variable.

Let’s try it:

$ node src/pswnode
password: *******

I typed nnamdii , 7 characters. Now, type i :

$ node src/pswnode
password: *******

Nothing is displayed. Now press enter

$ node src/pswnode
password: *******
Your password is: nnamdii

Our password is still nnamdii .

We have hardcoded our values so far, we will make it flexible very soon.

Now, let’s see another way we can make a terminal password field using Nodejs.

Using readline.question(...)

Create a file pswnode2.js in the src folder and open it with a file editor, VS Code preferably.

Now require some libraries:

// src/pswnode2.js
const readline = require("readline")
const Writable = require("stream").Writable
const stdout = process.stdout

We required the readline library and the required the stream library then accessed the Writable class.

Now, we will create our Writable instance with our configurations:

// src/pswnode2.js
...
const mutedStdOut = new Writable({
    write: function(chunk, encoding, callback) {
        if (!this.muted) 
            stdout.write(chunk, encoding)
        callback()
    }
})
mutedStdOut.muted = false

We have our own write method. this write method in Writable instance is called whenever we type to the terminal. Here we hook into it to not display anything typed by the user.

// src/pswnode2
...
const rdl = readline.createInterface({
    input: process.stdin,
    output: mutedStdOut,
    terminal: true
})rdl.question("password:", (chunk) => {
    l("\nYour password is: " + chunk)
    rdl.close()
})mutedStdOut.muted = true

We create a ReadLine instance using the createInterface method. We set the input to process.stdin and set the ouput to our Writable instance mutedStdOut .

Next, we set the question :) passing in the name to display and the callback function. This callback function will be called when the user presses the enter button.

The password typed is passed to the callback function in the chunk param

Now, let’s run the file:

$ node src/pswnode2
password:

Now, type your password. I type nnamdi . Notice nothing is displayed.

Now, if we hit the enter key:

$ node src/pswnode2
password:
Your password is: nnamdi

See our password is displayed.

Making our first implementation flexible

Now, it has an option for the name you want to be displayed and the password length. It has default configuration in case any is missing. I made the default name and length to be password: and 7 respectively.

To run it, we can just create an instance of Password passing in our options and calling start method.

// src/pswnode.js
...
const pass = new Password({ ask: "emm your password: ", passLength: 10 })
pass.start()

Running this will give us:

$ node src/pswnode
emm your password: **********
Your password is: 1234567899

Let’s share it using Bit

Bit is a tool and a platform that enables you to share and collaborate on modules/components across repositories to build faster as a team.

With Bit you can get all the benefits of working in a multi-repository environment while gaining the code-sharing flexibility of a monorepo.

RzERBnM.jpg Example: Lamda util functions in a bit component collection

To use Bit , make sure your component is re-usable and strictly obeys the SRP. Our Password implementation is a good example of a component shareable using Bit.

To move our implementation to Bit is super simple, first install the bit CLI globally.

$ npm i bit-bin -g

Login (after signing up on bit.dev )

$ bit login

Then, init a Bit enviorment in the project folder:

$ bit init

This will create a bit.json and .bitmap files in your root folder.

Now, we add the pswnode component

$ bit add src/pswnode.js
tracking 1 new component

Bit will add the pswnode.js to its list of staged files.

To make our component backward compatible, let’s configure a Bit compiler:

$ bit import bit.envs/compilers/babel --compiler

This command will install the Bit Babel transpiler and set it as our default build step for all components in our project.

Next, we tag our component with a version:

$ bit tag --all 0.1 --message "initial message version"

Move over to Bit’s playground bit.dev and create a collection .

You have a collection name. Publish your component to the Bit registry:

$ bit export <account-name>.<collection-name>
exported 1 component to <account-name>.<collection-name>

Hooray!!:ok_woman: all other devs can see your component!!

Conclusion

You can now proudly tell Git Bash that you now know how it works.

There are many more ways to implement the password field in the terminal using Nodejs. I urge you to implement yours, I would like to see what you can come up with.

If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email or DM me

Thanks !!!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK