woensdag 15 juli 2015

HMAC One Time Password in C#

Quite recently I started developing apps for Windows Phone, a completely new experience for me, and a complete new mindset.

For the app I was creating, I needed to implement a way to create One-Time-Passwords (OTP's) using an HMAC algorithm. Although Nuget (and Google) provides quite some libraries and inspiration, I was not able to find a solution that actually fit my needs.

The idea of an HOTP algorithm is defined in this RFC: HOTP: An HMAC-Based One-Time Password Algorithm, so I won't tread into detail on the how-to's and why's of the HOTP.

After some browsing, blood, sweat and hacking, I came up with this solution:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Security.Cryptography;
using Windows.Security.Cryptography.Core;
using Windows.Storage.Streams;



namespace Acuzio.Util
{
    /// <summary>
    /// This enumeration contains the algorithms which are allowed to be used for rendering the One-time-password.
    /// </summary>
    public enum HashAlgorithm
    {
        SHA1,
        SHA256,
        SHA384,
        SHA512,
        MD5
    }


    public class OTP
    {
        private byte[] secret;
        private HashAlgorithm digest = HashAlgorithm.SHA512;
        private int digits = 8;
        //                                                 0  1   2    3     4      5       6        7         8 
        private readonly int[] DIGITS_POWERS = new int[] { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };

        /// <summary>
        /// Create a new instance of the OTP class
        /// </summary>
        /// <param name="secret">The secret </param>
        public OTP(byte[] secret)
        {
            this.secret = secret;
        }

        /// <summary>
        /// Create a new instance of the OTP class
        /// </summary>
        /// <param name="secret">The secret </param>
        /// <param name="digits">The number of digits in the token </param>
        public OTP(byte[] secret, int digits)
        {
            this.secret = secret;
            this.digits = digits;
        }

        /// <summary>
        /// Create a new instance of the OTP class
        /// </summary>
        /// <param name="secret">The secret</param>
        /// <param name="digits">The number of digits in the token</param>
        /// <param name="digest">The algorithm to be used</param>
        public OTP(byte[] secret, int digits, HashAlgorithm digest)
        {
            this.secret = secret;
            this.digits = digits;
            this.digest = digest;
        }

        /// <summary>
        /// Generate a One-time-password for the provided challenge or movingfactor.
        /// </summary>
        /// <param name="input">The challenge which needs to be used as input</param>
        /// <returns>a new one time password</returns>
        public string generateOTP(UInt64 input)
        {
            if (this.digits <= 0 || this.digits > DIGITS_POWERS.Length - 1)
            {
                throw new ArgumentOutOfRangeException("digits", "The number of digits should be between 1 and 8");
            }

            String algorithm = null;

            switch (digest)
            {
                case HashAlgorithm.SHA1:
                    algorithm = MacAlgorithmNames.HmacSha1; break;
                case HashAlgorithm.SHA256:
                    algorithm = MacAlgorithmNames.HmacSha256; break;
                case HashAlgorithm.SHA384:
                    algorithm = MacAlgorithmNames.HmacSha384; break;
                case HashAlgorithm.SHA512:
                    algorithm = MacAlgorithmNames.HmacSha512; break;
                case HashAlgorithm.MD5:
                    algorithm = MacAlgorithmNames.HmacMd5; break;
                default:
                    throw new Exception("The provided algorithm is not supported");
            }

            MacAlgorithmProvider provider = MacAlgorithmProvider.OpenAlgorithm(algorithm);

            byte[] data = BitConverter.GetBytes(input);
            byte[] hmac = new byte[] { };

            if (BitConverter.IsLittleEndian)
                Array.Reverse(data);

            //create a hash for the known secret and append the challenge
            CryptographicHash hmacHasher = provider.CreateHash(CryptographicBuffer.CreateFromByteArray(this.secret));
            hmacHasher.Append(CryptographicBuffer.CreateFromByteArray(data));

            //create a hash and reset the hasher
            IBuffer buffer = hmacHasher.GetValueAndReset();
            CryptographicBuffer.CopyToByteArray(buffer, out hmac);

            int offset = hmac[hmac.Length - 1] & 0xf;

            int binary = ((hmac[offset + 0] & 0x7F) << 24) |
                        ((hmac[offset + 1] & 0xFF) << 16) |
                        ((hmac[offset + 2] & 0xFF) << 8) |
                        ((hmac[offset + 3] & 0xFF));

            int password = binary % DIGITS_POWERS[this.digits];

            //return the OTP with zero-padding for the number of desired digits
            return password.ToString(new string('0', this.digits));
        }
    }
}

Below an example on how to use the OTP class (e.g. unit tests).

using System;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Acuzio.Util;
using System.Text;

namespace TestProject
{
    [TestClass]
    public class OTPTest
    {
        //...

        [TestMethod]
        public void TestingSha512()
        {
            string secret = "SomeKey";
            byte[] key = Encoding.UTF8.GetBytes(secret);
            ulong interval = 66778;
            int returnDigits = 6;

            OTP generator = new OTP(key, returnDigits, HashAlgorithm.SHA512);
            String otp = generator.generateOTP(interval);

            Assert.IsNotNull(otp);
            Assert.AreEqual(6, otp.Length);
            Assert.AreEqual("539281", otp);
        }

        [TestMethod]
        public void TestingSha1()
        {
            string secret = "12345678901234567890";
            byte[] key = Encoding.UTF8.GetBytes(secret);
            ulong[] interval = new ulong[] { 0, 1, 2 };
            string[] expected = new string[] { "755224", "287082", "359152" };

            int returnDigits = 6;


            for (int i = 0; i < interval.Length; i++)
            {
                ulong nextInterval = interval[i];

                OTP generator = new OTP(key, returnDigits, HashAlgorithm.SHA1);
                String otp = generator.generateOTP(nextInterval);

                Assert.IsNotNull(otp);
                Assert.AreEqual(6, otp.Length);
                Assert.AreEqual(expected[i], otp);
            }
        }

        //...
    }
}

Some side notes:

  • Exception handling might be improved, but you can tailor it to your own needs;
  • I opted for defined class constructors in the OTP class, as we need to have a valid state of the object, before we can use it.
  • I used an array of powers, instead of using the pow-function, purely for speed.

Geen opmerkingen:

Een reactie posten