code.gitea.io/gitea@v1.22.3/models/migrations/v1_16/v210.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package v1_16 //nolint 5 6 import ( 7 "encoding/base32" 8 "fmt" 9 "strings" 10 11 "code.gitea.io/gitea/models/migrations/base" 12 "code.gitea.io/gitea/modules/timeutil" 13 14 "github.com/tstranex/u2f" 15 "xorm.io/xorm" 16 "xorm.io/xorm/schemas" 17 ) 18 19 // v208 migration was completely broken 20 func RemigrateU2FCredentials(x *xorm.Engine) error { 21 // Create webauthnCredential table 22 type webauthnCredential struct { 23 ID int64 `xorm:"pk autoincr"` 24 Name string 25 LowerName string `xorm:"unique(s)"` 26 UserID int64 `xorm:"INDEX unique(s)"` 27 CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety 28 PublicKey []byte 29 AttestationType string 30 AAGUID []byte 31 SignCount uint32 `xorm:"BIGINT"` 32 CloneWarning bool 33 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 34 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 35 } 36 if err := x.Sync(&webauthnCredential{}); err != nil { 37 return err 38 } 39 40 switch x.Dialect().URI().DBType { 41 case schemas.MYSQL: 42 _, err := x.Exec("ALTER TABLE webauthn_credential MODIFY COLUMN credential_id VARCHAR(410)") 43 if err != nil { 44 return err 45 } 46 case schemas.MSSQL: 47 // This column has an index on it. I could write all of the code to attempt to change the index OR 48 // I could just use recreate table. 49 sess := x.NewSession() 50 if err := sess.Begin(); err != nil { 51 _ = sess.Close() 52 return err 53 } 54 55 if err := base.RecreateTable(sess, new(webauthnCredential)); err != nil { 56 _ = sess.Close() 57 return err 58 } 59 if err := sess.Commit(); err != nil { 60 _ = sess.Close() 61 return err 62 } 63 if err := sess.Close(); err != nil { 64 return err 65 } 66 case schemas.POSTGRES: 67 _, err := x.Exec("ALTER TABLE webauthn_credential ALTER COLUMN credential_id TYPE VARCHAR(410)") 68 if err != nil { 69 return err 70 } 71 default: 72 // SQLite doesn't support ALTER COLUMN, and it already makes String _TEXT_ by default so no migration needed 73 // nor is there any need to re-migrate 74 } 75 76 exist, err := x.IsTableExist("u2f_registration") 77 if err != nil { 78 return err 79 } 80 if !exist { 81 return nil 82 } 83 84 // Now migrate the old u2f registrations to the new format 85 type u2fRegistration struct { 86 ID int64 `xorm:"pk autoincr"` 87 Name string 88 UserID int64 `xorm:"INDEX"` 89 Raw []byte 90 Counter uint32 `xorm:"BIGINT"` 91 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 92 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 93 } 94 95 var start int 96 regs := make([]*u2fRegistration, 0, 50) 97 for { 98 err := x.OrderBy("id").Limit(50, start).Find(®s) 99 if err != nil { 100 return err 101 } 102 103 err = func() error { 104 sess := x.NewSession() 105 defer sess.Close() 106 if err := sess.Begin(); err != nil { 107 return fmt.Errorf("unable to allow start session. Error: %w", err) 108 } 109 if x.Dialect().URI().DBType == schemas.MSSQL { 110 if _, err := sess.Exec("SET IDENTITY_INSERT `webauthn_credential` ON"); err != nil { 111 return fmt.Errorf("unable to allow identity insert on webauthn_credential. Error: %w", err) 112 } 113 } 114 for _, reg := range regs { 115 parsed := new(u2f.Registration) 116 err = parsed.UnmarshalBinary(reg.Raw) 117 if err != nil { 118 continue 119 } 120 pubKey, err := parsed.PubKey.ECDH() 121 if err != nil { 122 continue 123 } 124 remigrated := &webauthnCredential{ 125 ID: reg.ID, 126 Name: reg.Name, 127 LowerName: strings.ToLower(reg.Name), 128 UserID: reg.UserID, 129 CredentialID: base32.HexEncoding.EncodeToString(parsed.KeyHandle), 130 PublicKey: pubKey.Bytes(), 131 AttestationType: "fido-u2f", 132 AAGUID: []byte{}, 133 SignCount: reg.Counter, 134 UpdatedUnix: reg.UpdatedUnix, 135 CreatedUnix: reg.CreatedUnix, 136 } 137 138 has, err := sess.ID(reg.ID).Get(new(webauthnCredential)) 139 if err != nil { 140 return fmt.Errorf("unable to get webauthn_credential[%d]. Error: %w", reg.ID, err) 141 } 142 if !has { 143 has, err := sess.Where("`lower_name`=?", remigrated.LowerName).And("`user_id`=?", remigrated.UserID).Exist(new(webauthnCredential)) 144 if err != nil { 145 return fmt.Errorf("unable to check webauthn_credential[lower_name: %s, user_id: %d]. Error: %w", remigrated.LowerName, remigrated.UserID, err) 146 } 147 if !has { 148 _, err = sess.Insert(remigrated) 149 if err != nil { 150 return fmt.Errorf("unable to (re)insert webauthn_credential[%d]. Error: %w", reg.ID, err) 151 } 152 153 continue 154 } 155 } 156 157 _, err = sess.ID(remigrated.ID).AllCols().Update(remigrated) 158 if err != nil { 159 return fmt.Errorf("unable to update webauthn_credential[%d]. Error: %w", reg.ID, err) 160 } 161 } 162 return sess.Commit() 163 }() 164 if err != nil { 165 return err 166 } 167 168 if len(regs) < 50 { 169 break 170 } 171 start += 50 172 regs = regs[:0] 173 } 174 175 if x.Dialect().URI().DBType == schemas.POSTGRES { 176 if _, err := x.Exec("SELECT setval('webauthn_credential_id_seq', COALESCE((SELECT MAX(id)+1 FROM `webauthn_credential`), 1), false)"); err != nil { 177 return err 178 } 179 } 180 181 return nil 182 }