github.com/greenpau/go-authcrunch@v1.1.4/pkg/identity/mfa_token.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 identity
    16  
    17  import (
    18  	"crypto/ecdsa"
    19  	"crypto/elliptic"
    20  	"crypto/hmac"
    21  	"crypto/rsa"
    22  	"crypto/sha1"
    23  	"crypto/sha256"
    24  	"crypto/sha512"
    25  	"crypto/subtle"
    26  	"crypto/x509"
    27  	"encoding/base64"
    28  	"encoding/binary"
    29  	"encoding/json"
    30  	"fmt"
    31  	"hash"
    32  	"math"
    33  	"math/big"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/greenpau/go-authcrunch/pkg/errors"
    38  	"github.com/greenpau/go-authcrunch/pkg/requests"
    39  	"github.com/greenpau/go-authcrunch/pkg/tagging"
    40  	"github.com/greenpau/go-authcrunch/pkg/util"
    41  )
    42  
    43  // MfaTokenBundle is a collection of public keys.
    44  type MfaTokenBundle struct {
    45  	tokens []*MfaToken
    46  	size   int
    47  }
    48  
    49  // MfaToken is a puiblic key in a public-private key pair.
    50  type MfaToken struct {
    51  	ID               string            `json:"id,omitempty" xml:"id,omitempty" yaml:"id,omitempty"`
    52  	Type             string            `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty"`
    53  	Algorithm        string            `json:"algorithm,omitempty" xml:"algorithm,omitempty" yaml:"algorithm,omitempty"`
    54  	Comment          string            `json:"comment,omitempty" xml:"comment,omitempty" yaml:"comment,omitempty"`
    55  	Description      string            `json:"description,omitempty" xml:"description,omitempty" yaml:"description,omitempty"`
    56  	Secret           string            `json:"secret,omitempty" xml:"secret,omitempty" yaml:"secret,omitempty"`
    57  	Period           int               `json:"period,omitempty" xml:"period,omitempty" yaml:"period,omitempty"`
    58  	Digits           int               `json:"digits,omitempty" xml:"digits,omitempty" yaml:"digits,omitempty"`
    59  	Expired          bool              `json:"expired,omitempty" xml:"expired,omitempty" yaml:"expired,omitempty"`
    60  	ExpiredAt        time.Time         `json:"expired_at,omitempty" xml:"expired_at,omitempty" yaml:"expired_at,omitempty"`
    61  	CreatedAt        time.Time         `json:"created_at,omitempty" xml:"created_at,omitempty" yaml:"created_at,omitempty"`
    62  	Disabled         bool              `json:"disabled,omitempty" xml:"disabled,omitempty" yaml:"disabled,omitempty"`
    63  	DisabledAt       time.Time         `json:"disabled_at,omitempty" xml:"disabled_at,omitempty" yaml:"disabled_at,omitempty"`
    64  	Device           *MfaDevice        `json:"device,omitempty" xml:"device,omitempty" yaml:"device,omitempty"`
    65  	Parameters       map[string]string `json:"parameters,omitempty" xml:"parameters,omitempty" yaml:"parameters,omitempty"`
    66  	Flags            map[string]bool   `json:"flags,omitempty" xml:"flags,omitempty" yaml:"flags,omitempty"`
    67  	SignatureCounter uint32            `json:"signature_counter,omitempty" xml:"signature_counter,omitempty" yaml:"signature_counter,omitempty"`
    68  	Tags             []tagging.Tag     `json:"tags,omitempty" xml:"tags,omitempty" yaml:"tags,omitempty"`
    69  	Labels           []string          `json:"labels,omitempty" xml:"labels,omitempty" yaml:"labels,omitempty"`
    70  
    71  	pubkeyECDSA *ecdsa.PublicKey
    72  	pubkeyRSA   *rsa.PublicKey
    73  }
    74  
    75  // MfaDevice is the hardware device associated with MfaToken.
    76  type MfaDevice struct {
    77  	Name   string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"`
    78  	Vendor string `json:"vendor,omitempty" xml:"vendor,omitempty" yaml:"vendor,omitempty"`
    79  	Type   string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty"`
    80  }
    81  
    82  // NewMfaTokenBundle returns an instance of MfaTokenBundle.
    83  func NewMfaTokenBundle() *MfaTokenBundle {
    84  	return &MfaTokenBundle{
    85  		tokens: []*MfaToken{},
    86  	}
    87  }
    88  
    89  // Add adds MfaToken to MfaTokenBundle.
    90  func (b *MfaTokenBundle) Add(k *MfaToken) {
    91  	b.tokens = append(b.tokens, k)
    92  	b.size++
    93  }
    94  
    95  // Get returns MfaToken instances of the MfaTokenBundle.
    96  func (b *MfaTokenBundle) Get() []*MfaToken {
    97  	return b.tokens
    98  }
    99  
   100  // Size returns the number of MfaToken instances in MfaTokenBundle.
   101  func (b *MfaTokenBundle) Size() int {
   102  	return b.size
   103  }
   104  
   105  // NewMfaToken returns an instance of MfaToken.
   106  func NewMfaToken(req *requests.Request) (*MfaToken, error) {
   107  	p := &MfaToken{
   108  		ID:          util.GetRandomString(40),
   109  		CreatedAt:   time.Now().UTC(),
   110  		Parameters:  make(map[string]string),
   111  		Flags:       make(map[string]bool),
   112  		Comment:     req.MfaToken.Comment,
   113  		Type:        req.MfaToken.Type,
   114  		Description: req.MfaToken.Description,
   115  		Tags:        req.MfaToken.Tags,
   116  		Labels:      req.MfaToken.Labels,
   117  	}
   118  
   119  	if req.MfaToken.Disabled {
   120  		p.Disabled = true
   121  		p.DisabledAt = time.Now().UTC()
   122  	}
   123  
   124  	switch p.Type {
   125  	case "totp":
   126  		// Shared Secret
   127  		p.Secret = req.MfaToken.Secret
   128  		// Algorithm
   129  		p.Algorithm = strings.ToLower(req.MfaToken.Algorithm)
   130  		switch p.Algorithm {
   131  		case "sha1", "sha256", "sha512":
   132  		case "":
   133  			p.Algorithm = "sha1"
   134  		default:
   135  			return nil, errors.ErrMfaTokenInvalidAlgorithm.WithArgs(p.Algorithm)
   136  		}
   137  		req.MfaToken.Algorithm = p.Algorithm
   138  
   139  		// Period
   140  		p.Period = req.MfaToken.Period
   141  		if p.Period < 30 || p.Period > 300 {
   142  			return nil, errors.ErrMfaTokenInvalidPeriod.WithArgs(p.Period)
   143  		}
   144  		// Digits
   145  		p.Digits = req.MfaToken.Digits
   146  		if p.Digits == 0 {
   147  			p.Digits = 6
   148  		}
   149  		if p.Digits < 4 || p.Digits > 8 {
   150  			return nil, errors.ErrMfaTokenInvalidDigits.WithArgs(p.Digits)
   151  		}
   152  		// Codes
   153  		if !req.MfaToken.SkipVerification {
   154  			if err := p.ValidateCodeWithTime(req.MfaToken.Passcode, time.Now().Add(-time.Second*time.Duration(p.Period)).UTC()); err != nil {
   155  				return nil, err
   156  			}
   157  		}
   158  	case "u2f":
   159  		r := &WebAuthnRegisterRequest{}
   160  		if req.WebAuthn.Register == "" {
   161  			return nil, errors.ErrWebAuthnRegisterNotFound
   162  		}
   163  		if req.WebAuthn.Challenge == "" {
   164  			return nil, errors.ErrWebAuthnChallengeNotFound
   165  		}
   166  
   167  		// Decode WebAuthn Register.
   168  		decoded, err := base64.StdEncoding.DecodeString(req.WebAuthn.Register)
   169  		if err != nil {
   170  			return nil, errors.ErrWebAuthnParse.WithArgs(err)
   171  		}
   172  		if err := json.Unmarshal([]byte(decoded), r); err != nil {
   173  			return nil, errors.ErrWebAuthnParse.WithArgs(err)
   174  		}
   175  		// Set WebAuthn Challenge as Secret.
   176  		p.Secret = req.WebAuthn.Challenge
   177  
   178  		if r.ID == "" {
   179  			return nil, errors.ErrWebAuthnEmptyRegisterID
   180  		}
   181  
   182  		switch r.Type {
   183  		case "public-key":
   184  		case "":
   185  			return nil, errors.ErrWebAuthnEmptyRegisterKeyType
   186  		default:
   187  			return nil, errors.ErrWebAuthnInvalidRegisterKeyType.WithArgs(r.Type)
   188  		}
   189  
   190  		for _, tr := range r.Transports {
   191  			switch tr {
   192  			case "usb":
   193  			case "nfc":
   194  			case "ble":
   195  			case "internal":
   196  			case "":
   197  				return nil, errors.ErrWebAuthnEmptyRegisterTransport
   198  			default:
   199  				return nil, errors.ErrWebAuthnInvalidRegisterTransport.WithArgs(tr)
   200  			}
   201  		}
   202  
   203  		if r.AttestationObject == nil {
   204  			return nil, errors.ErrWebAuthnRegisterAttestationObjectNotFound
   205  		}
   206  		if r.AttestationObject.AuthData == nil {
   207  			return nil, errors.ErrWebAuthnRegisterAuthDataNotFound
   208  		}
   209  
   210  		// Extract rpIdHash from authData.
   211  		if r.AttestationObject.AuthData.RelyingPartyID == "" {
   212  			return nil, errors.ErrWebAuthnRegisterEmptyRelyingPartyID
   213  		}
   214  		p.Parameters["rp_id_hash"] = r.AttestationObject.AuthData.RelyingPartyID
   215  
   216  		// Extract flags from authData.
   217  		if r.AttestationObject.AuthData.Flags == nil {
   218  			return nil, errors.ErrWebAuthnRegisterEmptyFlags
   219  		}
   220  		for k, v := range r.AttestationObject.AuthData.Flags {
   221  			p.Flags[k] = v
   222  		}
   223  
   224  		// Extract signature counter from authData.
   225  		p.SignatureCounter = r.AttestationObject.AuthData.SignatureCounter
   226  
   227  		// Extract public key from credentialData.
   228  		if r.AttestationObject.AuthData.CredentialData == nil {
   229  			return nil, errors.ErrWebAuthnRegisterCredentialDataNotFound
   230  		}
   231  
   232  		if r.AttestationObject.AuthData.CredentialData.PublicKey == nil {
   233  			return nil, errors.ErrWebAuthnRegisterPublicKeyNotFound
   234  		}
   235  
   236  		// See https://www.iana.org/assignments/cose/cose.xhtml#key-type
   237  		var keyType string
   238  		if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["key_type"]; exists {
   239  			switch v.(float64) {
   240  			case 2:
   241  				keyType = "ec2"
   242  			case 3:
   243  				keyType = "rsa"
   244  			default:
   245  				return nil, errors.ErrWebAuthnRegisterPublicKeyUnsupported.WithArgs(v)
   246  			}
   247  		} else {
   248  			return nil, errors.ErrWebAuthnRegisterPublicKeyTypeNotFound
   249  		}
   250  
   251  		// See https://www.iana.org/assignments/cose/cose.xhtml#algorithms
   252  		var keyAlgo string
   253  		if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["algorithm"]; exists {
   254  			switch v.(float64) {
   255  			case -7:
   256  				keyAlgo = "es256"
   257  			case -257:
   258  				keyAlgo = "rs256"
   259  			default:
   260  				return nil, errors.ErrWebAuthnRegisterPublicKeyAlgorithmUnsupported.WithArgs(v)
   261  			}
   262  		} else {
   263  			return nil, errors.ErrWebAuthnRegisterPublicKeyAlgorithmNotFound
   264  		}
   265  
   266  		switch keyType {
   267  		case "ec2":
   268  			switch keyAlgo {
   269  			case "es256":
   270  				// See https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
   271  				var curveType, curveXcoord, curveYcoord string
   272  				if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["curve_type"]; exists {
   273  					switch v.(float64) {
   274  					case 1:
   275  						curveType = "p256"
   276  					default:
   277  						return nil, errors.ErrWebAuthnRegisterPublicKeyCurveUnsupported.WithArgs(v)
   278  					}
   279  				}
   280  				if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["curve_x"]; exists {
   281  					curveXcoord = v.(string)
   282  				}
   283  				if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["curve_y"]; exists {
   284  					curveYcoord = v.(string)
   285  				}
   286  				p.Parameters["curve_type"] = curveType
   287  				p.Parameters["curve_xcoord"] = curveXcoord
   288  				p.Parameters["curve_ycoord"] = curveYcoord
   289  			default:
   290  				return nil, errors.ErrWebAuthnRegisterPublicKeyTypeAlgorithmUnsupported.WithArgs(keyType, keyAlgo)
   291  			}
   292  		default:
   293  			if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["exponent"]; exists {
   294  				p.Parameters["exponent"] = v.(string)
   295  			} else {
   296  				return nil, errors.ErrWebAuthnRegisterPublicKeyParamNotFound.WithArgs(keyType, keyAlgo, "exponent")
   297  			}
   298  			if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["modulus"]; exists {
   299  				p.Parameters["modulus"] = v.(string)
   300  			} else {
   301  				return nil, errors.ErrWebAuthnRegisterPublicKeyParamNotFound.WithArgs(keyType, keyAlgo, "modulus")
   302  			}
   303  		}
   304  
   305  		p.Parameters["u2f_id"] = r.ID
   306  		p.Parameters["u2f_type"] = r.Type
   307  		p.Parameters["u2f_transports"] = strings.Join(r.Transports, ",")
   308  		p.Parameters["key_type"] = keyType
   309  		p.Parameters["key_algo"] = keyAlgo
   310  		//return nil, fmt.Errorf("XXX: %v", r.AttestationObject.AttestationStatement.Certificates)
   311  		//return nil, fmt.Errorf("XXX: %v", r.AttestationObject.AuthData.CredentialData)
   312  
   313  		if p.Comment == "" {
   314  			p.Comment = fmt.Sprintf("T%d", time.Now().UTC().Unix())
   315  		}
   316  	case "":
   317  		return nil, errors.ErrMfaTokenTypeEmpty
   318  	default:
   319  		return nil, errors.ErrMfaTokenInvalidType.WithArgs(p.Type)
   320  	}
   321  
   322  	return p, nil
   323  }
   324  
   325  // WebAuthnRequest processes WebAuthn requests.
   326  func (p *MfaToken) WebAuthnRequest(payload string) (*WebAuthnAuthenticateRequest, error) {
   327  	switch p.Type {
   328  	case "u2f":
   329  	default:
   330  		return nil, errors.ErrWebAuthnRequest.WithArgs("unsupported token type")
   331  	}
   332  
   333  	for _, reqParam := range []string{"u2f_id", "key_type"} {
   334  		if _, exists := p.Parameters[reqParam]; !exists {
   335  			return nil, errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   336  		}
   337  	}
   338  
   339  	switch p.Parameters["key_type"] {
   340  	case "ec2":
   341  		if p.pubkeyECDSA == nil {
   342  			if err := p.derivePublicKey(p.Parameters); err != nil {
   343  				return nil, err
   344  			}
   345  		}
   346  	case "rsa":
   347  		if p.pubkeyRSA == nil {
   348  			if err := p.derivePublicKey(p.Parameters); err != nil {
   349  				return nil, err
   350  			}
   351  		}
   352  	default:
   353  		return nil, errors.ErrWebAuthnRequest.WithArgs("unsupported key type")
   354  	}
   355  
   356  	decoded, err := base64.StdEncoding.DecodeString(payload)
   357  	if err != nil {
   358  		return nil, errors.ErrWebAuthnParse.WithArgs(err)
   359  	}
   360  
   361  	r := &WebAuthnAuthenticateRequest{}
   362  	if err := json.Unmarshal([]byte(decoded), r); err != nil {
   363  		return nil, errors.ErrWebAuthnParse.WithArgs(err)
   364  	}
   365  
   366  	// Validate key id.
   367  	if p.Parameters["u2f_id"] != r.ID {
   368  		return r, errors.ErrWebAuthnRequest.WithArgs("key id mismatch")
   369  	}
   370  
   371  	// Decode ClientDataJSON.
   372  	if strings.TrimSpace(r.ClientDataEncoded) == "" {
   373  		return r, errors.ErrWebAuthnRequest.WithArgs("encoded client data is empty")
   374  	}
   375  	clientDataBytes, err := base64.StdEncoding.DecodeString(r.ClientDataEncoded)
   376  	if err != nil {
   377  		return r, errors.ErrWebAuthnRequest.WithArgs("failed to decode client data")
   378  	}
   379  	clientData := &ClientData{}
   380  	if err := json.Unmarshal(clientDataBytes, clientData); err != nil {
   381  		return nil, errors.ErrWebAuthnParse.WithArgs("failed to unmarshal client data")
   382  	}
   383  	r.ClientData = clientData
   384  	r.clientDataBytes = clientDataBytes
   385  	clientDataHash := sha256.Sum256(clientDataBytes)
   386  	r.ClientDataEncoded = ""
   387  	if r.ClientData == nil {
   388  		return r, errors.ErrWebAuthnRequest.WithArgs("client data is nil")
   389  	}
   390  
   391  	// Decode Signature.
   392  	if strings.TrimSpace(r.SignatureEncoded) == "" {
   393  		return r, errors.ErrWebAuthnRequest.WithArgs("encoded signature is empty")
   394  	}
   395  	signatureBytes, err := base64.StdEncoding.DecodeString(r.SignatureEncoded)
   396  	if err != nil {
   397  		return r, errors.ErrWebAuthnRequest.WithArgs("failed to decode signature")
   398  	}
   399  	r.signatureBytes = signatureBytes
   400  	r.SignatureEncoded = ""
   401  
   402  	// Decode Authenticator Data.
   403  	// See also https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data
   404  	if strings.TrimSpace(r.AuthDataEncoded) == "" {
   405  		return r, errors.ErrWebAuthnRequest.WithArgs("encoded authenticator data is empty")
   406  	}
   407  	authDataBytes, err := base64.StdEncoding.DecodeString(r.AuthDataEncoded)
   408  	if err != nil {
   409  		return r, errors.ErrWebAuthnRequest.WithArgs("failed to decode auth data")
   410  	}
   411  	if err := r.unpackAuthData(authDataBytes); err != nil {
   412  		return r, errors.ErrWebAuthnRequest.WithArgs(err)
   413  	}
   414  	r.authDataBytes = authDataBytes
   415  	if r.AuthData == nil {
   416  		return r, errors.ErrWebAuthnRequest.WithArgs("auth data is nil")
   417  	}
   418  
   419  	// Verifying an Authentication Assertion
   420  	// See also https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion
   421  
   422  	// Verify that the value of C.type is the string webauthn.get.
   423  	if r.ClientData.Type != "webauthn.get" {
   424  		return r, errors.ErrWebAuthnRequest.WithArgs("client data type is not webauthn.get")
   425  	}
   426  
   427  	// Verify that the value of C.crossOrigin is false.
   428  	if r.ClientData.CrossOrigin {
   429  		return r, errors.ErrWebAuthnRequest.WithArgs("client data cross origin true is not supported")
   430  	}
   431  
   432  	// TODO(greenpau): Verify that the value of C.origin matches the Relying Party's origin.
   433  
   434  	// Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by
   435  	// the Relying Party.
   436  	if r.AuthData.RelyingPartyID != p.Parameters["rp_id_hash"] {
   437  		return r, errors.ErrWebAuthnRequest.WithArgs("rpIdHash mismatch")
   438  	}
   439  
   440  	// Verify that the User Present bit of the flags in authData is set.
   441  	if !r.AuthData.Flags["UP"] {
   442  		return r, errors.ErrWebAuthnRequest.WithArgs("authData User Present bit is not set")
   443  	}
   444  
   445  	// TODO(greenpau): If user verification is required for this assertion, verify that the User
   446  	// Verified bit of the flags in authData is set.
   447  	// This requires checking UV key in p.Flags.
   448  
   449  	// Verify signature.
   450  	signedData := append(authDataBytes, clientDataHash[:]...)
   451  	crt := &x509.Certificate{}
   452  	switch p.Parameters["key_type"] {
   453  	case "ec2":
   454  		crt.PublicKey = p.pubkeyECDSA
   455  	case "rsa":
   456  		crt.PublicKey = p.pubkeyRSA
   457  	}
   458  
   459  	switch p.Parameters["key_algo"] {
   460  	case "es256":
   461  		if err := crt.CheckSignature(x509.ECDSAWithSHA256, signedData, signatureBytes); err != nil {
   462  			return r, errors.ErrWebAuthnRequest.WithArgs(err)
   463  		}
   464  	case "rs256":
   465  		if err := crt.CheckSignature(x509.SHA256WithRSA, signedData, signatureBytes); err != nil {
   466  			return r, errors.ErrWebAuthnRequest.WithArgs(err)
   467  		}
   468  	default:
   469  		return r, errors.ErrWebAuthnRequest.WithArgs("failed signature verification due to unsupported algo")
   470  	}
   471  
   472  	return r, nil
   473  }
   474  
   475  // Disable disables MfaToken instance.
   476  func (p *MfaToken) Disable() {
   477  	p.Expired = true
   478  	p.ExpiredAt = time.Now().UTC()
   479  	p.Disabled = true
   480  	p.DisabledAt = time.Now().UTC()
   481  }
   482  
   483  // ValidateCode validates a passcode
   484  func (p *MfaToken) ValidateCode(code string) error {
   485  	switch p.Type {
   486  	case "totp":
   487  	default:
   488  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("unsupported token type")
   489  	}
   490  	ts := time.Now().UTC()
   491  	return p.ValidateCodeWithTime(code, ts)
   492  }
   493  
   494  // ValidateCodeWithTime validates a passcode at a particular time.
   495  func (p *MfaToken) ValidateCodeWithTime(code string, ts time.Time) error {
   496  	code = strings.TrimSpace(code)
   497  	if code == "" {
   498  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("empty")
   499  	}
   500  	if len(code) < 4 || len(code) > 8 {
   501  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("not 4-8 characters long")
   502  	}
   503  	if len(code) != p.Digits {
   504  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("digits length mismatch")
   505  	}
   506  	tp := uint64(math.Floor(float64(ts.Unix()) / float64(p.Period)))
   507  	tps := []uint64{}
   508  	tps = append(tps, tp)
   509  	tps = append(tps, tp+uint64(1))
   510  	tps = append(tps, tp-uint64(1))
   511  	for _, uts := range tps {
   512  		localCode, err := generateMfaCode(p.Secret, p.Algorithm, p.Digits, uts)
   513  		if err != nil {
   514  			continue
   515  		}
   516  		if subtle.ConstantTimeCompare([]byte(localCode), []byte(code)) == 1 {
   517  			return nil
   518  		}
   519  	}
   520  	return errors.ErrMfaTokenInvalidPasscode.WithArgs("failed")
   521  }
   522  
   523  func generateMfaCode(secret, algo string, digits int, ts uint64) (string, error) {
   524  	var mac hash.Hash
   525  	secretBytes := []byte(secret)
   526  	switch algo {
   527  	case "sha1":
   528  		mac = hmac.New(sha1.New, secretBytes)
   529  	case "sha256":
   530  		mac = hmac.New(sha256.New, secretBytes)
   531  	case "sha512":
   532  		mac = hmac.New(sha512.New, secretBytes)
   533  	case "":
   534  		return "", errors.ErrMfaTokenEmptyAlgorithm
   535  	default:
   536  		return "", errors.ErrMfaTokenInvalidAlgorithm.WithArgs(algo)
   537  	}
   538  
   539  	buf := make([]byte, 8)
   540  	binary.BigEndian.PutUint64(buf, ts)
   541  	mac.Write(buf)
   542  	sum := mac.Sum(nil)
   543  
   544  	off := sum[len(sum)-1] & 0xf
   545  	val := int64(((int(sum[off]) & 0x7f) << 24) |
   546  		((int(sum[off+1] & 0xff)) << 16) |
   547  		((int(sum[off+2] & 0xff)) << 8) |
   548  		(int(sum[off+3]) & 0xff))
   549  	mod := int32(val % int64(math.Pow10(digits)))
   550  	wrap := fmt.Sprintf("%%0%dd", digits)
   551  	return fmt.Sprintf(wrap, mod), nil
   552  }
   553  
   554  func (p *MfaToken) derivePublicKey(params map[string]string) error {
   555  	for _, reqParam := range []string{"key_algo"} {
   556  		if _, exists := params[reqParam]; !exists {
   557  			return errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   558  		}
   559  	}
   560  	switch params["key_algo"] {
   561  	case "es256":
   562  		for _, reqParam := range []string{"curve_xcoord", "curve_ycoord"} {
   563  			if _, exists := params[reqParam]; !exists {
   564  				return errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   565  			}
   566  		}
   567  		var coords []*big.Int
   568  		for _, ltr := range []string{"x", "y"} {
   569  			coord := "curve_" + ltr + "coord"
   570  			b, err := base64.StdEncoding.DecodeString(params[coord])
   571  			if err != nil {
   572  				return errors.ErrWebAuthnRegisterPublicKeyCurveCoord.WithArgs(ltr, err)
   573  			}
   574  			if len(b) != 32 {
   575  				return errors.ErrWebAuthnRegisterPublicKeyCurveCoord.WithArgs(ltr, "not 32 bytes in length")
   576  			}
   577  			i := new(big.Int)
   578  			i.SetBytes(b)
   579  			coords = append(coords, i)
   580  		}
   581  		p.pubkeyECDSA = &ecdsa.PublicKey{Curve: elliptic.P256(), X: coords[0], Y: coords[1]}
   582  	case "rs256":
   583  		for _, reqParam := range []string{"exponent", "modulus"} {
   584  			if _, exists := params[reqParam]; !exists {
   585  				return errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   586  			}
   587  		}
   588  		nb, err := base64.StdEncoding.DecodeString(params["modulus"])
   589  		if err != nil {
   590  			return errors.ErrWebAuthnRegisterPublicKeyMaterial.WithArgs("modulus", err)
   591  		}
   592  		n := new(big.Int)
   593  		n.SetBytes(nb)
   594  
   595  		/*
   596  			ne, err := base64.StdEncoding.DecodeString(params["exponent"])
   597  			if err != nil {
   598  				return errors.ErrWebAuthnRegisterPublicKeyMaterial.WithArgs("exponent", err)
   599  			}
   600  		*/
   601  		p.pubkeyRSA = &rsa.PublicKey{
   602  			N: n,
   603  			E: 65537,
   604  		}
   605  		// return errors.ErrWebAuthnRegisterPublicKeyAlgorithmUnsupported.WithArgs(params["key_algo"])
   606  	}
   607  	return nil
   608  }
   609  
   610  func (r *WebAuthnAuthenticateRequest) unpackAuthData(b []byte) error {
   611  	data := new(AuthData)
   612  	if len(b) < 37 {
   613  		return fmt.Errorf("auth data is less than 37 bytes long")
   614  	}
   615  	data.RelyingPartyID = fmt.Sprintf("%x", b[0:32])
   616  	data.Flags = make(map[string]bool)
   617  	for _, st := range []struct {
   618  		k string
   619  		v byte
   620  	}{
   621  		{"UP", 0x001},
   622  		{"RFU1", 0x002},
   623  		{"UV", 0x004},
   624  		{"RFU2a", 0x008},
   625  		{"RFU2b", 0x010},
   626  		{"RFU2c", 0x020},
   627  		{"AT", 0x040},
   628  		{"ED", 0x080},
   629  	} {
   630  		if (b[32] & st.v) == st.v {
   631  			data.Flags[st.k] = true
   632  		} else {
   633  			data.Flags[st.k] = false
   634  		}
   635  	}
   636  	data.SignatureCounter = binary.BigEndian.Uint32(b[33:37])
   637  
   638  	// TODO(greenpau): implement AT parser.
   639  	// if (data.Flags["AT"] == true) && len(b) > 37 {
   640  	//   // Extract attested credentials data.
   641  	// }
   642  
   643  	r.AuthData = data
   644  	return nil
   645  }