summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2022-01-14 23:03:31 +0800
committerGitHub <noreply@github.com>2022-01-14 16:03:31 +0100
commit35c3553870e35b2e7cfcc599645791acda6afcef (patch)
tree0ad600c2d1cd94ef12566482832768c9efcf8a69 /models
parent8808293247bebd20482c3c625c64937174503781 (diff)
downloadgitea-35c3553870e35b2e7cfcc599645791acda6afcef.tar.gz
gitea-35c3553870e35b2e7cfcc599645791acda6afcef.zip
Support webauthn (#17957)
Migrate from U2F to Webauthn Co-authored-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'models')
-rw-r--r--models/auth/main_test.go2
-rw-r--r--models/auth/u2f.go154
-rw-r--r--models/auth/u2f_test.go100
-rw-r--r--models/auth/webauthn.go222
-rw-r--r--models/auth/webauthn_test.go69
-rw-r--r--models/fixtures/u2f_registration.yml7
-rw-r--r--models/fixtures/webauthn_credential.yml8
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v207.go91
9 files changed, 393 insertions, 262 deletions
diff --git a/models/auth/main_test.go b/models/auth/main_test.go
index 94a1f405d9..4255f488fe 100644
--- a/models/auth/main_test.go
+++ b/models/auth/main_test.go
@@ -17,6 +17,6 @@ func TestMain(m *testing.M) {
"oauth2_application.yml",
"oauth2_authorization_code.yml",
"oauth2_grant.yml",
- "u2f_registration.yml",
+ "webauthn_credential.yml",
)
}
diff --git a/models/auth/u2f.go b/models/auth/u2f.go
deleted file mode 100644
index 71943b237c..0000000000
--- a/models/auth/u2f.go
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright 2018 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package auth
-
-import (
- "fmt"
-
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/timeutil"
-
- "github.com/tstranex/u2f"
-)
-
-// ____ ________________________________ .__ __ __ .__
-// | | \_____ \_ _____/\______ \ ____ ____ |__| _______/ |_____________ _/ |_|__| ____ ____
-// | | // ____/| __) | _// __ \ / ___\| |/ ___/\ __\_ __ \__ \\ __\ |/ _ \ / \
-// | | // \| \ | | \ ___// /_/ > |\___ \ | | | | \// __ \| | | ( <_> ) | \
-// |______/ \_______ \___ / |____|_ /\___ >___ /|__/____ > |__| |__| (____ /__| |__|\____/|___| /
-// \/ \/ \/ \/_____/ \/ \/ \/
-
-// ErrU2FRegistrationNotExist represents a "ErrU2FRegistrationNotExist" kind of error.
-type ErrU2FRegistrationNotExist struct {
- ID int64
-}
-
-func (err ErrU2FRegistrationNotExist) Error() string {
- return fmt.Sprintf("U2F registration does not exist [id: %d]", err.ID)
-}
-
-// IsErrU2FRegistrationNotExist checks if an error is a ErrU2FRegistrationNotExist.
-func IsErrU2FRegistrationNotExist(err error) bool {
- _, ok := err.(ErrU2FRegistrationNotExist)
- return ok
-}
-
-// U2FRegistration represents the registration data and counter of a security key
-type U2FRegistration struct {
- ID int64 `xorm:"pk autoincr"`
- Name string
- UserID int64 `xorm:"INDEX"`
- Raw []byte
- Counter uint32 `xorm:"BIGINT"`
- CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
- UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
-}
-
-func init() {
- db.RegisterModel(new(U2FRegistration))
-}
-
-// TableName returns a better table name for U2FRegistration
-func (reg U2FRegistration) TableName() string {
- return "u2f_registration"
-}
-
-// Parse will convert the db entry U2FRegistration to an u2f.Registration struct
-func (reg *U2FRegistration) Parse() (*u2f.Registration, error) {
- r := new(u2f.Registration)
- return r, r.UnmarshalBinary(reg.Raw)
-}
-
-func (reg *U2FRegistration) updateCounter(e db.Engine) error {
- _, err := e.ID(reg.ID).Cols("counter").Update(reg)
- return err
-}
-
-// UpdateCounter will update the database value of counter
-func (reg *U2FRegistration) UpdateCounter() error {
- return reg.updateCounter(db.GetEngine(db.DefaultContext))
-}
-
-// U2FRegistrationList is a list of *U2FRegistration
-type U2FRegistrationList []*U2FRegistration
-
-// ToRegistrations will convert all U2FRegistrations to u2f.Registrations
-func (list U2FRegistrationList) ToRegistrations() []u2f.Registration {
- regs := make([]u2f.Registration, 0, len(list))
- for _, reg := range list {
- r, err := reg.Parse()
- if err != nil {
- log.Error("parsing u2f registration: %v", err)
- continue
- }
- regs = append(regs, *r)
- }
-
- return regs
-}
-
-func getU2FRegistrationsByUID(e db.Engine, uid int64) (U2FRegistrationList, error) {
- regs := make(U2FRegistrationList, 0)
- return regs, e.Where("user_id = ?", uid).Find(&regs)
-}
-
-// GetU2FRegistrationByID returns U2F registration by id
-func GetU2FRegistrationByID(id int64) (*U2FRegistration, error) {
- return getU2FRegistrationByID(db.GetEngine(db.DefaultContext), id)
-}
-
-func getU2FRegistrationByID(e db.Engine, id int64) (*U2FRegistration, error) {
- reg := new(U2FRegistration)
- if found, err := e.ID(id).Get(reg); err != nil {
- return nil, err
- } else if !found {
- return nil, ErrU2FRegistrationNotExist{ID: id}
- }
- return reg, nil
-}
-
-// GetU2FRegistrationsByUID returns all U2F registrations of the given user
-func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) {
- return getU2FRegistrationsByUID(db.GetEngine(db.DefaultContext), uid)
-}
-
-// HasU2FRegistrationsByUID returns whether a given user has U2F registrations
-func HasU2FRegistrationsByUID(uid int64) (bool, error) {
- return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&U2FRegistration{})
-}
-
-func createRegistration(e db.Engine, userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) {
- raw, err := reg.MarshalBinary()
- if err != nil {
- return nil, err
- }
- r := &U2FRegistration{
- UserID: userID,
- Name: name,
- Counter: 0,
- Raw: raw,
- }
- _, err = e.InsertOne(r)
- if err != nil {
- return nil, err
- }
- return r, nil
-}
-
-// CreateRegistration will create a new U2FRegistration from the given Registration
-func CreateRegistration(userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) {
- return createRegistration(db.GetEngine(db.DefaultContext), userID, name, reg)
-}
-
-// DeleteRegistration will delete U2FRegistration
-func DeleteRegistration(reg *U2FRegistration) error {
- return deleteRegistration(db.GetEngine(db.DefaultContext), reg)
-}
-
-func deleteRegistration(e db.Engine, reg *U2FRegistration) error {
- _, err := e.Delete(reg)
- return err
-}
diff --git a/models/auth/u2f_test.go b/models/auth/u2f_test.go
deleted file mode 100644
index 32ad17839c..0000000000
--- a/models/auth/u2f_test.go
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright 2020 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package auth
-
-import (
- "encoding/hex"
- "testing"
-
- "code.gitea.io/gitea/models/unittest"
-
- "github.com/stretchr/testify/assert"
- "github.com/tstranex/u2f"
-)
-
-func TestGetU2FRegistrationByID(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- res, err := GetU2FRegistrationByID(1)
- assert.NoError(t, err)
- assert.Equal(t, "U2F Key", res.Name)
-
- _, err = GetU2FRegistrationByID(342432)
- assert.Error(t, err)
- assert.True(t, IsErrU2FRegistrationNotExist(err))
-}
-
-func TestGetU2FRegistrationsByUID(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- res, err := GetU2FRegistrationsByUID(32)
-
- assert.NoError(t, err)
- assert.Len(t, res, 1)
- assert.Equal(t, "U2F Key", res[0].Name)
-}
-
-func TestU2FRegistration_TableName(t *testing.T) {
- assert.Equal(t, "u2f_registration", U2FRegistration{}.TableName())
-}
-
-func TestU2FRegistration_UpdateCounter(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
- reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
- reg.Counter = 1
- assert.NoError(t, reg.UpdateCounter())
- unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1})
-}
-
-func TestU2FRegistration_UpdateLargeCounter(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
- reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
- reg.Counter = 0xffffffff
- assert.NoError(t, reg.UpdateCounter())
- unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 0xffffffff})
-}
-
-func TestCreateRegistration(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- res, err := CreateRegistration(1, "U2F Created Key", &u2f.Registration{Raw: []byte("Test")})
- assert.NoError(t, err)
- assert.Equal(t, "U2F Created Key", res.Name)
- assert.Equal(t, []byte("Test"), res.Raw)
-
- unittest.AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: 1})
-}
-
-func TestDeleteRegistration(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
- reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
-
- assert.NoError(t, DeleteRegistration(reg))
- unittest.AssertNotExistsBean(t, &U2FRegistration{ID: 1})
-}
-
-const validU2FRegistrationResponseHex = "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871"
-
-func TestToRegistrations_SkipInvalidItemsWithoutCrashing(t *testing.T) {
- regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex)
- regs := U2FRegistrationList{
- &U2FRegistration{ID: 1},
- &U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800},
- }
-
- actual := regs.ToRegistrations()
- assert.Len(t, actual, 1)
-}
-
-func TestToRegistrations(t *testing.T) {
- regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex)
- regs := U2FRegistrationList{
- &U2FRegistration{ID: 1, Name: "U2F Key", UserID: 1, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800},
- &U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800},
- }
-
- actual := regs.ToRegistrations()
- assert.Len(t, actual, 2)
-}
diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go
new file mode 100644
index 0000000000..75776f1e0e
--- /dev/null
+++ b/models/auth/webauthn.go
@@ -0,0 +1,222 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package auth
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/timeutil"
+ "xorm.io/xorm"
+
+ "github.com/duo-labs/webauthn/webauthn"
+)
+
+// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
+type ErrWebAuthnCredentialNotExist struct {
+ ID int64
+ CredentialID string
+}
+
+func (err ErrWebAuthnCredentialNotExist) Error() string {
+ if err.CredentialID == "" {
+ return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID)
+ }
+ return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %s]", err.CredentialID)
+}
+
+//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 string `xorm:"INDEX"`
+ 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() error {
+ return cred.updateSignCount(db.DefaultContext)
+}
+
+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(session *xorm.Session) {
+ cred.LowerName = strings.ToLower(cred.Name)
+}
+
+// WebAuthnCredentialList is a list of *WebAuthnCredential
+type WebAuthnCredentialList []*WebAuthnCredential
+
+// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
+func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
+ creds := make([]webauthn.Credential, 0, len(list))
+ for _, cred := range list {
+ credID, _ := base64.RawStdEncoding.DecodeString(cred.CredentialID)
+ creds = append(creds, webauthn.Credential{
+ ID: credID,
+ PublicKey: cred.PublicKey,
+ AttestationType: cred.AttestationType,
+ 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(uid int64) (WebAuthnCredentialList, error) {
+ return getWebAuthnCredentialsByUID(db.DefaultContext, uid)
+}
+
+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(uid int64) (bool, error) {
+ return existsWebAuthnCredentialsByUID(db.DefaultContext, uid)
+}
+
+func existsWebAuthnCredentialsByUID(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(uid int64, name string) (*WebAuthnCredential, error) {
+ return getWebAuthnCredentialByName(db.DefaultContext, uid, name)
+}
+
+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(id int64) (*WebAuthnCredential, error) {
+ return getWebAuthnCredentialByID(db.DefaultContext, 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(uid int64) (bool, error) {
+ return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&WebAuthnCredential{})
+}
+
+// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
+func GetWebAuthnCredentialByCredID(credID string) (*WebAuthnCredential, error) {
+ return getWebAuthnCredentialByCredID(db.DefaultContext, credID)
+}
+
+func getWebAuthnCredentialByCredID(ctx context.Context, credID string) (*WebAuthnCredential, error) {
+ cred := new(WebAuthnCredential)
+ if found, err := db.GetEngine(ctx).Where("credential_id = ?", 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(userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
+ return createCredential(db.DefaultContext, userID, name, cred)
+}
+
+func createCredential(ctx context.Context, userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) {
+ c := &WebAuthnCredential{
+ UserID: userID,
+ Name: name,
+ CredentialID: base64.RawStdEncoding.EncodeToString(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(id, userID int64) (bool, error) {
+ return deleteCredential(db.DefaultContext, id, userID)
+}
+
+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 implementns the webauthn.User interface
+func WebAuthnCredentials(userID int64) ([]webauthn.Credential, error) {
+ dbCreds, err := GetWebAuthnCredentialsByUID(userID)
+ if err != nil {
+ return nil, err
+ }
+
+ return dbCreds.ToCredentials(), nil
+}
diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go
new file mode 100644
index 0000000000..572636dbbf
--- /dev/null
+++ b/models/auth/webauthn_test.go
@@ -0,0 +1,69 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package auth
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/duo-labs/webauthn/webauthn"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetWebAuthnCredentialByID(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ res, err := GetWebAuthnCredentialByID(1)
+ assert.NoError(t, err)
+ assert.Equal(t, "WebAuthn credential", res.Name)
+
+ _, err = GetWebAuthnCredentialByID(342432)
+ assert.Error(t, err)
+ assert.True(t, IsErrWebAuthnCredentialNotExist(err))
+}
+
+func TestGetWebAuthnCredentialsByUID(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ res, err := GetWebAuthnCredentialsByUID(32)
+ assert.NoError(t, err)
+ assert.Len(t, res, 1)
+ assert.Equal(t, "WebAuthn credential", res[0].Name)
+}
+
+func TestWebAuthnCredential_TableName(t *testing.T) {
+ assert.Equal(t, "webauthn_credential", WebAuthnCredential{}.TableName())
+}
+
+func TestWebAuthnCredential_UpdateSignCount(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential)
+ cred.SignCount = 1
+ assert.NoError(t, cred.UpdateSignCount())
+ unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 1})
+}
+
+func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential)
+ cred.SignCount = 0xffffffff
+ assert.NoError(t, cred.UpdateSignCount())
+ unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
+}
+
+func TestCreateCredential(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
+ assert.NoError(t, err)
+ assert.Equal(t, "WebAuthn Created Credential", res.Name)
+ bs, err := base64.RawStdEncoding.DecodeString(res.CredentialID)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("Test"), bs)
+
+ unittest.AssertExistsIf(t, true, &WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
+}
diff --git a/models/fixtures/u2f_registration.yml b/models/fixtures/u2f_registration.yml
deleted file mode 100644
index 60555c43f1..0000000000
--- a/models/fixtures/u2f_registration.yml
+++ /dev/null
@@ -1,7 +0,0 @@
--
- id: 1
- name: "U2F Key"
- user_id: 32
- counter: 0
- created_unix: 946684800
- updated_unix: 946684800
diff --git a/models/fixtures/webauthn_credential.yml b/models/fixtures/webauthn_credential.yml
new file mode 100644
index 0000000000..b4109a03f2
--- /dev/null
+++ b/models/fixtures/webauthn_credential.yml
@@ -0,0 +1,8 @@
+- id: 1
+ name: "WebAuthn credential"
+ user_id: 32
+ attestation_type: none
+ sign_count: 0
+ clone_warning: false
+ created_unix: 946684800
+ updated_unix: 946684800
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 9423e5c5f6..4ee2bc839f 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -366,6 +366,8 @@ var migrations = []Migration{
NewMigration("Migrate to higher varchar on user struct", migrateUserPasswordSalt),
// v206 -> v207
NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit),
+ // v207 -> v208
+ NewMigration("Add webauthn table and migrate u2f data to webauthn", addWebAuthnCred),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v207.go b/models/migrations/v207.go
new file mode 100644
index 0000000000..82e2e3aa31
--- /dev/null
+++ b/models/migrations/v207.go
@@ -0,0 +1,91 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "crypto/elliptic"
+ "encoding/base64"
+ "strings"
+
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/tstranex/u2f"
+ "xorm.io/xorm"
+)
+
+func addWebAuthnCred(x *xorm.Engine) error {
+
+ // Create webauthnCredential table
+ type webauthnCredential struct {
+ ID int64 `xorm:"pk autoincr"`
+ Name string
+ LowerName string `xorm:"unique(s)"`
+ UserID int64 `xorm:"INDEX unique(s)"`
+ CredentialID string `xorm:"INDEX"`
+ PublicKey []byte
+ AttestationType string
+ AAGUID []byte
+ SignCount uint32 `xorm:"BIGINT"`
+ CloneWarning bool
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+ }
+ if err := x.Sync2(&webauthnCredential{}); err != nil {
+ return err
+ }
+
+ // Now migrate the old u2f registrations to the new format
+ type u2fRegistration struct {
+ ID int64 `xorm:"pk autoincr"`
+ Name string
+ UserID int64 `xorm:"INDEX"`
+ Raw []byte
+ Counter uint32 `xorm:"BIGINT"`
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+ }
+
+ var start int
+ regs := make([]*u2fRegistration, 0, 50)
+ for {
+ err := x.OrderBy("id").Limit(50, start).Find(&regs)
+ if err != nil {
+ return err
+ }
+
+ for _, reg := range regs {
+ parsed := new(u2f.Registration)
+ err = parsed.UnmarshalBinary(reg.Raw)
+ if err != nil {
+ continue
+ }
+
+ c := &webauthnCredential{
+ ID: reg.ID,
+ Name: reg.Name,
+ LowerName: strings.ToLower(reg.Name),
+ UserID: reg.UserID,
+ CredentialID: base64.RawStdEncoding.EncodeToString(parsed.KeyHandle),
+ PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y),
+ AttestationType: "fido-u2f",
+ AAGUID: []byte{},
+ SignCount: reg.Counter,
+ }
+
+ _, err := x.Insert(c)
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(regs) < 50 {
+ break
+ }
+ start += 50
+ regs = regs[:0]
+ }
+
+ return nil
+}