mirror of
https://github.com/1f349/twofactor.git
synced 2024-12-21 15:04:11 +00:00
Version 1.0
This commit is contained in:
commit
00e19bbd7f
7
.travis.yml
Normal file
7
.travis.yml
Normal file
@ -0,0 +1,7 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
13
LICENSE
Normal file
13
LICENSE
Normal 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
129
README.md
Normal 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
51
conversions.go
Normal 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
101
conversions_test.go
Normal 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
25
doc.go
Normal 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
2075
rfc4226.txt
Normal file
File diff suppressed because it is too large
Load Diff
899
rfc6238.txt
Normal file
899
rfc6238.txt
Normal 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
533
totp.go
Normal 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
288
totp_test.go
Normal 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")
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user