I've a scenario to create OTP token for a system
If that is the case, I dont understand why you would want to use the same set of randoms, ever. Perhaps it is for use in a 2 party system, so you need the server and client to use the same values? If so, read on.
There is already an algorithm for One Time Passwords (RFC4226). All the posts here are C# and/or sketchy (...having already created foo and bar, for step 4 do this: (some code)). I cannot find one that encodes the token/value the same way that the RFC calls for.
With some hints, helps and alterations to the references listed below, and mainly based on the RFC Appendix C, here is a VB version.
Note that the RFC describes a 2 party system. A client generates an OTP from a shared secret key (ignore the contradiction) and a "moving value" or sequence of values. The server then can validate by using the secret key and the next value. If it validates, then it moves the counter to the next in the series. Apparently, these OTPs are immediately used otherwise the server could not validate Client B until Client A's is "redeemed".
No one seems interested in that part, just the part about representing a hash in 6 digits with a very low chance of duplication. The RFC also has a provision for a check digit which no one seems interested in.
This one is also mainly interested in the core action of a 6 digit hash, so it is not RFC4226 compliant. The aspect of getting the value from a series of values could be added.
Partial Public Class CryptoTools
Private Const TicksPerSec = 10000000
Private Const TicksPerMilli = 10000
' generate your own key
Private Shared SecretKey As String = "PoNodJHz4jjVG6...UuiIvmA=="
The secret key is described later. (This comment is really to reduce indentation.)
Public Shared Function GetOneTimePass(somevalue As UInt64,
Optional size As Int32 = 6) As String
' undo how the key was made
Dim Key() As Byte = Convert.FromBase64String(SecretKey)
Dim hashData As Byte()
' encode the value to Data...called text[] in RFC
' this part seems missing in most SO answers
Dim Data(7) As Byte
For n As Int32 = Data.Length - 1 To 0 Step -1
Data(n) = CByte(CLng(somevalue) And &HFF)
somevalue = somevalue >> 8
Next
' create hasher with our key; hash
Using hasher As New HMACSHA1(Key)
hashData = hasher.ComputeHash(Data)
End Using
' THIS is the part everyone likes
' see links and https://www.rfc-editor.org/rfc/rfc4226#section-5.4
Dim offset = hashData(hashData.Length - 1) And &HF
Dim binCode = (hashData(offset) And &H7F) << 24 Or
(hashData(offset + 1) And &HFF) << 16 Or
(hashData(offset + 2) And &HFF) << 8 Or
(hashData(offset + 3) And &HFF)
Dim otp As UInt64
' RFC: 6 is minimim size, 10 is my max
Dim sz = If(size < 6, 6,
If(size > 10, 10, size))
otp = Convert.ToUInt64(binCode Mod Math.Pow(10, sz))
' optional check digit
' no one seems to implement; not included
Dim check = CalculateCheckSum(otp, size)
'Return otp.ToString().PadLeft(sz, "0"c) & String.Format("-{0}", check.ToString)
' pad to minimum length
' work in progress: only works out to 7 chars
'Console.WriteLine(ConvertToBase26(otp))
Return otp.ToString().PadLeft(sz, "0"c)
End Function
Public Shared Function GetTimeToken() As UInt64
Dim t As UInt64 = Convert.ToUInt64(DateTime.Now.Ticks / TicksPerMilli)
Return t
End Function
Public Shared Function GetRandomUInt64() As UInt64
Dim rBytes(7) As Byte
Using crng As New RNGCryptoServiceProvider()
crng.GetBytes(rBytes)
Return BitConverter.ToUInt64(rBytes, 0)
End Using
End Function
##Usage:
Dim token As UInt64 = CryptoTools.GetTimeToken()
Dim otp As String = CryptoTools.GetOneTimePass(token)
Dim rToken As UInt64 = CryptoTools.GetRandomUInt64()
Dim otp As String = CryptoTools.GetOneTimePass(rToken)
##Notes
- The recommended size of the secret key for the
HMACSHA1
hasher is 64 bytes. To create your key, use RNGCryptoServiceProvider
to create an array of 64 random bytes then Convert.ToBase64String()
.
- This requires a
UInt64
. For a "real" 2-party 4226 version, the value should be the next in a sequence of values. Otherwise...
- The value can come from anywhere: it can be the time or a random number.
GetTimeToken()
is a utility function to get the time as a UInt64
. GetRandomUInt64()
returns a crypto random UInt64
for use as the starting value.
- Most of the SO links above do not seem to encode the value/time/data quite the way the RFC calls for, if at all. One post mentions adding salt. But encoding the value passed as per RFC 4226 Appendix C seems critical to the result (which might explain the salt).
- I've only run a little over 2000 iterations, but saw no dupes ever (even when the values differed by only a millisecond).
- The RFC specifies a 6 digit minimum, which this implements, but can be longer. This allows 6-10 digits, but over 6 is hard to read, consider inserting a separator for 8 or more.
The RFC states that it is desirable that the HOTP value be 'numeric only' (Sec 4). Not that it cannot be alpha-numeric. Personally, it seems that if the result was encoded in the form of WYQ77-8WYB9
it would be easier (except maybe on phones). I can't get the result to convert to more than 7 characters.
As stated earlier, this is not RFC4226 compliant, just quite like it. Test it to death. It looks right, it seems right, it matches other versions and/or the RFC, but is presented As Is.
Added the GetRandomUInt64()
and GetTimeToken()
methods.
References: