git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otc/otc.go (about) 1 // package otc provides alphanumeric One Time Codes that can be used for email-based 2FA, 2 // account verification and more. 3 package otc 4 5 import ( 6 "crypto/rand" 7 "encoding/base32" 8 "encoding/base64" 9 "errors" 10 "strings" 11 "unicode" 12 13 "git.sr.ht/~pingoo/stdx/crypto" 14 ) 15 16 const ( 17 otcPrefix = "otc" 18 version1 = "v1" 19 tokenPrefix = otcPrefix + "." + version1 + "." 20 ) 21 22 var ( 23 ErrTokenIsNotValid = errors.New("otc: token is not valid") 24 ) 25 26 type Code struct { 27 code string 28 token string 29 } 30 31 type NewCodeOptions struct { 32 } 33 34 func New(length uint16) (code Code, err error) { 35 randomBytes := make([]byte, length) 36 37 _, err = rand.Read(randomBytes) 38 if err != nil { 39 return 40 } 41 42 codeText := base32.StdEncoding.EncodeToString(randomBytes) 43 // format code (with '-') 44 45 hash, err := crypto.HashPassword([]byte(codeText), crypto.DefaultHashPasswordParams) 46 if err != nil { 47 return 48 } 49 50 encodedHash := base64.RawURLEncoding.EncodeToString([]byte(hash)) 51 52 code = Code{ 53 code: codeText, 54 token: tokenPrefix + encodedHash, 55 } 56 57 return 58 } 59 60 func (code *Code) Code() string { 61 return code.code 62 } 63 64 // CodeHTML returns the code wrapped in a <span> and with numbers wrapped in <span style="color: red"> 65 func (code *Code) CodeHTML() (ret string) { 66 ret = "<span>" 67 for _, c := range code.code { 68 if unicode.IsLetter(c) || c == '-' { 69 ret += string(c) 70 } else { 71 ret += `<span style="color: red">` 72 ret += string(c) 73 ret += `</span>` 74 } 75 } 76 77 ret += "</span>" 78 return 79 } 80 81 // Token returns a token of the form otc.v[N].[XXXX] 82 // where [N] is the version number of the token 83 // and [XXXX] is Base64URL encoded data 84 // The token should be stored in a database or a similar secure place 85 // and use it later to verify that a code is valid 86 func (code *Code) Token() string { 87 return code.token 88 } 89 90 func Verify(code, token string) bool { 91 if strings.Count(token, ".") != 2 { 92 return false 93 } 94 if !strings.HasPrefix(token, tokenPrefix) { 95 return false 96 } 97 98 encodedHashStart := strings.LastIndexByte(token, '.') 99 encodedHash := token[encodedHashStart+1:] 100 hash, err := base64.RawURLEncoding.DecodeString(encodedHash) 101 if err != nil { 102 return false 103 } 104 105 // TODO: cleanup code from '_' 106 107 return crypto.VerifyPasswordHash([]byte(code), string(hash)) 108 }