// Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package auth import ( "context" "fmt" "strings" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" ) // ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error. type ErrWebAuthnCredentialNotExist struct { ID int64 CredentialID []byte } func (err ErrWebAuthnCredentialNotExist) Error() string { if len(err.CredentialID) == 0 { return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID) } return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %x]", err.CredentialID) } // Unwrap unwraps this as a ErrNotExist err func (err ErrWebAuthnCredentialNotExist) Unwrap() error { return util.ErrNotExist } // IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist. func IsErrWebAuthnCredentialNotExist(err error) bool { _, ok := err.(ErrWebAuthnCredentialNotExist) return ok } // WebAuthnCredential represents the WebAuthn credential data for a public-key // credential conformant to WebAuthn Level 1 type WebAuthnCredential struct { ID int64 `xorm:"pk autoincr"` Name string LowerName string `xorm:"unique(s)"` UserID int64 `xorm:"INDEX unique(s)"` CredentialID []byte `xorm:"INDEX VARBINARY(1024)"` PublicKey []byte AttestationType string AAGUID []byte SignCount uint32 `xorm:"BIGINT"` CloneWarning bool CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } func init() { db.RegisterModel(new(WebAuthnCredential)) } // TableName returns a better table name for WebAuthnCredential func (cred WebAuthnCredential) TableName() string { return "webauthn_credential" } // UpdateSignCount will update the database value of SignCount func (cred *WebAuthnCredential) UpdateSignCount(ctx context.Context) error { _, err := db.GetEngine(ctx).ID(cred.ID).Cols("sign_count").Update(cred) return err } // BeforeInsert will be invoked by XORM before updating a record func (cred *WebAuthnCredential) BeforeInsert() { cred.LowerName = strings.ToLower(cred.Name) } // BeforeUpdate will be invoked by XORM before updating a record func (cred *WebAuthnCredential) BeforeUpdate() { cred.LowerName = strings.ToLower(cred.Name) } // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (cred *WebAuthnCredential) AfterLoad() { cred.LowerName = strings.ToLower(cred.Name) } // WebAuthnCredentialList is a list of *WebAuthnCredential type WebAuthnCredentialList []*WebAuthnCredential // newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337 // to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags { return webauthn.CredentialFlags{ UserPresent: flags.HasUserPresent(), UserVerified: flags.HasUserVerified(), BackupEligible: flags.HasBackupEligible(), BackupState: flags.HasBackupState(), } } // ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential { // TODO: at the moment, Gitea doesn't store or check the flags // so we need to use the default flags from the authenticator to make the login validation pass // In the future, we should: // 1. store the flags when registering the credential // 2. provide the stored flags when converting the credentials (for login) // 3. for old users, still use this fallback to the default flags defAuthFlags := util.OptionalArg(defaultAuthFlags) creds := make([]webauthn.Credential, 0, len(list)) for _, cred := range list { creds = append(creds, webauthn.Credential{ ID: cred.CredentialID, PublicKey: cred.PublicKey, AttestationType: cred.AttestationType, Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags), Authenticator: webauthn.Authenticator{ AAGUID: cred.AAGUID, SignCount: cred.SignCount, CloneWarning: cred.CloneWarning, }, }) } return creds } // GetWebAuthnCredentialsByUID returns all WebAuthn credentials of the given user func GetWebAuthnCredentialsByUID(ctx context.Context, uid int64) (WebAuthnCredentialList, error) { creds := make(WebAuthnCredentialList, 0) return creds, db.GetEngine(ctx).Where("user_id = ?", uid).Find(&creds) } // ExistsWebAuthnCredentialsForUID returns if the given user has credentials func ExistsWebAuthnCredentialsForUID(ctx context.Context, uid int64) (bool, error) { return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{}) } // GetWebAuthnCredentialByName returns WebAuthn credential by id func GetWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*WebAuthnCredential, error) { cred := new(WebAuthnCredential) if found, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ?", uid, strings.ToLower(name)).Get(cred); err != nil { return nil, err } else if !found { return nil, ErrWebAuthnCredentialNotExist{} } return cred, nil } // GetWebAuthnCredentialByID returns WebAuthn credential by id func GetWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) { cred := new(WebAuthnCredential) if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil { return nil, err } else if !found { return nil, ErrWebAuthnCredentialNotExist{ID: id} } return cred, nil } // HasWebAuthnRegistrationsByUID returns whether a given user has WebAuthn registrations func HasWebAuthnRegistrationsByUID(ctx context.Context, uid int64) (bool, error) { return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{}) } // GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID func GetWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID []byte) (*WebAuthnCredential, error) { cred := new(WebAuthnCredential) if found, err := db.GetEngine(ctx).Where("user_id = ? AND credential_id = ?", userID, credID).Get(cred); err != nil { return nil, err } else if !found { return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID} } return cred, nil } // CreateCredential will create a new WebAuthnCredential from the given Credential func CreateCredential(ctx context.Context, userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) { c := &WebAuthnCredential{ UserID: userID, Name: name, CredentialID: cred.ID, PublicKey: cred.PublicKey, AttestationType: cred.AttestationType, AAGUID: cred.Authenticator.AAGUID, SignCount: cred.Authenticator.SignCount, CloneWarning: false, } if err := db.Insert(ctx, c); err != nil { return nil, err } return c, nil } // DeleteCredential will delete WebAuthnCredential func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) { had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{}) return had > 0, err } // WebAuthnCredentials implements the webauthn.User interface func WebAuthnCredentials(ctx context.Context, userID int64) ([]webauthn.Credential, error) { dbCreds, err := GetWebAuthnCredentialsByUID(ctx, userID) if err != nil { return nil, err } return dbCreds.ToCredentials(), nil }