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