Night Hour

Reading under a cool night sky ... 宁静沉思的夜晚 ...

Two Factor Authentication Using Deno

television

Any program is only as good as it is useful. , Linus Torvalds


11 Nov 2020


Introduction

Two factor authentication is an important security mechanism for modern applications. A mobile application (what you have) like Google Authenticator can be used to generate a time based one time password (TOTP) code from a secret key. When a user logs into an application, he or she submits a password (what you know) as well as the TOTP code. This additional factor improves the security of the application, ensuring that only authorized users can log in successfully. This article shows how to implement a simple time based one time password (TOTP) using Deno.

Design and Approach

The Time based one time password (TOTP) is described in RFC6238. In an earlier article Implementing 2 Factor Authentication for Web Security, we have shown how to implement 2fa using a simple Java Servlet/JSP application that runs on Google AppEngine.

Here, we will show how to generate TOTP code using Deno. Deno is a secure runtime for javascript and typescript. It has a strong focus on security and is a Node JS replacement. Ryan Dahl who created Node JS, is also the creator of Deno. Deno incorporates many lessons learnt from Node JS. I particularly like the strong focus on security and its better system of managing and importing external modules.

Complexity is the enemy of security, so we should try to reduce complexity while still getting the task done. For the TOTP implementation, we will only use Deno built in functions and the standard library that has been reviewed by the core Deno team. No other external third party modules will be used. Having less external dependencies can help to reduce the attack surface and the security risks of an application.

Implementation

We will write 2 simple typescript programs, generateTOTP.ts for generating the TOTP from a secret key and generateSecret.ts for creating a base32 secret key. Google Authenticator can be used to validate the TOTP codes generated from our programs. The full source code for the 2 programs are available at the Github link included at the end of the article.

Deno standard library has a hash module that implements basics like sha1 hashing. There is also an encoding module that can decode base32 string into its binary equivalent. We will use these 2 modules from the standard library in generateTOTP.ts.

TOTP requires a HMAC (Hashed based Message Authentication Code) that we have to implement ourselves. The wikipedia link on HMAC provides a useful psuedo code that can serve as the logic for our typescript implementation.

The following shows the hmacSHA1() function in generateTOTP.ts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* 

 Function to generate a SHA1 HMAC.
 Takes a byte array key and byte array input as parameters.
 Returns an array of bytes containing a SHA1 HMAC for the key and input.

 Based on pseudo code description at 
 https://en.wikipedia.org/wiki/HMAC

*/
function hmacSHA1(key:Uint8Array, input:Uint8Array)
{
    const blksize = 64; //64 bytes block size for sha1
    let pkey = key;

    /* Shortens key if it is longer than blksize */
    if (pkey.length > blksize)
    {
        const sha1 = createHash("sha1");
        sha1.update(pkey);
        pkey = new Uint8Array(sha1.digest());
      
    }
    
    if (pkey.length < blksize)
    { //pad to blocksize long with zeros on the right
        let tmpkey = new Uint8Array(blksize);
        for(let i=0; i<blksize; i++)
        {
            if(i < pkey.length)
            {
                tmpkey[i] = pkey[i];
            }
            else
            {
                tmpkey[i] = 0;
            }

        }

        pkey = tmpkey;
    }

    let outer_pkey  = new Uint8Array(blksize);
    let inner_pkey = new Uint8Array(blksize);

    for(let i = 0 ; i < blksize; i++)
    {
        outer_pkey[i] = pkey[i] ^ 0x5c;
        inner_pkey[i] = pkey[i] ^ 0x36;
    }

    let sha1 = createHash("sha1");
    sha1.update(inner_pkey);
    sha1.update(input);
    let sha1_digest = new Uint8Array(sha1.digest());
    

    sha1 = createHash("sha1");
    sha1.update(outer_pkey);
    sha1.update(sha1_digest);
    sha1_digest = new Uint8Array(sha1.digest());

    return sha1_digest;

}

TOTP requires a time input that we can get from Date.now(). This time is converted into seconds and we divide it by 30 seconds to get the timestep that will be valid for 30 seconds. 30 seconds is the default for the TOTP code generated by Google Authenticator. The timestep can be passed as input to the HMAC function.

This input has to be in binary format, 8 bytes long when passed to the HMAC function. An easy way to obtain this 8 bytes is to convert the timestep value into a hexadecimal string. Then pad it with leading "0" so that the hexadecimal string is 16 chracters long. This padded hexadecimal string can be converted into its binary equivalent. 16 hexadecimal characters work out to be 8 bytes.

The following shows the function hexToBytes() for this hexadecimal to bytes conversion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/*
 Converts a hexadcimal string to a byte array
*/
function hexToBytes(hexstr:string)
{

    if(hexstr.length % 2 != 0)
    {
        return null;
    }

    let bytes = new Uint8Array(hexstr.length/2);
    let index = 0; 
    hexstr = hexstr.toLowerCase();

    for(let i=0; i < hexstr.length; i+=2)
    {
        let hbyte = [];
        hbyte.push(hexstr.charAt(i));
        hbyte.push(hexstr.charAt(i+1));
 
        let hexvalue = 0; 

        for(let j=0; j<2;j++)
        {
            switch (hbyte[j])
            {
                case '0':
                    hexvalue |= 0x00;
                    break;
                case '1':
                    hexvalue |= 0x01;
                    break; 
                case '2':
                    hexvalue |= 0x02;
                    break; 
                case '3':
                    hexvalue |= 0x03;
                    break; 
                case '4':
                    hexvalue |= 0x04;
                    break; 
                case '5':
                    hexvalue |= 0x05;
                    break; 
                case '6':
                    hexvalue |= 0x06;
                    break; 
                case '7':
                    hexvalue |= 0x07;
                    break; 
                case '8':
                    hexvalue |= 0x08;
                    break; 
                case '9':
                    hexvalue |= 0x09;
                    break; 
                case 'a':
                    hexvalue |= 0x0a;
                    break; 
                case 'b':
                    hexvalue |= 0x0b;
                    break; 
                case 'c':
                    hexvalue |= 0x0c;
                    break; 
                case 'd':
                    hexvalue |= 0x0d;
                    break; 
                case 'e':
                    hexvalue |= 0x0e;
                    break; 
                case 'f':
                    hexvalue |= 0x0f;
                    break;
                default:
                    console.log("Invalid hex value");
                    return null;
            }

            if(j == 0)
            {
                hexvalue = hexvalue << 4; 
            }

        }

        bytes[index] = hexvalue & 0x00ff;
        index++;

    }

   return bytes;
}

The following function, generateTOTP(), creates the TOTP code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
Generates TOTP 
*/
function generateTOTP(secret:string, currenttime:number)
{
   
    if(isBase32Secret(secret) === false)
    {
        console.error("Invalid base32 TOTP secret");
        Deno.exit(1);

    }

    const bin_secret = base32.decode(secret);

    let time_input = Math.floor(Math.trunc((currenttime / 1000 )) /30);
    let time_hexvalue = time_input.toString(16);

    let num_padding = 16 - time_hexvalue.length;
    let padding = '';
    for(let i = 0 ; i<num_padding; i++)
    {
        padding += '0';
    }

    time_hexvalue = padding + time_hexvalue;
    const input_bytes = hexToBytes(time_hexvalue);

    if(input_bytes == null)
    {
        console.error("Error cannot convert time to input bytes");
        Deno.exit(1);
    }

    let hmac_arr = hmacSHA1(bin_secret,input_bytes);
    let last_hash_byte = hmac_arr[hmac_arr.length -1];

    let fa_index = last_hash_byte & 0x0f;

    let fa_code = 0;
    fa_code = fa_code | ( (hmac_arr[fa_index] & 0x7f) << 24 );
    fa_code = fa_code | ( (hmac_arr[fa_index + 1] & 0xff ) << 16 );
    fa_code = fa_code | ( (hmac_arr[fa_index + 2] & 0xff ) << 8 );
    fa_code = fa_code | ( (hmac_arr[fa_index + 3] & 0xff ));

    return fa_code % 1000000; 

}

The logic follows the description in RFC6238. In the RFC itself there is also a Java reference implementation that can help in understanding the TOTP mechanism. The secret key and timestep are inputs to the HMAC function. The 20 bytes returned by the HMAC is used to derive the 6 digit code that we are familiar with.

The last 4 bits of the 20 bytes are used as a index into the 20 bytes array. The 4 bytes starting from this index is combined into a positive integer. The mask of 0x7f (01111111) ensures that any signed bit is zeroed out. This allows a positive integer with a maximum value of 2147483647 (0x7fffffff). To get the 6 digit code, we modulus this positive integer with 1000000.

Generating the TOTP code is only one part of the picture. We also need to create the base32 secret key for each user. The secret key is 32 characters long. Each character is a base32 representation, and is therefore 5 bits. A 32 characters key will represent 32 x 5, 160 bits in total.

We need to obtain 160 bits or 20 bytes of random data. Deno has a built in Crypto interface with a getRandomValues() method. In our case here though, we choose to use the /dev/urandom device on linux. The script will open /dev/urandom as a file and read in 32 bytes of random data.

The following shows the code for generateSecret.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* 
  Creates a 32 characters base32 secret (160 bits/20bytes) for TOTP
  Ng Chiang Lin
  Nov 2020

*/

async function getRandomBytes(size:number)
{
    if(size <= 0)
    {
        console.error("Size cannot be zero or negative");
        return null;
    }

    const randevice = await Deno.open("/dev/urandom");
    const buf = new Uint8Array(size);
    const numRead = await Deno.read(randevice.rid, buf);
    Deno.close(randevice.rid);

    if(numRead != size)
    {
        console.error("Unable to get enough bytes from /dev/urandom");
        return null;
    }

    return buf;
}


/* 
Generates a 32 characters base32 secret
*/
async function genSecret()
{
    const base32string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    const random_bytes = await getRandomBytes(32); 

    if(random_bytes === null)
    {
        console.error("Error getting 32 random bytes");
        Deno.exit(1);
    }

    let index=0;
    let secret='';

    for(let i = 0; i < random_bytes.length; i++)
    {
        index = random_bytes[i] % base32string.length;
        secret += base32string.charAt(index);
    }

    return secret; 
}


function formatSecret(secret:string)
{
    let formatted = '';
    for(let i = 0; i < secret.length; i++)
    {
        formatted += secret.charAt(i); 

        if( (i + 1) % 4 === 0)
        {
            formatted += ' ';
        }

    }

    return formatted.toLowerCase(); 

}

let secret = await genSecret();

console.log(secret);
console.log(formatSecret(secret)); 

These 32 random bytes will be used to create 32 random base32 characters, joined together as a string. Each of the base32 character represents 5 bits. When this secret string is converted into binary, it will be 160 bits or 20 bytes of randomness.

generateSecret.ts will output the base32 secret as a single string without any spaces as well as a more human readable format. For the human readable format, there is a space every 4 base32 characters and all the characters are in lower case.

Testing the Deno TOTP Generator

We can test the 2 small deno programs on a linux system. I am using a ubuntu desktop. It is assumed that Deno is already installed and setup in the system path. First we can generate a new secret key. This key must be kept private.

Runs the following the command

deno run --allow-read=/dev/urandom generateSecret.ts

The screenshot shows the result. The second output line is the human readable base32 secret. We can manually enter this into the Google Authenticator mobile application.

Generate 32 characters (base32) secret key
Fig 1. Generate 32 characters (base32) secret key

A more convenient way is to format a URI protocol string and creating a QR code that can be scanned by Google Authenticator. The first line of the output is the same secret key which can be used in a URI protocol string.

We format a URI string starting with otpauth:// like the following.

otpauth://totp/deno2fa?secret=7H3SRIKCTLD3HC3AKX4SH34V7GTNMUNY&issuer=Nighthour

The otpauth format is described at https://github.com/google/google-authenticator/wiki/Key-Uri-Format.

For the QR Code generation, we can use the javascript library provided by davidshimjs. I have a handy html webpage that is already configured with davidshimjs QR code generator.

Generate OTPAUTH QR Code
Fig 2. Generate OTPAUTH QR Code

Take note that the QR code contains the secret key and should be kept private as well. On your mobile device, start up Google Authenticator and scan in the QR code. Newer versions of Google Authenticator prevents the taking of screenshots, so we will not show the screenshot here. If everything is setup properly, there will be a new deno2fa entry in Google Authenticator displaying the 6 digit TOTP code.

Let's run our Deno TOTP code generator program, generateTOTP.ts. We need to pass the human readable secret as a command line argument.

deno run generateTOTP.ts "7h3s rikc tld3 hc3a kx4s h34v 7gtn muny"

The following is a screenshot of the output.

Generate TOTP 6 digit code using Deno
Fig 3. Generate TOTP 6 digit code using Deno

Compare the 6 digit code with the one generated from Google Authenticator. They should match. Both the computer time and the time on the mobile device needs to be accurate. This TOTP generation code can be incorporated into a Deno web application for 2 factor authentication.

Conclusion and Afterthought

Modern applications should not solely rely on password based authentication. To enhance security, a multi factor authentication approach can be adopted. This article shows how we can implement our own 2 Factor Authentication code using Deno. The 2 FA is based on both what we know (password) and what we have (a mobile authenication device).

This can mitigate the security risk of account takeover due to password being leaked or a weak password being brute forced. 2 Factor authentication is a standard security mechanism adopted by many modern applications.

Useful References

The full source code is available at the following Github link.
https://github.com/ngchianglin/deno2fa

If you have any feedback, comments, corrections or suggestions to improve this article. You can reach me via the contact/feedback link at the bottom of the page.