git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otp/otp.go (about)

     1  /**
     2   *  Copyright 2014 Paul Querna
     3   *
     4   *  Licensed under the Apache License, Version 2.0 (the "License");
     5   *  you may not use this file except in compliance with the License.
     6   *  You may obtain a copy of the License at
     7   *
     8   *      http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   *  Unless required by applicable law or agreed to in writing, software
    11   *  distributed under the License is distributed on an "AS IS" BASIS,
    12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   *  See the License for the specific language governing permissions and
    14   *  limitations under the License.
    15   *
    16   */
    17  
    18  package otp
    19  
    20  import (
    21  	"crypto/md5"
    22  	"crypto/sha1"
    23  	"crypto/sha512"
    24  	"errors"
    25  	"fmt"
    26  	"hash"
    27  	"image"
    28  	"net/url"
    29  	"strconv"
    30  	"strings"
    31  
    32  	"crypto/sha256"
    33  
    34  	"git.sr.ht/~pingoo/stdx/barcode"
    35  	"git.sr.ht/~pingoo/stdx/barcode/qr"
    36  )
    37  
    38  // Error when attempting to convert the secret from base32 to raw bytes.
    39  var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.")
    40  
    41  // The user provided passcode length was not expected.
    42  var ErrValidateInputInvalidLength = errors.New("Input length unexpected")
    43  
    44  // When generating a Key, the Issuer must be set.
    45  var ErrGenerateMissingIssuer = errors.New("Issuer must be set")
    46  
    47  // When generating a Key, the Account Name must be set.
    48  var ErrGenerateMissingAccountName = errors.New("AccountName must be set")
    49  
    50  // Key represents an TOTP or HTOP key.
    51  type Key struct {
    52  	orig string
    53  	url  *url.URL
    54  }
    55  
    56  // NewKeyFromURL creates a new Key from an TOTP or HOTP url.
    57  //
    58  // The URL format is documented here:
    59  //
    60  //	https://github.com/google/google-authenticator/wiki/Key-Uri-Format
    61  func NewKeyFromURL(orig string) (*Key, error) {
    62  	s := strings.TrimSpace(orig)
    63  
    64  	u, err := url.Parse(s)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	return &Key{
    70  		orig: s,
    71  		url:  u,
    72  	}, nil
    73  }
    74  
    75  func (k *Key) String() string {
    76  	return k.orig
    77  }
    78  
    79  // QrCode returns an QR-Code image of the specified width and height,
    80  // suitable for use by many clients like Google-Authenricator
    81  // to enroll a user's TOTP/HOTP key.
    82  func (k *Key) QrCode(width int, height int) (image.Image, error) {
    83  	b, err := qr.Encode(k.orig, qr.M, qr.Auto)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	b, err = barcode.Scale(b, width, height)
    89  
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	return b, nil
    95  }
    96  
    97  // Type returns "hotp" or "totp".
    98  func (k *Key) Type() string {
    99  	return k.url.Host
   100  }
   101  
   102  // Issuer returns the name of the issuing organization.
   103  func (k *Key) Issuer() string {
   104  	q := k.url.Query()
   105  
   106  	issuer := q.Get("issuer")
   107  
   108  	if issuer != "" {
   109  		return issuer
   110  	}
   111  
   112  	p := strings.TrimPrefix(k.url.Path, "/")
   113  	i := strings.Index(p, ":")
   114  
   115  	if i == -1 {
   116  		return ""
   117  	}
   118  
   119  	return p[:i]
   120  }
   121  
   122  // AccountName returns the name of the user's account.
   123  func (k *Key) AccountName() string {
   124  	p := strings.TrimPrefix(k.url.Path, "/")
   125  	i := strings.Index(p, ":")
   126  
   127  	if i == -1 {
   128  		return p
   129  	}
   130  
   131  	return p[i+1:]
   132  }
   133  
   134  // Secret returns the opaque secret for this Key.
   135  func (k *Key) Secret() string {
   136  	q := k.url.Query()
   137  
   138  	return q.Get("secret")
   139  }
   140  
   141  // Period returns a tiny int representing the rotation time in seconds.
   142  func (k *Key) Period() uint64 {
   143  	q := k.url.Query()
   144  
   145  	if u, err := strconv.ParseUint(q.Get("period"), 10, 64); err == nil {
   146  		return u
   147  	}
   148  
   149  	// If no period is defined 30 seconds is the default per (rfc6238)
   150  	return 30
   151  }
   152  
   153  // Digits returns a tiny int representing the number of OTP digits.
   154  func (k *Key) Digits() Digits {
   155  	q := k.url.Query()
   156  
   157  	if u, err := strconv.ParseUint(q.Get("digits"), 10, 64); err == nil {
   158  		switch u {
   159  		case 8:
   160  			return DigitsEight
   161  		default:
   162  			return DigitsSix
   163  		}
   164  	}
   165  
   166  	// Six is the most common value.
   167  	return DigitsSix
   168  }
   169  
   170  // Algorithm returns the algorithm used or the default (SHA1).
   171  func (k *Key) Algorithm() Algorithm {
   172  	q := k.url.Query()
   173  
   174  	a := strings.ToLower(q.Get("algorithm"))
   175  	switch a {
   176  	case "md5":
   177  		return AlgorithmMD5
   178  	case "sha256":
   179  		return AlgorithmSHA256
   180  	case "sha512":
   181  		return AlgorithmSHA512
   182  	default:
   183  		return AlgorithmSHA1
   184  	}
   185  }
   186  
   187  // URL returns the OTP URL as a string
   188  func (k *Key) URL() string {
   189  	return k.url.String()
   190  }
   191  
   192  // Algorithm represents the hashing function to use in the HMAC
   193  // operation needed for OTPs.
   194  type Algorithm int
   195  
   196  const (
   197  	// AlgorithmSHA1 should be used for compatibility with Google Authenticator.
   198  	//
   199  	// See https://git.sr.ht/~pingoo/stdx/otp/issues/55 for additional details.
   200  	AlgorithmSHA1 Algorithm = iota
   201  	AlgorithmSHA256
   202  	AlgorithmSHA512
   203  	AlgorithmMD5
   204  )
   205  
   206  func (a Algorithm) String() string {
   207  	switch a {
   208  	case AlgorithmSHA1:
   209  		return "SHA1"
   210  	case AlgorithmSHA256:
   211  		return "SHA256"
   212  	case AlgorithmSHA512:
   213  		return "SHA512"
   214  	case AlgorithmMD5:
   215  		return "MD5"
   216  	}
   217  	panic("unreached")
   218  }
   219  
   220  func (algo Algorithm) Hash() hash.Hash {
   221  	switch algo {
   222  	case AlgorithmSHA1:
   223  		return sha1.New()
   224  	case AlgorithmSHA256:
   225  		return sha256.New()
   226  	case AlgorithmSHA512:
   227  		return sha512.New()
   228  	case AlgorithmMD5:
   229  		return md5.New()
   230  	}
   231  	panic("unreached")
   232  }
   233  
   234  // Digits represents the number of digits present in the
   235  // user's OTP passcode. Six and Eight are the most common values.
   236  type Digits int
   237  
   238  const (
   239  	DigitsSix   Digits = 6
   240  	DigitsEight Digits = 8
   241  )
   242  
   243  // Format converts an integer into the zero-filled size for this Digits.
   244  func (d Digits) Format(in int32) string {
   245  	f := fmt.Sprintf("%%0%dd", d)
   246  	return fmt.Sprintf(f, in)
   247  }
   248  
   249  // Length returns the number of characters for this Digits.
   250  func (d Digits) Length() int {
   251  	return int(d)
   252  }
   253  
   254  func (d Digits) String() string {
   255  	return fmt.Sprintf("%d", d)
   256  }