func TestTOTP(t *testing.T) { keySha1, err := hex.DecodeString(sha1KeyHex) checkError(t, err) keySha256, err := hex.DecodeString(sha256KeyHex) checkError(t, err) keySha512, err := hex.DecodeString(sha512KeyHex) checkError(t, err) // create the OTP otp := new(Totp) otp.digits = 8 otp.issuer = "Sec51" otp.account = "*****@*****.**" // Test SHA1 otp.key = keySha1 for index, ts := range timeCounters { counter := increment(ts, 30) otp.counter = bigendian.ToUint64(counter) hash := hmac.New(sha1.New, otp.key) token := calculateToken(otp.counter[:], otp.digits, hash) expected := sha1TestData[index] if token != expected { t.Errorf("SHA1 test data, token mismatch. Got %s, expected %s\n", token, expected) } } // Test SHA256 otp.key = keySha256 for index, ts := range timeCounters { counter := increment(ts, 30) otp.counter = bigendian.ToUint64(counter) hash := hmac.New(sha256.New, otp.key) token := calculateToken(otp.counter[:], otp.digits, hash) expected := sha256TestData[index] if token != expected { t.Errorf("SHA256 test data, token mismatch. Got %s, expected %s\n", token, expected) } } // Test SHA512 otp.key = keySha512 for index, ts := range timeCounters { counter := increment(ts, 30) otp.counter = bigendian.ToUint64(counter) hash := hmac.New(sha512.New, otp.key) token := calculateToken(otp.counter[:], otp.digits, hash) expected := sha512TestData[index] if token != expected { t.Errorf("SHA512 test data, token mismatch. Got %s, expected %s\n", token, expected) } } }
// ToBytes serialises a TOTP object in a byte array // Sizes: 4 4 N 8 4 4 N 4 N 4 4 4 8 4 // Format: |total_bytes|key_size|key|counter|digits|issuer_size|issuer|account_size|account|steps|offset|total_failures|verification_time|hashFunction_type| // hashFunction_type: 0 = SHA1; 1 = SHA256; 2 = SHA512 // The data is encrypted using the cryptoengine library (which is a wrapper around the golang NaCl library) // TODO: // 1- improve sizes. For instance the hashFunction_type could be a short. func (otp *Totp) ToBytes() ([]byte, error) { // check Totp initialization if err := totpHasBeenInitialized(otp); err != nil { return nil, err } var buffer bytes.Buffer // caluclate the length of the key and create its byte representation keySize := len(otp.key) keySizeBytes := bigendian.ToInt(keySize) //bigEndianInt(keySize) // caluclate the length of the issuer and create its byte representation issuerSize := len(otp.issuer) issuerSizeBytes := bigendian.ToInt(issuerSize) // caluclate the length of the account and create its byte representation accountSize := len(otp.account) accountSizeBytes := bigendian.ToInt(accountSize) totalSize := 4 + 4 + keySize + 8 + 4 + 4 + issuerSize + 4 + accountSize + 4 + 4 + 4 + 8 + 4 totalSizeBytes := bigendian.ToInt(totalSize) // at this point we are ready to write the data to the byte buffer // total size if _, err := buffer.Write(totalSizeBytes[:]); err != nil { return nil, err } // key if _, err := buffer.Write(keySizeBytes[:]); err != nil { return nil, err } if _, err := buffer.Write(otp.key); err != nil { return nil, err } // counter counterBytes := bigendian.ToUint64(otp.getIntCounter()) if _, err := buffer.Write(counterBytes[:]); err != nil { return nil, err } // digits digitBytes := bigendian.ToInt(otp.digits) if _, err := buffer.Write(digitBytes[:]); err != nil { return nil, err } // issuer if _, err := buffer.Write(issuerSizeBytes[:]); err != nil { return nil, err } if _, err := buffer.WriteString(otp.issuer); err != nil { return nil, err } // account if _, err := buffer.Write(accountSizeBytes[:]); err != nil { return nil, err } if _, err := buffer.WriteString(otp.account); err != nil { return nil, err } // steps stepsBytes := bigendian.ToInt(otp.stepSize) if _, err := buffer.Write(stepsBytes[:]); err != nil { return nil, err } // offset offsetBytes := bigendian.ToInt(otp.clientOffset) if _, err := buffer.Write(offsetBytes[:]); err != nil { return nil, err } // total_failures totalFailuresBytes := bigendian.ToInt(otp.totalVerificationFailures) if _, err := buffer.Write(totalFailuresBytes[:]); err != nil { return nil, err } // last verification time verificationTimeBytes := bigendian.ToUint64(uint64(otp.lastVerificationTime.Unix())) if _, err := buffer.Write(verificationTimeBytes[:]); err != nil { return nil, err } // has_function_type switch otp.hashFunction { case crypto.SHA256: sha256Bytes := bigendian.ToInt(1) if _, err := buffer.Write(sha256Bytes[:]); err != nil { return nil, err } break case crypto.SHA512: sha512Bytes := bigendian.ToInt(2) if _, err := buffer.Write(sha512Bytes[:]); err != nil { return nil, err } break default: sha1Bytes := bigendian.ToInt(0) if _, err := buffer.Write(sha1Bytes[:]); err != nil { return nil, err } } // encrypt the TOTP bytes engine, err := cryptoengine.InitCryptoEngine(otp.issuer) if err != nil { return nil, err } // init the message to be encrypted message, err := cryptoengine.NewMessage(buffer.String(), message_type) if err != nil { return nil, err } // encrypt it encryptedMessage, err := engine.NewEncryptedMessage(message) if err != nil { return nil, err } return encryptedMessage.ToBytes() }
// Basically, we define TOTP as TOTP = HOTP(K, T), where T is an integer // and represents the number of time steps between the initial counter // time T0 and the current Unix time. // T = (Current Unix time - T0) / X, where the // default floor function is used in the computation. // For example, with T0 = 0 and Time Step X = 30, T = 1 if the current // Unix time is 59 seconds, and T = 2 if the current Unix time is // 60 seconds. func (otp *Totp) incrementCounter(index int) { // Unix returns t as a Unix time, the number of seconds elapsed since January 1, 1970 UTC. counterOffset := time.Duration(index*otp.stepSize) * time.Second now := time.Now().UTC().Add(counterOffset).Unix() otp.counter = bigendian.ToUint64(increment(now, otp.stepSize)) }