/// <summary> /// This generates a 6, 7 or 8 digit code from the secret key and seed counter. /// </summary> /// <param name="key">Secret key</param> /// <param name="counter">8byte counter to seed into the key</param> ///<param name="length">length of key, most often 6 digits is used.</param> ///<returns>Code digits</returns> /// <exception cref="ArgumentNullException">If key is null</exception> /// <remarks><![CDATA[ /// Background info (RFC4226): /// The following code example describes the extraction of a dynamic /// binary code given that hmac_result is a byte array with the HMAC- /// SHA-1 result: /// /// int offset = hmac_result[19] & 0xf ; /// int bin_code = (hmac_result[offset] & 0x7f) << 24 /// | (hmac_result[offset+1] & 0xff) << 16 /// | (hmac_result[offset+2] & 0xff) << 8 /// | (hmac_result[offset+3] & 0xff) ; /// /// SHA-1 HMAC Bytes (Example) /// /// ------------------------------------------------------------- /// | Byte Number | /// ------------------------------------------------------------- /// |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19| /// ------------------------------------------------------------- /// | Byte Value | /// ------------------------------------------------------------- /// |1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a| /// -------------------------------***********----------------++| /// /// * The last byte (byte 19) has the hex value 0x5a. /// * The value of the lower 4 bits is 0xa (the offset value). /// * The offset value is byte 10 (0xa). /// * The value of the 4 bytes starting at byte 10 is 0x50ef7f19, /// which is the dynamic binary code DBC1. /// * The MSB of DBC1 is 0x50 so DBC2 = DBC1 = 0x50ef7f19 . /// * HOTP = DBC2 modulo 10^6 = 872921. /// /// We treat the dynamic binary code as a 31-bit, unsigned, big-endian /// integer; the first byte is masked with a 0x7f. /// /// We then take this number modulo 1,000,000 (10^6) to generate the 6- /// digit HOTP value 872921 decimal. ///]]></remarks> public static string Generate(byte[] key, long counter, CodeLength length) { byte[] hmacResult; byte[] byteCounter = BitConverter.GetBytes(counter); if (key == null) throw new ArgumentNullException("key", "Key may not be null"); //If we are on a system that uses Little Endian, like intel ;-) //RFC4226 states we need Big Endian. if (BitConverter.IsLittleEndian) byteCounter = byteCounter.Reverse().ToArray(); //do hash using (var hmac = new HMACSHA1(key)) { hmacResult = hmac.ComputeHash(byteCounter); } var offset = hmacResult[19] & 0xf; //first byte is masked with a 0x7f as per RFC var binCode = (hmacResult[offset] & 0x7f) << 24 | (hmacResult[offset + 1] & 0xff) << 16 | (hmacResult[offset + 2] & 0xff) << 8 | (hmacResult[offset + 3] & 0xff); //modulo our binCode with the code length (6, 7 or 8) //make sure to pad left with 0's. int i = (int) length; var code = (binCode%Math.Pow(10, i)).ToString(CultureInfo.InvariantCulture).PadLeft(i, '0'); return code; }
/// <summary> /// Generates a onetime password code /// </summary> /// <param name="key">shared secret</param> /// <param name="offset">interval offset, default is 0</param> /// <param name="length">length of code, six, seven or eight. Default is six</param> /// <param name="timeStep">timestep eg. secs code should work, default is 30</param> /// <returns>code generated</returns> public static string Generate(byte[] key, int offset = 0, CodeLength length = CodeLength.Six, int timeStep=30) { return Generate(key, GetCurrentTimeInterval(timeStep) + offset, length); }