Version 1.0

This commit is contained in:
silenteh 2015-08-04 14:15:51 +02:00
commit 00e19bbd7f
10 changed files with 4121 additions and 0 deletions

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: go
go:
- 1.4
script:
- go test -v ./...

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright (c) 2015 Sec51.com <info@sec51.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

129
README.md Normal file
View File

@ -0,0 +1,129 @@
#### Current test status
[![Build Status](https://travis-ci.io/github.com/sec51/twofactor/status.png)](https://travis-ci.io/github.com/sec51/twofactor/status.png)
## `totp`
This package implements the RFC 6238 OATH-TOTP algorithm;
See also the [godocs](https://godoc.org/github.com/sec51/twofactor/)
for this package.
### Installation
```go get github.com/sec51/twofactor```
### Features
* Built-in support for secure crypto keys generation
* Built-in back-off time when a user fails to authenticate more than 3 times
* Bult-in serialization and deserialization to store the one time token struct in a persistence layer
* Automatic re-synchronization with the client device
* Built-in generation of a PNG QR Code for adding easily the secret key on the user device
* Supports 6, 7, 8 digits tokens
* Supports HMAC-SHA1, HMAC-SHA256, HMAC-SHA512
### Storing Keys
> **The key crerated is using go crypto random function and it's a cryptographic secret key.**
> It needs to be protected against unauthorized access and they cannot be leaked.
> In addition when shared with the client, the connection should be secured.
The `totp` struct can be easily serialized using the `ToBytes()` function.
The bytes can then be stored on a persistent layer. Again the secret key needs to be protected.
You can then retrieve the object back with the function: `TOTPFromBytes`
Again if you trannsfer those bytes via a network connection, this should be a secured one.
The struct needs to be stored in a persistent layer becase its values, like last token verification time,
max user authentication failures, etc.. needs to be preserved.
The secret key needs to be preserved too, between the user accound and the user device.
The secret key is used to derive tokens.
Once more the secret key needs to be safely stored.
### Upcoming features
* Securely store the secret keys in the persistent layer and allow secure transfer on the network
* Integration with Twilio for sending the token via SMS, in case the user loses its entry in the Google authenticator app.
### Example Usages
#### Case 1: Google Authenticator
* How to use the library
1- Import the library
```
import github.com/sec51/twofactor
```
2- Instanciate the `totp` object via:
```
otp, err := twofactor.NewTOTP("info@sec51.com", "Sec51", crypto.SHA1, 8)
if err != nil {
return err
}
```
3- Display the PNG QR code to the user and an input text field, so that he can insert the token generated from his device
```
qrBytes, err := otp.QR()
if err != nil {
return err
}
```
4- Verify the user provided token, coming from the google authenticator app
```
err := otp.Validate(USER_PROVIDED_TOKEN)
if err != nil {
return err
}
// if there is an error, then the authentication failed
// if it succeeded, then store this information and do not display the QR code ever again.
```
5- All following authentications should display only a input field with no QR code.
### References
* [RFC 6238 - *TOTP: Time-Based One-Time Password Algorithm*](https://tools.ietf.org/rfc/rfc6238.txt)
* The [Key URI Format](https://code.google.com/p/google-authenticator/wiki/KeyUriFormat)
### Author
`totp` was written by Sec51 <info@sec51.com>.
### License
```
Copyright (c) 2015 Sec51.com <info@sec51.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```

51
conversions.go Normal file
View File

@ -0,0 +1,51 @@
package twofactor
import (
"math"
)
// Helper function which rounds the float to the nearest integet
func round(n float64) uint64 {
if n < 0 {
return uint64(math.Ceil(n - 0.5))
}
return uint64(math.Floor(n + 0.5))
}
// helper function which converts a uint64 to a []byte in Big Endian
func bigEndianUint64(n uint64) [8]byte {
data := [8]byte{}
data[0] = byte((n >> 56) & 0xFF)
data[1] = byte((n >> 48) & 0xFF)
data[2] = byte((n >> 40) & 0xFF)
data[3] = byte((n >> 32) & 0xFF)
data[4] = byte((n >> 24) & 0xFF)
data[5] = byte((n >> 16) & 0xFF)
data[6] = byte((n >> 8) & 0xFF)
data[7] = byte(n & 0xFF)
return data
}
// helper function which converts a big endian []byte to a uint64
func uint64FromBigEndian(data [8]byte) uint64 {
i := (uint64(data[7]) << 0) | (uint64(data[6]) << 8) |
(uint64(data[5]) << 16) | (uint64(data[4]) << 24) |
(uint64(data[3]) << 32) | (uint64(data[2]) << 40) |
(uint64(data[1]) << 48) | (uint64(data[0]) << 56)
return uint64(i)
}
func bigEndianInt(n int) [4]byte {
data := [4]byte{}
data[0] = byte((n >> 24) & 0xFF)
data[1] = byte((n >> 16) & 0xFF)
data[2] = byte((n >> 8) & 0xFF)
data[3] = byte(n & 0xFF)
return data
}
func intFromBigEndian(data [4]byte) int {
i := (int(data[3]) << 0) | (int(data[2]) << 8) |
(int(data[1]) << 16) | (int(data[0]) << 24)
return int(i)
}

101
conversions_test.go Normal file
View File

@ -0,0 +1,101 @@
package twofactor
import (
"encoding/binary"
"testing"
)
func TestRound(t *testing.T) {
// TODO: test negative numbers, although not used in our case
input := float64(3.7)
expected := uint64(4)
result := round(input)
if result != expected {
t.Fatalf("Expected %d - got %d\n", expected, result)
}
input = float64(3.5)
expected = uint64(4)
result = round(input)
if result != expected {
t.Fatalf("Expected %d - got %d\n", expected, result)
}
input = float64(3.499999999)
expected = uint64(3)
result = round(input)
if result != expected {
t.Fatalf("Expected %d - got %d\n", expected, result)
}
input = float64(3.0)
expected = uint64(3)
result = round(input)
if result != expected {
t.Fatalf("Expected %d - got %d\n", expected, result)
}
input = float64(3.9999)
expected = uint64(4)
result = round(input)
if result != expected {
t.Fatalf("Expected %d - got %d\n", expected, result)
}
}
func TestBigEndianUint64(t *testing.T) {
// convert ot bytes
input := uint64(2984983220)
inputBytes := bigEndianUint64(input)
// convert from bytes back
result := uint64FromBigEndian(inputBytes)
if result != input {
t.Fatal("Big endian conversion failed")
}
goResult := binary.BigEndian.Uint64(inputBytes[:])
if goResult != input {
t.Fatal("It's not a big endian representation")
}
input = uint64(18446744073709551615)
inputBytes = bigEndianUint64(input)
// convert from bytes back
result = uint64FromBigEndian(inputBytes)
if result != input {
t.Fatal("Big endian conversion failed")
}
goResult = binary.BigEndian.Uint64(inputBytes[:])
if goResult != input {
t.Fatal("It's not a big endian representation")
}
}
func TestBigEndianInt(t *testing.T) {
// convert ot bytes
input := int(2984983220)
inputBytes := bigEndianInt(input)
// convert from bytes back
result := intFromBigEndian(inputBytes)
if result != input {
t.Fatal("Big endian conversion failed")
}
goResult := binary.BigEndian.Uint32(inputBytes[:])
if int(goResult) != input {
t.Fatal("It's not a big endian representation")
}
}

25
doc.go Normal file
View File

@ -0,0 +1,25 @@
/*
The package twofactor implements the RFC 6238 TOTP: Time-Based One-Time Password Algorithm
The library provides a simple and secure way to generate and verify the OTP tokens
and provides the possibility to display QR codes out of the box
The library supports HMAC-SHA1, HMAC-SHA256, HMAC-SHA512
*/
package twofactor
/*
Copyright (c) 2015 Sec51 <info@sec51.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

2075
rfc4226.txt Normal file

File diff suppressed because it is too large Load Diff

899
rfc6238.txt Normal file
View File

@ -0,0 +1,899 @@
Internet Engineering Task Force (IETF) D. M'Raihi
Request for Comments: 6238 Verisign, Inc.
Category: Informational S. Machani
ISSN: 2070-1721 Diversinet Corp.
M. Pei
Symantec
J. Rydell
Portwise, Inc.
May 2011
TOTP: Time-Based One-Time Password Algorithm
Abstract
This document describes an extension of the One-Time Password (OTP)
algorithm, namely the HMAC-based One-Time Password (HOTP) algorithm,
as defined in RFC 4226, to support the time-based moving factor. The
HOTP algorithm specifies an event-based OTP algorithm, where the
moving factor is an event counter. The present work bases the moving
factor on a time value. A time-based variant of the OTP algorithm
provides short-lived OTP values, which are desirable for enhanced
security.
The proposed algorithm can be used across a wide range of network
applications, from remote Virtual Private Network (VPN) access and
Wi-Fi network logon to transaction-oriented Web applications. The
authors believe that a common and shared algorithm will facilitate
adoption of two-factor authentication on the Internet by enabling
interoperability across commercial and open-source implementations.
Status of This Memo
This document is not an Internet Standards Track specification; it is
published for informational purposes.
This document is a product of the Internet Engineering Task Force
(IETF). It represents the consensus of the IETF community. It has
received public review and has been approved for publication by the
Internet Engineering Steering Group (IESG). Not all documents
approved by the IESG are a candidate for any level of Internet
Standard; see Section 2 of RFC 5741.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
http://www.rfc-editor.org/info/rfc6238.
M'Raihi, et al. Informational [Page 1]
RFC 6238 HOTPTimeBased May 2011
Copyright Notice
Copyright (c) 2011 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document. Code Components extracted from this document must
include Simplified BSD License text as described in Section 4.e of
the Trust Legal Provisions and are provided without warranty as
described in the Simplified BSD License.
Table of Contents
1. Introduction ....................................................2
1.1. Scope ......................................................2
1.2. Background .................................................3
2. Notation and Terminology ........................................3
3. Algorithm Requirements ..........................................3
4. TOTP Algorithm ..................................................4
4.1. Notations ..................................................4
4.2. Description ................................................4
5. Security Considerations .........................................5
5.1. General ....................................................5
5.2. Validation and Time-Step Size ..............................6
6. Resynchronization ...............................................7
7. Acknowledgements ................................................7
8. References ......................................................8
8.1. Normative References .......................................8
8.2. Informative References .....................................8
Appendix A. TOTP Algorithm: Reference Implementation ...............9
Appendix B. Test Vectors ..........................................14
1. Introduction
1.1. Scope
This document describes an extension of the One-Time Password (OTP)
algorithm, namely the HMAC-based One-Time Password (HOTP) algorithm,
as defined in [RFC4226], to support the time-based moving factor.
M'Raihi, et al. Informational [Page 2]
RFC 6238 HOTPTimeBased May 2011
1.2. Background
As defined in [RFC4226], the HOTP algorithm is based on the
HMAC-SHA-1 algorithm (as specified in [RFC2104]) and applied to an
increasing counter value representing the message in the HMAC
computation.
Basically, the output of the HMAC-SHA-1 calculation is truncated to
obtain user-friendly values:
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
where Truncate represents the function that can convert an HMAC-SHA-1
value into an HOTP value. K and C represent the shared secret and
counter value; see [RFC4226] for detailed definitions.
TOTP is the time-based variant of this algorithm, where a value T,
derived from a time reference and a time step, replaces the counter C
in the HOTP computation.
TOTP implementations MAY use HMAC-SHA-256 or HMAC-SHA-512 functions,
based on SHA-256 or SHA-512 [SHA2] hash functions, instead of the
HMAC-SHA-1 function that has been specified for the HOTP computation
in [RFC4226].
2. Notation and Terminology
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
document are to be interpreted as described in [RFC2119].
3. Algorithm Requirements
This section summarizes the requirements taken into account for
designing the TOTP algorithm.
R1: The prover (e.g., token, soft token) and verifier (authentication
or validation server) MUST know or be able to derive the current
Unix time (i.e., the number of seconds elapsed since midnight UTC
of January 1, 1970) for OTP generation. See [UT] for a more
detailed definition of the commonly known "Unix time". The
precision of the time used by the prover affects how often the
clock synchronization should be done; see Section 6.
R2: The prover and verifier MUST either share the same secret or the
knowledge of a secret transformation to generate a shared secret.
R3: The algorithm MUST use HOTP [RFC4226] as a key building block.
M'Raihi, et al. Informational [Page 3]
RFC 6238 HOTPTimeBased May 2011
R4: The prover and verifier MUST use the same time-step value X.
R5: There MUST be a unique secret (key) for each prover.
R6: The keys SHOULD be randomly generated or derived using key
derivation algorithms.
R7: The keys MAY be stored in a tamper-resistant device and SHOULD be
protected against unauthorized access and usage.
4. TOTP Algorithm
This variant of the HOTP algorithm specifies the calculation of a
one-time password value, based on a representation of the counter as
a time factor.
4.1. Notations
o X represents the time step in seconds (default value X =
30 seconds) and is a system parameter.
o T0 is the Unix time to start counting time steps (default value is
0, i.e., the Unix epoch) and is also a system parameter.
4.2. Description
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.
More specifically, 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.
The implementation of this algorithm MUST support a time value T
larger than a 32-bit integer when it is beyond the year 2038. The
value of the system parameters X and T0 are pre-established during
the provisioning process and communicated between a prover and
verifier as part of the provisioning step. The provisioning flow is
out of scope of this document; refer to [RFC6030] for such
provisioning container specifications.
M'Raihi, et al. Informational [Page 4]
RFC 6238 HOTPTimeBased May 2011
5. Security Considerations
5.1. General
The security and strength of this algorithm depend on the properties
of the underlying building block HOTP, which is a construction based
on HMAC [RFC2104] using SHA-1 as the hash function.
The conclusion of the security analysis detailed in [RFC4226] is
that, for all practical purposes, the outputs of the dynamic
truncation on distinct inputs are uniformly and independently
distributed strings.
The analysis demonstrates that the best possible attack against the
HOTP function is the brute force attack.
As indicated in the algorithm requirement section, keys SHOULD be
chosen at random or using a cryptographically strong pseudorandom
generator properly seeded with a random value.
Keys SHOULD be of the length of the HMAC output to facilitate
interoperability.
We RECOMMEND following the recommendations in [RFC4086] for all
pseudorandom and random number generations. The pseudorandom numbers
used for generating the keys SHOULD successfully pass the randomness
test specified in [CN], or a similar well-recognized test.
All the communications SHOULD take place over a secure channel, e.g.,
Secure Socket Layer/Transport Layer Security (SSL/TLS) [RFC5246] or
IPsec connections [RFC4301].
We also RECOMMEND storing the keys securely in the validation system,
and, more specifically, encrypting them using tamper-resistant
hardware encryption and exposing them only when required: for
example, the key is decrypted when needed to verify an OTP value, and
re-encrypted immediately to limit exposure in the RAM to a short
period of time.
The key store MUST be in a secure area, to avoid, as much as
possible, direct attack on the validation system and secrets
database. Particularly, access to the key material should be limited
to programs and processes required by the validation system only.
M'Raihi, et al. Informational [Page 5]
RFC 6238 HOTPTimeBased May 2011
5.2. Validation and Time-Step Size
An OTP generated within the same time step will be the same. When an
OTP is received at a validation system, it doesn't know a client's
exact timestamp when an OTP was generated. The validation system may
typically use the timestamp when an OTP is received for OTP
comparison. Due to network latency, the gap (as measured by T, that
is, the number of time steps since T0) between the time that the OTP
was generated and the time that the OTP arrives at the receiving
system may be large. The receiving time at the validation system and
the actual OTP generation may not fall within the same time-step
window that produced the same OTP. When an OTP is generated at the
end of a time-step window, the receiving time most likely falls into
the next time-step window. A validation system SHOULD typically set
a policy for an acceptable OTP transmission delay window for
validation. The validation system should compare OTPs not only with
the receiving timestamp but also the past timestamps that are within
the transmission delay. A larger acceptable delay window would
expose a larger window for attacks. We RECOMMEND that at most one
time step is allowed as the network delay.
The time-step size has an impact on both security and usability. A
larger time-step size means a larger validity window for an OTP to be
accepted by a validation system. There are implications for using a
larger time-step size, as follows:
First, a larger time-step size exposes a larger window to attack.
When an OTP is generated and exposed to a third party before it is
consumed, the third party can consume the OTP within the time-step
window.
We RECOMMEND a default time-step size of 30 seconds. This default
value of 30 seconds is selected as a balance between security and
usability.
Second, the next different OTP must be generated in the next time-
step window. A user must wait until the clock moves to the next
time-step window from the last submission. The waiting time may not
be exactly the length of the time step, depending on when the last
OTP was generated. For example, if the last OTP was generated at the
halfway point in a time-step window, the waiting time for the next
OTP is half the length of the time step. In general, a larger time-
step window means a longer waiting time for a user to get the next
valid OTP after the last successful OTP validation. A too-large
window (for example, 10 minutes) most probably won't be suitable for
typical Internet login use cases; a user may not be able to get the
next OTP within 10 minutes and therefore will have to re-login to the
same site in 10 minutes.
M'Raihi, et al. Informational [Page 6]
RFC 6238 HOTPTimeBased May 2011
Note that a prover may send the same OTP inside a given time-step
window multiple times to a verifier. The verifier MUST NOT accept
the second attempt of the OTP after the successful validation has
been issued for the first OTP, which ensures one-time only use of an
OTP.
6. Resynchronization
Because of possible clock drifts between a client and a validation
server, we RECOMMEND that the validator be set with a specific limit
to the number of time steps a prover can be "out of synch" before
being rejected.
This limit can be set both forward and backward from the calculated
time step on receipt of the OTP value. If the time step is
30 seconds as recommended, and the validator is set to only accept
two time steps backward, then the maximum elapsed time drift would be
around 89 seconds, i.e., 29 seconds in the calculated time step and
60 seconds for two backward time steps.
This would mean the validator could perform a validation against the
current time and then two further validations for each backward step
(for a total of 3 validations). Upon successful validation, the
validation server can record the detected clock drift for the token
in terms of the number of time steps. When a new OTP is received
after this step, the validator can validate the OTP with the current
timestamp adjusted with the recorded number of time-step clock drifts
for the token.
Also, it is important to note that the longer a prover has not sent
an OTP to a validation system, the longer (potentially) the
accumulated clock drift between the prover and the verifier. In such
cases, the automatic resynchronization described above may not work
if the drift exceeds the allowed threshold. Additional
authentication measures should be used to safely authenticate the
prover and explicitly resynchronize the clock drift between the
prover and the validator.
7. Acknowledgements
The authors of this document would like to thank the following people
for their contributions and support to make this a better
specification: Hannes Tschofenig, Jonathan Tuliani, David Dix,
Siddharth Bajaj, Stu Veath, Shuh Chang, Oanh Hoang, John Huang, and
Siddhartha Mohapatra.
M'Raihi, et al. Informational [Page 7]
RFC 6238 HOTPTimeBased May 2011
8. References
8.1. Normative References
[RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-
Hashing for Message Authentication", RFC 2104,
February 1997.
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119, March 1997.
[RFC4086] Eastlake 3rd, D., Schiller, J., and S. Crocker,
"Randomness Recommendations for Security", BCP 106,
RFC 4086, June 2005.
[RFC4226] M'Raihi, D., Bellare, M., Hoornaert, F., Naccache, D., and
O. Ranen, "HOTP: An HMAC-Based One-Time Password
Algorithm", RFC 4226, December 2005.
[SHA2] NIST, "FIPS PUB 180-3: Secure Hash Standard (SHS)",
October 2008, <http://csrc.nist.gov/publications/fips/
fips180-3/fips180-3_final.pdf>.
8.2. Informative References
[CN] Coron, J. and D. Naccache, "An Accurate Evaluation of
Maurer's Universal Test", LNCS 1556, February 1999,
<http://www.gemplus.com/smart/rd/publications/pdf/
CN99maur.pdf>.
[RFC4301] Kent, S. and K. Seo, "Security Architecture for the
Internet Protocol", RFC 4301, December 2005.
[RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer Security
(TLS) Protocol Version 1.2", RFC 5246, August 2008.
[RFC6030] Hoyer, P., Pei, M., and S. Machani, "Portable Symmetric
Key Container (PSKC)", RFC 6030, October 2010.
[UT] Wikipedia, "Unix time", February 2011,
<http://en.wikipedia.org/wiki/Unix_time>.
M'Raihi, et al. Informational [Page 8]
RFC 6238 HOTPTimeBased May 2011
Appendix A. TOTP Algorithm: Reference Implementation
<CODE BEGINS>
/**
Copyright (c) 2011 IETF Trust and the persons identified as
authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, is permitted pursuant to, and subject to the license
terms contained in, the Simplified BSD License set forth in Section
4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info).
*/
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.util.TimeZone;
/**
* This is an example implementation of the OATH
* TOTP algorithm.
* Visit www.openauthentication.org for more information.
*
* @author Johan Rydell, PortWise, Inc.
*/
public class TOTP {
private TOTP() {}
/**
* This method uses the JCE to provide the crypto algorithm.
* HMAC computes a Hashed Message Authentication Code with the
* crypto hash algorithm as a parameter.
*
* @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
* HmacSHA512)
* @param keyBytes: the bytes to use for the HMAC key
* @param text: the message or text to be authenticated
*/
M'Raihi, et al. Informational [Page 9]
RFC 6238 HOTPTimeBased May 2011
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
byte[] text){
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
}
/**
* This method converts a HEX string to Byte[]
*
* @param hex: the HEX string
*
* @return: a byte array
*/
private static byte[] hexStr2Bytes(String hex){
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
// Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i+1];
return ret;
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
M'Raihi, et al. Informational [Page 10]
RFC 6238 HOTPTimeBased May 2011
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(String key,
String time,
String returnDigits){
return generateTOTP(key, time, returnDigits, "HmacSHA1");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP256(String key,
String time,
String returnDigits){
return generateTOTP(key, time, returnDigits, "HmacSHA256");
}
M'Raihi, et al. Informational [Page 11]
RFC 6238 HOTPTimeBased May 2011
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP512(String key,
String time,
String returnDigits){
return generateTOTP(key, time, returnDigits, "HmacSHA512");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @param crypto: the crypto function to use
*
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(String key,
String time,
String returnDigits,
String crypto){
int codeDigits = Integer.decode(returnDigits).intValue();
String result = null;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16 )
time = "0" + time;
// Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] k = hexStr2Bytes(key);
M'Raihi, et al. Informational [Page 12]
RFC 6238 HOTPTimeBased May 2011
byte[] hash = hmac_sha(crypto, k, msg);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[codeDigits];
result = Integer.toString(otp);
while (result.length() < codeDigits) {
result = "0" + result;
}
return result;
}
public static void main(String[] args) {
// Seed for HMAC-SHA1 - 20 bytes
String seed = "3132333435363738393031323334353637383930";
// Seed for HMAC-SHA256 - 32 bytes
String seed32 = "3132333435363738393031323334353637383930" +
"313233343536373839303132";
// Seed for HMAC-SHA512 - 64 bytes
String seed64 = "3132333435363738393031323334353637383930" +
"3132333435363738393031323334353637383930" +
"3132333435363738393031323334353637383930" +
"31323334";
long T0 = 0;
long X = 30;
long testTime[] = {59L, 1111111109L, 1111111111L,
1234567890L, 2000000000L, 20000000000L};
String steps = "0";
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
M'Raihi, et al. Informational [Page 13]
RFC 6238 HOTPTimeBased May 2011
try {
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
System.out.println(
"| Time(sec) | Time (UTC format) " +
"| Value of T(Hex) | TOTP | Mode |");
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
for (int i=0; i<testTime.length; i++) {
long T = (testTime[i] - T0)/X;
steps = Long.toHexString(T).toUpperCase();
while (steps.length() < 16) steps = "0" + steps;
String fmtTime = String.format("%1$-11s", testTime[i]);
String utcTime = df.format(new Date(testTime[i]*1000));
System.out.print("| " + fmtTime + " | " + utcTime +
" | " + steps + " |");
System.out.println(generateTOTP(seed, steps, "8",
"HmacSHA1") + "| SHA1 |");
System.out.print("| " + fmtTime + " | " + utcTime +
" | " + steps + " |");
System.out.println(generateTOTP(seed32, steps, "8",
"HmacSHA256") + "| SHA256 |");
System.out.print("| " + fmtTime + " | " + utcTime +
" | " + steps + " |");
System.out.println(generateTOTP(seed64, steps, "8",
"HmacSHA512") + "| SHA512 |");
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
}
}catch (final Exception e){
System.out.println("Error : " + e);
}
}
}
<CODE ENDS>
Appendix B. Test Vectors
This section provides test values that can be used for the HOTP time-
based variant algorithm interoperability test.
M'Raihi, et al. Informational [Page 14]
RFC 6238 HOTPTimeBased May 2011
The test token shared secret uses the ASCII string value
"12345678901234567890". With Time Step X = 30, and the Unix epoch as
the initial value to count time steps, where T0 = 0, the TOTP
algorithm will display the following values for specified modes and
timestamps.
+-------------+--------------+------------------+----------+--------+
| Time (sec) | UTC Time | Value of T (hex) | TOTP | Mode |
+-------------+--------------+------------------+----------+--------+
| 59 | 1970-01-01 | 0000000000000001 | 94287082 | SHA1 |
| | 00:00:59 | | | |
| 59 | 1970-01-01 | 0000000000000001 | 46119246 | SHA256 |
| | 00:00:59 | | | |
| 59 | 1970-01-01 | 0000000000000001 | 90693936 | SHA512 |
| | 00:00:59 | | | |
| 1111111109 | 2005-03-18 | 00000000023523EC | 07081804 | SHA1 |
| | 01:58:29 | | | |
| 1111111109 | 2005-03-18 | 00000000023523EC | 68084774 | SHA256 |
| | 01:58:29 | | | |
| 1111111109 | 2005-03-18 | 00000000023523EC | 25091201 | SHA512 |
| | 01:58:29 | | | |
| 1111111111 | 2005-03-18 | 00000000023523ED | 14050471 | SHA1 |
| | 01:58:31 | | | |
| 1111111111 | 2005-03-18 | 00000000023523ED | 67062674 | SHA256 |
| | 01:58:31 | | | |
| 1111111111 | 2005-03-18 | 00000000023523ED | 99943326 | SHA512 |
| | 01:58:31 | | | |
| 1234567890 | 2009-02-13 | 000000000273EF07 | 89005924 | SHA1 |
| | 23:31:30 | | | |
| 1234567890 | 2009-02-13 | 000000000273EF07 | 91819424 | SHA256 |
| | 23:31:30 | | | |
| 1234567890 | 2009-02-13 | 000000000273EF07 | 93441116 | SHA512 |
| | 23:31:30 | | | |
| 2000000000 | 2033-05-18 | 0000000003F940AA | 69279037 | SHA1 |
| | 03:33:20 | | | |
| 2000000000 | 2033-05-18 | 0000000003F940AA | 90698825 | SHA256 |
| | 03:33:20 | | | |
| 2000000000 | 2033-05-18 | 0000000003F940AA | 38618901 | SHA512 |
| | 03:33:20 | | | |
| 20000000000 | 2603-10-11 | 0000000027BC86AA | 65353130 | SHA1 |
| | 11:33:20 | | | |
| 20000000000 | 2603-10-11 | 0000000027BC86AA | 77737706 | SHA256 |
| | 11:33:20 | | | |
| 20000000000 | 2603-10-11 | 0000000027BC86AA | 47863826 | SHA512 |
| | 11:33:20 | | | |
+-------------+--------------+------------------+----------+--------+
Table 1: TOTP Table
M'Raihi, et al. Informational [Page 15]
RFC 6238 HOTPTimeBased May 2011
Authors' Addresses
David M'Raihi
Verisign, Inc.
685 E. Middlefield Road
Mountain View, CA 94043
USA
EMail: davidietf@gmail.com
Salah Machani
Diversinet Corp.
2225 Sheppard Avenue East, Suite 1801
Toronto, Ontario M2J 5C2
Canada
EMail: smachani@diversinet.com
Mingliang Pei
Symantec
510 E. Middlefield Road
Mountain View, CA 94043
USA
EMail: Mingliang_Pei@symantec.com
Johan Rydell
Portwise, Inc.
275 Hawthorne Ave., Suite 119
Palo Alto, CA 94301
USA
EMail: johanietf@gmail.com
M'Raihi, et al. Informational [Page 16]

533
totp.go Normal file
View File

@ -0,0 +1,533 @@
package twofactor
import (
"bytes"
"code.google.com/p/rsc/qr"
"crypto"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base32"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"net/url"
"strconv"
"time"
)
const (
BACKOFF_MINUTES = 5 // this is the time to wait before verifying another token
MAX_FAILURES = 3 // total amount of failures, after that the user needs to wait for the backoff time
COUNTER_SIZE = 8
)
type totp struct {
key []byte // this is the secret key
counter [COUNTER_SIZE]byte // this is the counter used to synchronize with the client device
digits int // total amount of digits of the code displayed on the device
issuer string // the company which issues the 2FA
account string // usually the suer email or the account id
stepSize int // by default 30 seconds
clientOffset int // the amount of steps the client is off
totalVerificationFailures int // the total amount of verification failures from the client - by default 10
lastVerificationTime time.Time // the last verification executed
hashFunction crypto.Hash // the hash function used in the HMAC construction (sha1 - sha156 - sha512)
}
// This function is used to synchronize the counter with the client
// Offset can be a negative number as well
// Usually it's either -1, 0 or 1
// This is used internally
func (otp *totp) synchronizeCounter(offset int) {
otp.clientOffset = offset
}
// Label returns the combination of issuer:account string
func (otp *totp) label() string {
return url.QueryEscape(fmt.Sprintf("%s:%s", otp.issuer, otp.account))
}
// Counter returns the TOTP's 8-byte counter as unsigned 64-bit integer.
func (otp *totp) getIntCounter() uint64 {
return uint64FromBigEndian(otp.counter)
}
// This function creates a new TOTP object
// This is the function which is needed to start the whole process
// account: usually the user email
// issuer: the name of the company/service
// hash: is the crypto function used: crypto.SHA1, crypto.SHA256, crypto.SHA512
// digits: is the token amount of digits (6 or 7 or 8)
// steps: the amount of second the token is valid
// it autmatically generates a secret key using the golang crypto rand package. If there is not enough entropy the function returns an error
// The key is not encrypted in this package. It's a secret key. Therefore if you transfer the key bytes in the network,
// please take care of protecting the key or in fact all the bytes.
func NewTOTP(account, issuer string, hash crypto.Hash, digits int) (*totp, error) {
keySize := hash.Size()
key := make([]byte, keySize)
total, err := rand.Read(key)
if err != nil {
return nil, errors.New(fmt.Sprintf("TOTP failed to create because there is not enough entropy, we got only %d random bytes", total))
}
// sanitize the digits range otherwise it may create invalid tokens !
if digits < 6 || digits > 8 {
digits = 8
}
return makeTOTP(key, account, issuer, hash, digits)
}
// Private function which initialize the TOTP so that it's easier to unit test it
// Used internnaly
func makeTOTP(key []byte, account, issuer string, hash crypto.Hash, digits int) (*totp, error) {
otp := new(totp)
otp.key = key
otp.account = account
otp.issuer = issuer
otp.digits = digits
otp.stepSize = 30 // we set it to 30 seconds which is the recommended value from the RFC
otp.clientOffset = 0
otp.hashFunction = hash
return otp, nil
}
// This function validates the user privided token
// It calculates 3 different tokens. The current one, one before now and one after now.
// The difference is driven by the TOTP step size
// Based on which of the 3 steps it succeeds to validates, the client offset is updated.
// It also updates the total amount of verification failures and the last time a verification happened in UTC time
// Returns an error in case of verification failure, with the reason
// There is a very basic method which protects from timing attacks, although if the step time used is low it should not be necessary
// An attacker can still learn the synchronization offset. This is however irrelevant because the attacker has then 30 seconds to
// guess the code and after 3 failures the function returns an error for the following 5 minutes
func (otp *totp) Validate(userCode string) error {
// verify that the token is valid
if userCode == "" {
return errors.New("User provided token is empty")
}
// check against the total amount of failures
if otp.totalVerificationFailures >= MAX_FAILURES && !validBackoffTime(otp.lastVerificationTime) {
return errors.New("The verification is locked down, because of too many trials.")
}
if otp.totalVerificationFailures >= MAX_FAILURES && validBackoffTime(otp.lastVerificationTime) {
// reset the total verification failures counter
otp.totalVerificationFailures = 0
}
// calculate the sha256 of the user code
userTokenHash := sha256.Sum256([]byte(userCode))
userToken := hex.EncodeToString(userTokenHash[:])
// 1 calculate the 3 tokens
tokens := make([]string, 3)
token0Hash := sha256.Sum256([]byte(calculateTOTP(otp, -1)))
token1Hash := sha256.Sum256([]byte(calculateTOTP(otp, 0)))
token2Hash := sha256.Sum256([]byte(calculateTOTP(otp, 1)))
tokens[0] = hex.EncodeToString(token0Hash[:]) // sha256.Sum256() // 30 seconds ago token
tokens[1] = hex.EncodeToString(token1Hash[:]) // current token
tokens[2] = hex.EncodeToString(token2Hash[:]) // next 30 seconds token
// if the current time token is valid then, no need to re-sync and return nil
if tokens[1] == userToken {
return nil
}
// if the let's say 30 seconds ago token is valid then return nil, but re-synchronize
if tokens[0] == userToken {
otp.synchronizeCounter(-1)
return nil
}
// if the let's say 30 seconds ago token is valid then return nil, but re-synchronize
if tokens[2] == userToken {
otp.synchronizeCounter(1)
return nil
}
otp.totalVerificationFailures++
otp.lastVerificationTime = time.Now().UTC() // important to have it in UTC
// if we got here everything is good
return errors.New("Tokens mismatch.")
}
// Checks the time difference between the function call time and the parameter
// if the difference of time is greater than BACKOFF_MINUTES it returns true, otherwise false
func validBackoffTime(lastVerification time.Time) bool {
diff := lastVerification.UTC().Add(BACKOFF_MINUTES * time.Minute)
return time.Now().UTC().After(diff)
}
// 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
clientOffset := time.Duration(otp.clientOffset*otp.stepSize) * time.Second
now := time.Now().UTC().Add(counterOffset).Add(clientOffset).Unix()
otp.counter = bigEndianUint64(increment(now, otp.stepSize))
}
// Function which calculates the value of T (see rfc6238)
func increment(ts int64, stepSize int) uint64 {
T := float64(ts / int64(stepSize)) // TODO: improve this conversions
n := round(T) // round T
return n // convert n to big endian byte array
}
// Generates a new one time password with hmac-(HASH-FUNCTION)
func (otp *totp) OTP() string {
// it uses the index 0, meaning that it calculates the current one
return calculateTOTP(otp, 0)
}
// Private function which calculates the OTP token based on the index offset
// example: 1 * steps or -1 * steps
func calculateTOTP(otp *totp, index int) string {
var h hash.Hash
switch otp.hashFunction {
case crypto.SHA256:
h = hmac.New(sha256.New, otp.key)
break
case crypto.SHA512:
h = hmac.New(sha512.New, otp.key)
break
default:
h = hmac.New(sha1.New, otp.key)
break
}
// set the counter to the current step based ont he current time
// this is necessary to generate the proper OTP
otp.incrementCounter(index)
return calculateToken(otp.counter[:], otp.digits, h)
}
func truncateHash(hmac_result []byte, size int) int64 {
offset := hmac_result[size-1] & 0xf
bin_code := (uint32(hmac_result[offset])&0x7f)<<24 |
(uint32(hmac_result[offset+1])&0xff)<<16 |
(uint32(hmac_result[offset+2])&0xff)<<8 |
(uint32(hmac_result[offset+3]) & 0xff)
return int64(bin_code)
}
// this is the function which calculates the HTOP code
func calculateToken(counter []byte, digits int, h hash.Hash) string {
h.Write(counter)
hashResult := h.Sum(nil)
result := truncateHash(hashResult, h.Size())
var mod uint64
if digits == 8 {
mod = uint64(result % 100000000)
}
if digits == 7 {
mod = uint64(result % 10000000)
}
if digits == 6 {
mod = uint64(result % 1000000)
}
fmtStr := fmt.Sprintf("%%0%dd", digits)
return fmt.Sprintf(fmtStr, mod)
}
// URL returns a suitable URL, such as for the Google Authenticator app
// example: otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
func (otp *totp) URL() string {
secret := base32.StdEncoding.EncodeToString(otp.key)
u := url.URL{}
v := url.Values{}
u.Scheme = "otpauth"
u.Host = "totp"
u.Path = otp.label()
v.Add("secret", secret)
v.Add("counter", fmt.Sprintf("%d", otp.getIntCounter()))
v.Add("issuer", otp.issuer)
v.Add("digits", strconv.Itoa(otp.digits))
v.Add("period", strconv.Itoa(otp.stepSize))
switch otp.hashFunction {
case crypto.SHA256:
v.Add("algorithm", "SHA256")
break
case crypto.SHA512:
v.Add("algorithm", "SHA512")
break
default:
v.Add("algorithm", "SHA1")
break
}
u.RawQuery = v.Encode()
return u.String()
}
// QR generates a byte array containing QR code encoded PNG image, with level Q error correction,
// needed for the client apps to generate tokens
// The QR code should be displayed only the first time the user enabled the Two-Factor authentication.
// The QR code contains the shared KEY between the server application and the client application,
// therefore the QR code should be delivered via secure connection.
func (otp *totp) QR() ([]byte, error) {
u := otp.URL()
code, err := qr.Encode(u, qr.Q)
if err != nil {
return nil, err
}
return code.PNG(), nil
}
// 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
// TODO:
// 1- improve sizes. For instance the hashFunction_type could be a short.
// 2- Encrypt the key, in case it's transferred in the network unsafely
func (otp *totp) ToBytes() ([]byte, error) {
var buffer bytes.Buffer
// caluclate the length of the key and create its byte representation
keySize := len(otp.key)
keySizeBytes := bigEndianInt(keySize)
// caluclate the length of the issuer and create its byte representation
issuerSize := len(otp.issuer)
issuerSizeBytes := bigEndianInt(issuerSize)
// caluclate the length of the account and create its byte representation
accountSize := len(otp.account)
accountSizeBytes := bigEndianInt(accountSize)
totalSize := 4 + 4 + keySize + 8 + 4 + 4 + issuerSize + 4 + accountSize + 4 + 4 + 4 + 8 + 4
totalSizeBytes := bigEndianInt(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 := bigEndianUint64(otp.getIntCounter())
if _, err := buffer.Write(counterBytes[:]); err != nil {
return nil, err
}
// digits
digitBytes := bigEndianInt(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 := bigEndianInt(otp.stepSize)
if _, err := buffer.Write(stepsBytes[:]); err != nil {
return nil, err
}
// offset
offsetBytes := bigEndianInt(otp.clientOffset)
if _, err := buffer.Write(offsetBytes[:]); err != nil {
return nil, err
}
// total_failures
totalFailuresBytes := bigEndianInt(otp.totalVerificationFailures)
if _, err := buffer.Write(totalFailuresBytes[:]); err != nil {
return nil, err
}
// last verification time
verificationTimeBytes := bigEndianUint64(uint64(otp.lastVerificationTime.Unix()))
if _, err := buffer.Write(verificationTimeBytes[:]); err != nil {
return nil, err
}
// has_function_type
switch otp.hashFunction {
case crypto.SHA256:
sha256Bytes := bigEndianInt(1)
if _, err := buffer.Write(sha256Bytes[:]); err != nil {
return nil, err
}
break
case crypto.SHA512:
sha512Bytes := bigEndianInt(2)
if _, err := buffer.Write(sha512Bytes[:]); err != nil {
return nil, err
}
break
default:
sha1Bytes := bigEndianInt(0)
if _, err := buffer.Write(sha1Bytes[:]); err != nil {
return nil, err
}
}
//fmt.Println("Total bytes", len(buffer.Bytes()))
return buffer.Bytes(), nil
}
// TOTPFromBytes converts a byte array to a totp object
// it stores the state of the TOTP object, like the key, the current counter, the client offset,
// the total amount of verification failures and the last time a verification happened
func TOTPFromBytes(data []byte) (*totp, error) {
// fmt.Println("Bytes", len(data))
// new reader
reader := bytes.NewReader(data)
// otp object
otp := new(totp)
// get the lenght
lenght := make([]byte, 4)
_, err := reader.Read(lenght) // read the 4 bytes for the total lenght
if err != nil && err != io.EOF {
return otp, err
}
totalSize := intFromBigEndian([4]byte{lenght[0], lenght[1], lenght[2], lenght[3]})
buffer := make([]byte, totalSize-4)
_, err = reader.Read(buffer)
if err != nil && err != io.EOF {
return otp, err
}
// skip the total bytes size
startOffset := 0
// read key size
endOffset := startOffset + 4
keyBytes := buffer[startOffset:endOffset]
keySize := intFromBigEndian([4]byte{keyBytes[0], keyBytes[1], keyBytes[2], keyBytes[3]})
// read the key
startOffset = endOffset
endOffset = startOffset + keySize
otp.key = buffer[startOffset:endOffset]
// read the counter
startOffset = endOffset
endOffset = startOffset + 8
b := buffer[startOffset:endOffset]
otp.counter = [8]byte{b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]}
// read the digits
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
otp.digits = intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]}) //
// read the issuer size
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
issuerSize := intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]})
// read the issuer string
startOffset = endOffset
endOffset = startOffset + issuerSize
otp.issuer = string(buffer[startOffset:endOffset])
// read the account size
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
accountSize := intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]})
// read the account string
startOffset = endOffset
endOffset = startOffset + accountSize
otp.account = string(buffer[startOffset:endOffset])
// read the steps
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
otp.stepSize = intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]})
// read the offset
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
otp.clientOffset = intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]})
// read the total failuers
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
otp.totalVerificationFailures = intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]})
// read the offset
startOffset = endOffset
endOffset = startOffset + 8
b = buffer[startOffset:endOffset]
ts := uint64FromBigEndian([8]byte{b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]})
otp.lastVerificationTime = time.Unix(int64(ts), 0)
// read the hash type
startOffset = endOffset
endOffset = startOffset + 4
b = buffer[startOffset:endOffset]
hashType := intFromBigEndian([4]byte{b[0], b[1], b[2], b[3]})
switch hashType {
case 1:
otp.hashFunction = crypto.SHA256
break
case 2:
otp.hashFunction = crypto.SHA512
break
default:
otp.hashFunction = crypto.SHA1
}
return otp, err
}

288
totp_test.go Normal file
View File

@ -0,0 +1,288 @@
package twofactor
import (
"bytes"
"crypto"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"net/url"
"testing"
"time"
)
var sha1KeyHex = "3132333435363738393031323334353637383930"
var sha256KeyHex = "3132333435363738393031323334353637383930313233343536373839303132"
var sha512KeyHex = "31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334"
var sha1TestData = []string{
"94287082",
"07081804",
"14050471",
"89005924",
"69279037",
"65353130",
}
var sha256TestData = []string{
"46119246",
"68084774",
"67062674",
"91819424",
"90698825",
"77737706",
}
var sha512TestData = []string{
"90693936",
"25091201",
"99943326",
"93441116",
"38618901",
"47863826",
}
var timeCounters = []int64{
int64(59), // 1970-01-01 00:00:59
int64(1111111109), // 2005-03-18 01:58:29
int64(1111111111), // 2005-03-18 01:58:31
int64(1234567890), // 2009-02-13 23:31:30
int64(2000000000), // 2033-05-18 03:33:20
int64(20000000000), // 2603-10-11 11:33:20
}
func checkError(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
}
}
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 = "no-reply@sec51.com"
// Test SHA1
otp.key = keySha1
for index, ts := range timeCounters {
counter := increment(ts, 30)
otp.counter = bigEndianUint64(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 = bigEndianUint64(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 = bigEndianUint64(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)
}
}
}
func TestVerificationFailures(t *testing.T) {
otp, err := NewTOTP("info@sec51.com", "Sec51", crypto.SHA1, 7)
//checkError(t, err)
if err != nil {
t.Fatal(err)
}
// generate a new token
expectedToken := otp.OTP()
//verify the new token
if err := otp.Validate(expectedToken); err != nil {
t.Fatal(err)
}
// verify the wrong token for 3 times and check the internal counters values
for i := 0; i < 3; i++ {
if err := otp.Validate("1234567"); err == nil {
t.Fatal(err)
}
}
if otp.totalVerificationFailures != 3 {
t.Errorf("Expected 3 verifcation failures, instead we've got %d\n", otp.totalVerificationFailures)
}
// at this point we crossed the max failures, therefore it should always return an error
for i := 0; i < 10; i++ {
if err := otp.Validate(expectedToken); err == nil {
t.Fatal(err)
}
}
// set the lastVerificationTime ahead in the future.
// it should at this point pass
back10Minutes := time.Duration(-10) * time.Minute
otp.lastVerificationTime = time.Now().UTC().Add(back10Minutes)
for i := 0; i < 10; i++ {
if err := otp.Validate(expectedToken); err != nil {
t.Fatal(err)
}
if i == 0 {
// at this point the max failure counter should have been reset to zero
if otp.totalVerificationFailures != 0 {
t.Errorf("totalVerificationFailures counter not reset to zero. We've got: %d\n", otp.totalVerificationFailures)
}
}
}
}
func TestIncrementCounter(t *testing.T) {
ts := int64(1438601387)
unixTime := time.Unix(ts, 0).UTC()
// DEBUG
// fmt.Println(time.Unix(ts, 0).UTC().Format(time.RFC1123))
result := increment(unixTime.Unix(), 30)
expected := uint64(47953379)
if result != expected {
t.Fatal("Error incrementing counter")
}
}
func TestSerialization(t *testing.T) {
// create a new TOTP
otp, err := NewTOTP("info@sec51.com", "Sec51", crypto.SHA512, 8)
if err != nil {
t.Fatal(err)
}
// set some properties to a value different than the default
otp.totalVerificationFailures = 2
otp.stepSize = 27
otp.lastVerificationTime = time.Now().UTC()
otp.clientOffset = 1
// Serialize it to bytes
otpData, err := otp.ToBytes()
if err != nil {
t.Fatal(err)
}
// Convert it back from bytes to TOTP
deserializedOTP, err := TOTPFromBytes(otpData)
if err != nil {
t.Fatal(err)
}
deserializedOTPData, err := deserializedOTP.ToBytes()
if err != nil {
t.Fatal(err)
}
if deserializedOTP == nil {
t.Error("Could not deserialize back the TOTP object from bytes")
}
if bytes.Compare(deserializedOTP.key, otp.key) != 0 {
t.Error("Deserialized digits property differ from original TOTP")
}
if deserializedOTP.digits != otp.digits {
t.Error("Deserialized digits property differ from original TOTP")
}
if deserializedOTP.totalVerificationFailures != otp.totalVerificationFailures {
t.Error("Deserialized totalVerificationFailures property differ from original TOTP")
}
if deserializedOTP.stepSize != otp.stepSize {
t.Error("Deserialized stepSize property differ from original TOTP")
}
if deserializedOTP.lastVerificationTime.Unix() != otp.lastVerificationTime.Unix() {
t.Error("Deserialized lastVerificationTime property differ from original TOTP")
}
if deserializedOTP.getIntCounter() != otp.getIntCounter() {
t.Error("Deserialized counter property differ from original TOTP")
}
if deserializedOTP.clientOffset != otp.clientOffset {
t.Error("Deserialized clientOffset property differ from original TOTP")
}
if deserializedOTP.account != otp.account {
t.Error("Deserialized account property differ from original TOTP")
}
if deserializedOTP.issuer != otp.issuer {
t.Error("Deserialized issuer property differ from original TOTP")
}
if deserializedOTP.OTP() != otp.OTP() {
t.Error("Deserialized OTP token property differ from original TOTP")
}
if deserializedOTP.hashFunction != otp.hashFunction {
t.Error("Deserialized hash property differ from original TOTP")
}
if deserializedOTP.URL() != otp.URL() {
t.Error("Deserialized URL property differ from original TOTP")
}
if deserializedOTP.label() != otp.label() {
t.Error("Deserialized Label property differ from original TOTP")
}
if base64.StdEncoding.EncodeToString(otpData) != base64.StdEncoding.EncodeToString(deserializedOTPData) {
t.Error("Problems encoding TOTP to base64")
}
label, err := url.QueryUnescape(otp.label())
if err != nil {
t.Fatal(err)
}
if label != "Sec51:info@sec51.com" {
t.Error("Creation of TOTP Label failed")
}
}