Skip to content

Commit a54e8ab

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 32f5caa + f2c8cd7 commit a54e8ab

File tree

6 files changed

+118
-56
lines changed

6 files changed

+118
-56
lines changed

.github/workflows/go.yml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
11+
build:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v2
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v2
18+
with:
19+
go-version: 1.17
20+
21+
- name: Build
22+
run: go build -v ./...
23+
24+
- name: Test
25+
run: go test -v ./...

hotp.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"sync"
1515
)
1616

17+
const DefaultTransactionOffset = -1
18+
1719
// HOTP is an implementation of RFC4226, HMAC-based one-time password algorithm
1820
type HOTP struct {
1921
OTP
@@ -56,7 +58,7 @@ func NewHOTPFromUri(uri string) (*OTPKeyData, error) {
5658
if !u.Query().Has(counterKey) {
5759
return nil, fmt.Errorf("'counter' parameter required")
5860
}
59-
label, issuer := getLabelIssuer(u)
61+
accountName, issuer := getAccountIssuer(u)
6062
digits := int64(DefaultDigits)
6163
if u.Query().Has(digitsKey) {
6264
digits, err = strconv.ParseInt(u.Query().Get(digitsKey), 10, 32)
@@ -84,17 +86,17 @@ func NewHOTPFromUri(uri string) (*OTPKeyData, error) {
8486
}
8587

8688
return &OTPKeyData{
87-
OTP: NewHOTPHash(key, counter, int(digits), -1, algorithm),
88-
Label: label,
89-
Issuer: issuer}, nil
89+
OTP: NewHOTPHash(key, counter, int(digits), DefaultTransactionOffset, algorithm),
90+
Account: accountName,
91+
Issuer: issuer}, nil
9092
}
9193

9294
// NewDefaultHOTP crates an instance of Hotp with default parameters:
9395
// Number of OTP digits is 6, SHA1 for hashing and using dynamic truncation offset
9496
//
9597
// key is the shared secret key
9698
func NewDefaultHOTP(key []byte, counter int64) *HOTP {
97-
return NewHOTP(key, counter, DefaultDigits, -1)
99+
return NewHOTP(key, counter, DefaultDigits, DefaultTransactionOffset)
98100
}
99101

100102
// NewHOTPDigits creates an instance of Hotp with given number of digits for the OTP
@@ -103,7 +105,7 @@ func NewDefaultHOTP(key []byte, counter int64) *HOTP {
103105
// key is the shared secret key
104106
// digits is the number of digits in the resulting one-time password code
105107
func NewHOTPDigits(key []byte, counter int64, digits int) *HOTP {
106-
return NewHOTP(key, counter, digits, -1)
108+
return NewHOTP(key, counter, digits, DefaultTransactionOffset)
107109
}
108110

109111
// NewHOTP allows to create an instance of Hotp and set the parameters

hotp_test.go

+17-11
Original file line numberDiff line numberDiff line change
@@ -79,41 +79,41 @@ func TestHOTPVerify(t *testing.T) {
7979

8080
func TestHotpUrlGenerator(t *testing.T) {
8181
hotp := NewHOTPDigits([]byte("key"), 342, 8)
82-
url := hotp.ProvisioningUri("Example", "test@example.com")
82+
url := hotp.ProvisioningUri("test@example.com", "Issuer")
8383

84-
expected := "otpauth://hotp/test@example.com:Example?counter=342&digits=8&issuer=test%40example.com&secret=NNSXS"
84+
expected := "otpauth://hotp/Issuer:test@example.com?counter=342&digits=8&issuer=Issuer&secret=NNSXS"
8585
if url != expected {
8686
t.Errorf("Invalid url generated.\nExpected: %s\n Actual: %s", expected, url)
8787
}
8888

8989
hotp = NewDefaultHOTP([]byte("key"), 2342)
90-
url = hotp.ProvisioningUri("Example", "test@example.com")
90+
url = hotp.ProvisioningUri("test@example.com", "Issuer")
9191

92-
expected = "otpauth://hotp/test@example.com:Example?counter=2342&issuer=test%40example.com&secret=NNSXS"
92+
expected = "otpauth://hotp/Issuer:test@example.com?counter=2342&issuer=Issuer&secret=NNSXS"
9393
if url != expected {
9494
t.Errorf("Invalid url generated.\nExpected: %s\n Actual: %s", expected, url)
9595
}
9696

9797
hotp = NewHOTPHash([]byte("key"), 2342, DefaultDigits, -1, crypto.SHA512)
98-
url = hotp.ProvisioningUri("Example", "test@example.com")
98+
url = hotp.ProvisioningUri("test@example.com", "Issuer")
9999

100-
expected = "otpauth://hotp/test@example.com:Example?algorithm=SHA512&counter=2342&issuer=test%40example.com&secret=NNSXS"
100+
expected = "otpauth://hotp/Issuer:test@example.com?algorithm=SHA512&counter=2342&issuer=Issuer&secret=NNSXS"
101101
if url != expected {
102102
t.Errorf("Invalid url generated.\nExpected: %s\n Actual: %s", expected, url)
103103
}
104104
}
105105

106106
func TestHotpUrlParser(t *testing.T) {
107-
data, err := NewHOTPFromUri("otpauth://hotp/test1@example.com:Example?digits=8&issuer=test%40example.com&secret=NNSXS&counter=10")
107+
data, err := NewHOTPFromUri("otpauth://hotp/test1@example.com?digits=8&issuer=Issuer&secret=NNSXS&counter=10")
108108
if err != nil {
109109
t.Error(err)
110110
}
111111
otp := data.OTP.(*HOTP)
112112

113-
if data.Label != "Example" {
113+
if data.Issuer != "Issuer" {
114114
t.Errorf("Error parsing label from URL")
115115
}
116-
if data.Issuer != "test@example.com" {
116+
if data.Account != "test1@example.com" {
117117
t.Errorf("Error parsing issuer from URL")
118118
}
119119
if !reflect.DeepEqual(otp.Secret, []byte("key")) {
@@ -126,7 +126,7 @@ func TestHotpUrlParser(t *testing.T) {
126126
t.Errorf("Error setting counter from URL")
127127
}
128128

129-
data, err = NewHOTPFromUri("otpauth://hotp/test@example.com:Example?issuer=test%40example.com&counter=45&secret=NNSXS")
129+
data, err = NewHOTPFromUri("otpauth://hotp/Issuer:test@example.com:Example?issuer=Overriden&counter=45&secret=NNSXS")
130130
if err != nil {
131131
t.Error(err)
132132
}
@@ -137,12 +137,18 @@ func TestHotpUrlParser(t *testing.T) {
137137
if otp.GetCounter() != 45 {
138138
t.Errorf("Error parsing counter from URL")
139139
}
140+
if data.Issuer != "Overriden" {
141+
t.Errorf("Error parsing label from URL")
142+
}
140143

141-
data, err = NewHOTPFromUri("otpauth://hotp/test@example.com:Example?algorithm=SHA512&counter=10&issuer=test%40example.com&secret=NNSXS")
144+
data, err = NewHOTPFromUri("otpauth://hotp/Issuer:test@example.com?algorithm=SHA512&counter=10&secret=NNSXS")
142145
if err != nil {
143146
t.Error(err)
144147
}
145148
otp = data.OTP.(*HOTP)
149+
if data.Issuer != "Issuer" {
150+
t.Errorf("Error parsing label from URL")
151+
}
146152
if otp.GetHash() != crypto.SHA512 {
147153
t.Errorf("Error parsing time step from URL")
148154
}

otp.go

+35-16
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,22 @@ type OTP interface {
4949
type OTPKeyData struct {
5050
// OTP implementation, either *HOTP or *TOTP
5151
OTP OTP
52-
// Label contains user-visible label for the OTP
53-
Label string
52+
// Account contains the account Id for the OTP
53+
Account string
5454
// Issuer contains the name of the issuer of the OTP
5555
Issuer string
5656
}
5757

58+
// GetLabelRepr returns OTP label in human readable format
59+
// DO NOT use it for constructing otpauth: URIs
60+
func (okd *OTPKeyData) GetLabelRepr() string {
61+
if okd.Issuer != "" {
62+
return fmt.Sprintf("%s:%s", okd.Issuer, okd.Account)
63+
} else {
64+
return okd.Account
65+
}
66+
}
67+
5868
func adjustForHash(key []byte, algorithm crypto.Hash) []byte {
5969
hash := algorithm.New()
6070
if len(key) > hash.BlockSize() { // if key is longer than block size
@@ -67,25 +77,26 @@ func adjustForHash(key []byte, algorithm crypto.Hash) []byte {
6777
return key
6878
}
6979

70-
// getLabelIssuer extracts label and issuer name from the URL
71-
// label is populated from URL's account name
80+
// getAccountIssuer extracts account name and issuer name from the URL
81+
// returns (Account Name, Issuer)
82+
// account name is populated from URL's label
7283
// if label contains issuer separated from the account name with ':', then issuer is extracted from the label
7384
// if URL contains 'issuer' parameter then it overrides any other issuer value set previously
74-
func getLabelIssuer(u *url.URL) (string, string) {
75-
label := u.Path[1:] // skip '/'
85+
func getAccountIssuer(u *url.URL) (string, string) {
86+
accountName := u.Path[1:] // skip '/'
7687
var labelIssuer string
77-
if strings.Contains(label, ":") {
78-
lbl := strings.Split(label, ":")
88+
if strings.Contains(accountName, ":") {
89+
lbl := strings.Split(accountName, ":")
7990
labelIssuer = lbl[0]
80-
label = lbl[1]
91+
accountName = lbl[1]
8192
}
8293
var issuer string
8394
if u.Query().Has(issuerKey) {
8495
issuer = u.Query().Get(issuerKey)
8596
} else {
8697
issuer = labelIssuer
8798
}
88-
return label, issuer
99+
return accountName, issuer
89100
}
90101

91102
// EncodeKey converts a key to Base32 representation
@@ -136,17 +147,25 @@ const (
136147
SHA1 = "SHA1"
137148
)
138149

139-
func generateProvisioningUri(otpType string, accountName string, issuer string, digits int, key []byte, extra url.Values) string {
140-
extra.Add(secretKey, EncodeKey(key))
141-
extra.Add(issuerKey, issuer)
150+
func generateProvisioningUri(otpType string, accountName string, issuer string, digits int, key []byte, params url.Values) string {
151+
params.Add(secretKey, EncodeKey(key))
152+
if issuer != "" {
153+
params.Add(issuerKey, issuer)
154+
}
142155
if digits != DefaultDigits {
143-
extra.Add(digitsKey, fmt.Sprintf("%d", digits))
156+
params.Add(digitsKey, fmt.Sprintf("%d", digits))
157+
}
158+
var label string
159+
if issuer != "" {
160+
label = url.PathEscape(issuer) + ":" + url.PathEscape(accountName)
161+
} else {
162+
label = url.PathEscape(accountName)
144163
}
145164
u := url.URL{
146165
Scheme: otpAuthSheme,
147166
Host: otpType,
148-
Path: url.PathEscape(issuer) + ":" + url.PathEscape(accountName),
149-
RawQuery: extra.Encode(),
167+
Path: label,
168+
RawQuery: params.Encode(),
150169
}
151170
return u.String()
152171
}

topt_test.go

+21-15
Original file line numberDiff line numberDiff line change
@@ -85,25 +85,25 @@ func TestTOTPVerifyWithWindow(t *testing.T) {
8585

8686
func TestTotpUrlGenerator(t *testing.T) {
8787
totp := NewTOTPDigits([]byte("key"), 8)
88-
url := totp.ProvisioningUri("Example", "test@example.com")
88+
url := totp.ProvisioningUri("test@example.com", "Issuer")
8989

90-
expected := "otpauth://totp/test@example.com:Example?digits=8&issuer=test%40example.com&secret=NNSXS"
90+
expected := "otpauth://totp/Issuer:test@example.com?digits=8&issuer=Issuer&secret=NNSXS"
9191
if url != expected {
9292
t.Errorf("Invalid url generated.\nExpected: %s\n Actual: %s", expected, url)
9393
}
9494

9595
totp = NewDefaultTOTP([]byte("key"))
96-
url = totp.ProvisioningUri("Example", "test@example.com")
96+
url = totp.ProvisioningUri("test@example.com", "Issuer")
9797

98-
expected = "otpauth://totp/test@example.com:Example?issuer=test%40example.com&secret=NNSXS"
98+
expected = "otpauth://totp/Issuer:test@example.com?issuer=Issuer&secret=NNSXS"
9999
if url != expected {
100100
t.Errorf("Invalid url generated.\nExpected: %s\n Actual: %s", expected, url)
101101
}
102102

103103
totp = NewTOTP([]byte("key"), 8, 45, 0)
104-
url = totp.ProvisioningUri("Example", "test@example.com")
104+
url = totp.ProvisioningUri("test@example.com", "Issuer")
105105

106-
expected = "otpauth://totp/test@example.com:Example?digits=8&issuer=test%40example.com&period=45&secret=NNSXS"
106+
expected = "otpauth://totp/Issuer:test@example.com?digits=8&issuer=Issuer&period=45&secret=NNSXS"
107107
if url != expected {
108108
t.Errorf("Invalid url generated.\nExpected: %s\n Actual: %s", expected, url)
109109
}
@@ -118,16 +118,16 @@ func TestTotpUrlGenerator(t *testing.T) {
118118
}
119119

120120
func TestTotpUrlParser(t *testing.T) {
121-
data, err := NewTOTPFromUri("otpauth://totp/test1@example.com:Example?digits=8&issuer=test%40example.com&secret=NNSXS")
121+
data, err := NewTOTPFromUri("otpauth://totp/test1@example.com?digits=8&issuer=Issuer&secret=NNSXS")
122122
if err != nil {
123123
t.Error(err)
124124
}
125125
otp := data.OTP.(*TOTP)
126126

127-
if data.Label != "Example" {
127+
if data.Account != "test1@example.com" {
128128
t.Errorf("Error parsing label from URL")
129129
}
130-
if data.Issuer != "test@example.com" {
130+
if data.Issuer != "Issuer" {
131131
t.Errorf("Error parsing issuer from URL")
132132
}
133133
if !reflect.DeepEqual(otp.Secret, []byte("key")) {
@@ -140,7 +140,7 @@ func TestTotpUrlParser(t *testing.T) {
140140
t.Errorf("Error setting default time step")
141141
}
142142

143-
data, err = NewTOTPFromUri("otpauth://totp/test@example.com:Example?issuer=test%40example.com&period=45&secret=DDINI")
143+
data, err = NewTOTPFromUri("otpauth://totp/Issuer:test@example.com?issuer=Overriden&period=45&secret=DDINI")
144144
if err != nil {
145145
t.Error(err)
146146
}
@@ -151,11 +151,17 @@ func TestTotpUrlParser(t *testing.T) {
151151
if otp.GetTimeStep() != 45 {
152152
t.Errorf("Error parsing time step from URL")
153153
}
154+
if data.Issuer != "Overriden" {
155+
t.Errorf("Error parsing issuer from URL")
156+
}
154157

155-
data, err = NewTOTPFromUri("otpauth://totp/test@example.com:Example?algorithm=SHA256&issuer=test%40example.com&secret=NNSXS")
158+
data, err = NewTOTPFromUri("otpauth://totp/Issuer:test@example.com?algorithm=SHA256&secret=NNSXS")
156159
if err != nil {
157160
t.Error(err)
158161
}
162+
if data.Issuer != "Issuer" {
163+
t.Errorf("Error parsing issuer from URL")
164+
}
159165
otp = data.OTP.(*TOTP)
160166
if otp.GetHash() != crypto.SHA256 {
161167
t.Errorf("Error parsing time step from URL")
@@ -165,18 +171,18 @@ func TestTotpUrlParser(t *testing.T) {
165171
func TestTotpUrlParserErrors(t *testing.T) {
166172
_, err := NewTOTPFromUri("otpauth://hotp/test@example.com:Example?digits=8&issuer=test%40example.com&secret=DDINI")
167173
if err == nil {
168-
t.Errorf("Expected to faile because of invalid otp type")
174+
t.Errorf("Expected to fail because of invalid otp type")
169175
}
170176
_, err = NewTOTPFromUri("not_otpauth://totp/test@example.com:Example?digits=8&issuer=test%40example.com&secret=DDINI")
171177
if err == nil {
172-
t.Errorf("Expected to faile because of invalid URI schema")
178+
t.Errorf("Expected to fail because of invalid URI schema")
173179
}
174180
_, err = NewTOTPFromUri("otpauth://totp/test@example.com:Example?digits=8&issuer=test%40example.com")
175181
if err == nil {
176-
t.Errorf("Expected to faile because of missing secret")
182+
t.Errorf("Expected to fail because of missing secret")
177183
}
178184
_, err = NewTOTPFromUri("otpauth://totp/test@example.com:Example?digits=8&issuer=test%40example.com&secret=X0NNSXS")
179185
if err == nil {
180-
t.Errorf("Expected to faile because of invalid secret")
186+
t.Errorf("Expected to fail because of invalid secret")
181187
}
182188
}

0 commit comments

Comments
 (0)