Skip to content

Instantly share code, notes, and snippets.

@yusufsyaifudin
Created February 18, 2021 07:05
Show Gist options
  • Save yusufsyaifudin/60c0ed2638ded59ed9b27bfa77290d2d to your computer and use it in GitHub Desktop.
Save yusufsyaifudin/60c0ed2638ded59ed9b27bfa77290d2d to your computer and use it in GitHub Desktop.

This is my learning towards how to generate Time Based OTP using SHA-512 algorithm.

package generator
import (
"crypto/hmac"
"crypto/sha512"
"fmt"
"math"
"time"
)
// TimeCounter set current time with step counter.
func TimeCounter(t time.Time, timeStep int64) []byte {
// Divide based on step counter.
// For example, if we set 30 seconds, then counter will different every 30 seconds
counter := t.Unix() / timeStep
// convert counter (time based) into byte array
// put movingFactor value into text byte array
var movingFactor = make([]byte, 8) // byte[] text = new byte[8];
for i := len(movingFactor) - 1; i >= 0; i-- {
movingFactor[i] = (byte)(counter & 0xff)
// The >> operator is the right shift operator.
// >>= is a contracted form of the right shift operator and assignment:
// https://stackoverflow.com/a/32933369/5489910
counter = counter >> 8
}
return movingFactor
}
// TOtp generate HMAC OTP using SHA512 based on
// https://tools.ietf.org/html/rfc6238 and https://tools.ietf.org/html/rfc4226#section-5.3
func TOtp(counter []byte, secret string) (string, error) {
// Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C)
// secret_buffer_size for SHA 512 is 64 bytes
hash := hmac.New(sha512.New, []byte(secret))
hmacHash := hash.Sum(counter)
// Step 2: Generate a 4-byte string (Dynamic Truncation)
// 0xf is 15 in decimal
// & is a bitwise AND operator command in integer type https://golang.org/ref/spec#Arithmetic_operators
offset := int(hmacHash[len(hmacHash)-1] & 0xf)
// Step 3: Compute an HOTP value
// Let Snum = StToNum(Sbits)
// Convert S to a number in 0...2^{31}-1
// Return D = Snum mod 10^Digit
// D is a number in the range 0...10^{Digit}-1
code := (int(hmacHash[offset]&0x7f) << 24) |
(int(hmacHash[offset+1]&0xff) << 16) |
(int(hmacHash[offset+2]&0xff) << 8) |
int(hmacHash[offset+3]&0xff)
// example in RFC: int otp = binary % DIGITS_POWER[codeDigits];
const digit = 4 // generate fix 4 digit length of OTP
code = code % int(math.Pow10(digit))
// %04d will return 0001 if value = 1, 0023 if value = 23
digitFormat := "%0" + fmt.Sprint(digit) + "d"
result := fmt.Sprintf(digitFormat, code)
return result, nil
}
package generator
import (
"testing"
"time"
)
func TestOTP(t *testing.T) {
var generatedOTP = make(map[string]time.Time)
currentTime := time.Now()
const timeStep = int64(30)
const secret = "abc"
for i := 0; i < 120; i++ {
currentTime = currentTime.Add(time.Duration(timeStep) * time.Second)
counter := TimeCounter(currentTime, timeStep) // every 30 second
otp, err := TOtp(counter, secret)
if err != nil {
t.Fatal(err)
}
if previousTime, exist := generatedOTP[otp]; exist {
t.Fatalf(
"otp %s already generated with different time but return same OTP %s (old) vs %s (new)",
otp, previousTime, currentTime,
)
}
generatedOTP[otp] = currentTime
}
for otp, generatedTime := range generatedOTP {
counter := TimeCounter(generatedTime, timeStep) // every 30 second
newOTP, err := TOtp(counter, secret)
if err != nil {
t.Fatal(err)
}
if newOTP != otp {
t.Fatalf("otp %s and %s should be the same because they generated in the same time %s",
newOTP, otp, generatedTime)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment