github.com/greenpau/go-authcrunch@v1.1.4/pkg/identity/qr/qr.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package qr 16 17 import ( 18 "encoding/base32" 19 "encoding/base64" 20 "fmt" 21 "net/url" 22 "strings" 23 ) 24 25 // Code holds the data associated with a QR code. 26 type Code struct { 27 Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty"` 28 Secret string `json:"secret,omitempty" xml:"secret,omitempty" yaml:"secret,omitempty"` 29 Algorithm string `json:"algorithm,omitempty" xml:"algorithm,omitempty" yaml:"algorithm,omitempty"` 30 Label string `json:"label,omitempty" xml:"label,omitempty" yaml:"label,omitempty"` 31 Issuer string `json:"issuer,omitempty" xml:"issuer,omitempty" yaml:"issuer,omitempty"` 32 Period int `json:"period,omitempty" xml:"period,omitempty" yaml:"period,omitempty"` 33 Digits int `json:"digits,omitempty" xml:"digits,omitempty" yaml:"digits,omitempty"` 34 Counter int `json:"counter,omitempty" xml:"counter,omitempty" yaml:"counter,omitempty"` 35 text string 36 encoded string 37 } 38 39 // NewCode returns an instance of Code. 40 func NewCode() *Code { 41 return &Code{} 42 } 43 44 func (c *Code) validate() error { 45 if c.Label == "" { 46 return fmt.Errorf("token label must be set") 47 } 48 if c.Secret == "" { 49 return fmt.Errorf("token secret must be set") 50 } 51 if len(c.Secret) < 6 { 52 return fmt.Errorf("token secret must be at least 6 characters long") 53 } 54 if c.Digits == 0 { 55 c.Digits = 6 56 } else { 57 if c.Digits < 4 || c.Digits > 8 { 58 return fmt.Errorf("digits must be between 4 and 8 numbers long") 59 } 60 } 61 if c.Period == 0 { 62 c.Period = 30 63 } else { 64 if c.Period < 30 || c.Period > 180 { 65 return fmt.Errorf("token period must be between 30 and 180 seconds") 66 } 67 } 68 switch c.Type { 69 case "totp": 70 case "hotp": 71 if c.Counter < 1 { 72 return fmt.Errorf("hotp token counter must be set") 73 } 74 default: 75 return fmt.Errorf("token type must be either totp or hotp") 76 } 77 78 c.Algorithm = strings.ToLower(c.Algorithm) 79 switch c.Algorithm { 80 case "sha1", "sha256", "sha512": 81 case "": 82 default: 83 return fmt.Errorf("token algo must be SHA1, SHA256, or SHA512") 84 } 85 86 return nil 87 } 88 89 // Build validates and build QR code. 90 func (c *Code) Build() error { 91 if err := c.validate(); err != nil { 92 return err 93 } 94 var sb strings.Builder 95 sb.WriteString("otpauth://") 96 sb.WriteString(c.Type + "/" + url.QueryEscape(c.Label)) 97 secretEncoder := base32.StdEncoding.WithPadding(base32.NoPadding) 98 sb.WriteString("?secret=" + secretEncoder.EncodeToString([]byte(c.Secret))) 99 if c.Issuer != "" { 100 sb.WriteString("&issuer=" + url.QueryEscape(c.Issuer)) 101 } 102 if c.Algorithm != "" { 103 sb.WriteString("&algorithm=" + c.Algorithm) 104 } 105 if c.Digits > 0 { 106 sb.WriteString(fmt.Sprintf("&digits=%d", c.Digits)) 107 } 108 if c.Counter > 0 { 109 sb.WriteString(fmt.Sprintf("&counter=%d", c.Counter)) 110 } 111 if c.Period > 0 { 112 sb.WriteString(fmt.Sprintf("&period=%d", c.Period)) 113 } 114 115 c.text = sb.String() 116 c.encoded = base64.StdEncoding.EncodeToString([]byte(c.text)) 117 return nil 118 } 119 120 // Get return QR code. 121 func (c *Code) Get() string { 122 return c.text 123 } 124 125 // GetEncoded returns base64-encoded QR code. 126 func (c *Code) GetEncoded() string { 127 return c.encoded 128 }