• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

H0llyW00dzZ / fiber2fa / #4

12 Jun 2024 11:32AM UTC coverage: 85.819% (+0.1%) from 85.714%
#4

push

web-flow
Improve Performance for HOTP OCRA (#196)

* Refactor OCRA Verify

- [+] refactor(otpverifier): simplify OCRA token comparison logic

* Reduce allocations per operation

- [+] refactor(benchmark_test.go): update benchmark results for OCRAVerify with reduced allocations per operation
- [+] refactor(hotp_ocra.go): optimize generateOCRA by preallocating data slice and using copy instead of append
- [+] refactor(hotp_ocra.go): simplify HOTP string formatting using fmt.Sprintf with width and padding options

6 of 6 new or added lines in 1 file covered. (100.0%)

14 existing lines in 3 files now uncovered.

1174 of 1368 relevant lines covered (85.82%)

65.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.51
/internal/otpverifier/hotp_ocra.go
1
// Copyright (c) 2024 H0llyW00dz All rights reserved.
2
//
3
// License: BSD 3-Clause License
4

5
package otpverifier
6

7
import (
8
        "crypto/hmac"
9
        "crypto/sha1"
10
        "crypto/sha256"
11
        "crypto/sha512"
12
        "crypto/subtle"
13
        "encoding/binary"
14
        "fmt"
15
        "hash"
16
        "strconv"
17
        "strings"
18
)
19

20
// OCRAVerifier is an OCRA verifier (RFC 6287) that implements the OTPVerifier interface.
21
type OCRAVerifier struct {
22
        config Config
23
}
24

25
// NewOCRAVerifier creates a new OCRAVerifier with the given configuration.
26
//
27
// Note: This requires building own 2FA apps because it involves a question-answer process.
28
// For example, a mobile app can be built to generate OCRA tokens based on the shared secret key and the challenge.
29
// The app would prompt the user with the question from the challenge and expect the user to provide the correct answer.
30
// The answer, along with other parameters like the counter value, would be used to generate the OCRA token.
31
// The generated token can then be entered by the user on the server-side for verification.
32
// Building own 2FA app allows customizing the user experience and integrating OCRA seamlessly into the authentication flow.
33
func NewOCRAVerifier(config ...Config) *OCRAVerifier {
12✔
34
        c := DefaultConfig
12✔
35
        if len(config) > 0 {
24✔
36
                c = config[0]
12✔
37
        }
12✔
38

39
        // Use default values if not provided
40
        if c.Digits <= 4 { // minimum is 5 and max is 8
20✔
41
                c.Digits = DefaultConfig.Digits
8✔
42
                c.Crypto = DefaultConfig.Crypto
8✔
43
        }
8✔
44
        if c.URITemplate == "" {
20✔
45
                c.URITemplate = DefaultConfig.URITemplate
8✔
46
        }
8✔
47

48
        return &OCRAVerifier{
12✔
49
                config: c,
12✔
50
        }
12✔
51
}
52

53
// GenerateToken generates an OCRA token based on the provided challenge.
54
func (v *OCRAVerifier) GenerateToken(challenge string) string {
23✔
55
        // Assume the following challenge string format: "OCRA-1:HOTP-<hash>-<digits>:<parameters>-<counter>-<question>"
23✔
56
        parts := strings.Split(challenge, ":")
23✔
57
        if len(parts) != 3 {
25✔
58
                panic("Invalid challenge format")
2✔
59
        }
60

61
        // Extract the relevant parts from the challenge
62
        ocraSuite := parts[0] + ":" + parts[1]
21✔
63
        remainingParts := strings.Split(parts[2], "-")
21✔
64
        if len(remainingParts) < 3 {
22✔
65
                panic("Invalid challenge format")
1✔
66
        }
67

68
        counter, err := strconv.ParseUint(remainingParts[1], 10, 64)
20✔
69
        if err != nil {
21✔
70
                panic("Invalid counter value: " + err.Error())
1✔
71
        }
72
        question := remainingParts[2]
19✔
73

19✔
74
        // Further checks on the OCRA suite format (if necessary)
19✔
75
        suiteComponents := strings.Split(ocraSuite, ":")
19✔
76
        if len(suiteComponents) != 2 || suiteComponents[0] != "OCRA-1" {
20✔
77
                panic("Unsupported OCRA suite")
1✔
78
        }
79

80
        // Determine the hash algorithm based on the OCRA suite
81
        //
82
        // TODO: use constant
83
        //
84
        // Note: This hash implementation does not rely on the RFC truncation method (see https://datatracker.ietf.org/doc/html/rfc6287#section-5.2 which is bad it literally break cryptographic principles) because it is written in Go, not in other languages like Java.
85
        // It is already 100% secure and guaranteed due to the use of the crypto/subtle package.
86
        // Also note that we might implement our own method because it's relatively easy to create a custom OTP based on HMAC-Truncated.
87
        var hash func() hash.Hash
18✔
88
        switch {
18✔
89
        case strings.HasPrefix(suiteComponents[1], "HOTP-SHA1"):
9✔
90
                hash = sha1.New
9✔
91
        case strings.HasPrefix(suiteComponents[1], "HOTP-SHA256"):
4✔
92
                hash = sha256.New
4✔
93
        case strings.HasPrefix(suiteComponents[1], "HOTP-SHA512"):
4✔
94
                hash = sha512.New
4✔
95
        default:
1✔
96
                panic("Unsupported hash algorithm")
1✔
97
        }
98

99
        // Generate the OCRA token based on the OCRA suite
100
        return v.generateOCRA(counter, question, hash)
17✔
101
}
102

103
// generateOCRA generates an OCRA token using the specified hash algorithm.
104
func (v *OCRAVerifier) generateOCRA(counter uint64, question string, hash func() hash.Hash) string {
17✔
105
        // Prepare the input data
17✔
106
        // Note: The counterBytes and counter are not just any values. They can be bound to a cryptographically secure pseudorandom number,
17✔
107
        // along with questionBytes, similar to how [DecodeBase32WithPadding] is used to manipulate the result in the frontend hahaha.
17✔
108
        var data []byte
17✔
109
        data = make([]byte, 8+len(question))
17✔
110
        binary.BigEndian.PutUint64(data[:8], counter)
17✔
111
        copy(data[8:], question)
17✔
112

17✔
113
        // Generate the HMAC hash
17✔
114
        hmacHash := hmac.New(hash, v.config.DecodeBase32WithPadding())
17✔
115
        hmacHash.Write(data)
17✔
116
        hashValue := hmacHash.Sum(nil)
17✔
117

17✔
118
        // Truncate the hash to obtain the HOTP value
17✔
119
        //
17✔
120
        // Note: This method is the same as the one used in the GOTP library by xlzd.
17✔
121
        // the only thing different, this not hard-coded raw and allow customized truncated across signature of HMAC
17✔
122
        offset := hashValue[len(hashValue)-1] & 0xf
17✔
123
        truncatedHash := binary.BigEndian.Uint32(hashValue[offset : offset+4])
17✔
124

17✔
125
        // Calculate the HOTP Ocra value using modulo operation instead of [math.Pow10].
17✔
126
        // This achieves the same result as using [math.Pow10] however this is more efficient due use magic calculator. ¯\_(ツ)_/¯
17✔
127
        p10n := v.config.cryptoPow10n(v.config.Digits)
17✔
128
        hotp := truncatedHash % uint32(p10n)
17✔
129

17✔
130
        // Format the HOTP value as a string with the specified number of digits
17✔
131
        return fmt.Sprintf("%0*d", v.config.Digits, hotp) // Result will padding it with leading zeros if necessary.
17✔
132
}
17✔
133

134
// GenerateOTPURL creates the URL for the QR code based on the provided URI template.
135
func (v *OCRAVerifier) GenerateOTPURL(issuer, accountName string) string {
×
UNCOV
136
        return v.config.generateOTPURL(issuer, accountName)
×
UNCOV
137
}
×
138

139
// Verify checks if the provided token and signature are valid for the specified challenge.
140
func (v *OCRAVerifier) Verify(token string, challenge string) bool {
6✔
141
        // Generate the expected OCRA token based on the challenge
6✔
142
        expectedToken := v.GenerateToken(challenge)
6✔
143

6✔
144
        // Compare the provided token with the expected token
6✔
145
        // Note: Signature verification is not applicable here because the OCRA algorithm itself provides sufficient security.
6✔
146
        // It follows the specifications defined in RFC 6287 (https://datatracker.ietf.org/doc/html/rfc6287#section-7.1)
6✔
147
        // and uses this [crypto/subtle] package, which is a crucial component in cryptographic operations.
6✔
148
        return subtle.ConstantTimeCompare([]byte(token), []byte(expectedToken)) == 1
6✔
149
}
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc