-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvoicecode.go
More file actions
137 lines (116 loc) · 3.26 KB
/
voicecode.go
File metadata and controls
137 lines (116 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// Copyright 2017-2026 Allow2 Pty Ltd. All rights reserved.
// Use of this source code is governed by the Allow2 API and SDK Licence.
package allow2service
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
)
// GenerateVoiceChallenge generates an offline voice code challenge-response pair.
//
// Challenge format: T A MM NN
//
// T = Request type (0 = more time, 1 = day type change, 2 = ban lift)
// A = Activity ID (0-99)
// MM = Minutes in 5-minute increments (0-99, so 0-495 minutes)
// NN = Random nonce (0-99)
//
// Response: First 6 digits of HMAC-SHA256(secret, challenge + date)
// Date-bound: expires at midnight in the child's timezone.
//
// Pass an empty string for date to use today's date.
func GenerateVoiceChallenge(secret string, reqType RequestType, activityID, minutes int, date string) (*VoiceCodePair, error) {
if date == "" {
date = time.Now().Format("2006-01-02")
}
// T: request type code (0-2)
t := reqType.VoiceCode()
// A: activity ID clamped to 0-99
a := activityID
if a < 0 {
a = 0
}
if a > 99 {
a = 99
}
// MM: minutes in 5-minute increments, clamped to 0-99
mm := int(float64(minutes)/5.0 + 0.5)
if mm < 0 {
mm = 0
}
if mm > 99 {
mm = 99
}
// NN: random nonce 0-99
nn := rand.Intn(100)
challenge := fmt.Sprintf("%d %02d %02d %02d", t, a, mm, nn)
expectedResponse := computeResponse(secret, challenge, date)
return &VoiceCodePair{
Challenge: challenge,
ExpectedResponse: expectedResponse,
}, nil
}
// VerifyVoiceResponse verifies a voice code response against a challenge.
// Pass an empty string for date to use today's date.
func VerifyVoiceResponse(secret, challenge, response, date string) bool {
if date == "" {
date = time.Now().Format("2006-01-02")
}
expected := computeResponse(secret, challenge, date)
// Constant-time comparison
return hmac.Equal([]byte(expected), []byte(response))
}
// DecodeChallenge decodes a challenge string into its component parts.
// Returns nil if the format is invalid.
func DecodeChallenge(challenge string) *DecodedChallenge {
parts := strings.Fields(strings.TrimSpace(challenge))
if len(parts) != 4 {
return nil
}
typeCode, err := strconv.Atoi(parts[0])
if err != nil {
return nil
}
activityID, err := strconv.Atoi(parts[1])
if err != nil {
return nil
}
minuteIncrements, err := strconv.Atoi(parts[2])
if err != nil {
return nil
}
nonce, err := strconv.Atoi(parts[3])
if err != nil {
return nil
}
return &DecodedChallenge{
Type: typeCode,
ActivityID: activityID,
Minutes: minuteIncrements * 5,
Nonce: nonce,
}
}
// DecodedChallenge represents the parsed components of a voice code challenge.
type DecodedChallenge struct {
Type int
ActivityID int
Minutes int
Nonce int
}
// computeResponse computes the 6-digit response for a challenge + date.
func computeResponse(secret, challenge, date string) string {
message := challenge + date
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
hmacHex := hex.EncodeToString(mac.Sum(nil))
// Take first 8 hex chars, convert to integer, modulo 1,000,000 for 6 digits
hex8 := hmacHex[:8]
truncated, _ := strconv.ParseUint(hex8, 16, 64)
code := truncated % 1000000
return fmt.Sprintf("%06d", code)
}