code.gitea.io/gitea@v1.21.7/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.ORACLE:
    47  		_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY credential_id VARCHAR(410)")
    48  		if err != nil {
    49  			return err
    50  		}
    51  	case schemas.MSSQL:
    52  		// This column has an index on it. I could write all of the code to attempt to change the index OR
    53  		// I could just use recreate table.
    54  		sess := x.NewSession()
    55  		if err := sess.Begin(); err != nil {
    56  			_ = sess.Close()
    57  			return err
    58  		}
    59  
    60  		if err := base.RecreateTable(sess, new(webauthnCredential)); err != nil {
    61  			_ = sess.Close()
    62  			return err
    63  		}
    64  		if err := sess.Commit(); err != nil {
    65  			_ = sess.Close()
    66  			return err
    67  		}
    68  		if err := sess.Close(); err != nil {
    69  			return err
    70  		}
    71  	case schemas.POSTGRES:
    72  		_, err := x.Exec("ALTER TABLE webauthn_credential ALTER COLUMN credential_id TYPE VARCHAR(410)")
    73  		if err != nil {
    74  			return err
    75  		}
    76  	default:
    77  		// SQLite doesn't support ALTER COLUMN, and it already makes String _TEXT_ by default so no migration needed
    78  		// nor is there any need to re-migrate
    79  	}
    80  
    81  	exist, err := x.IsTableExist("u2f_registration")
    82  	if err != nil {
    83  		return err
    84  	}
    85  	if !exist {
    86  		return nil
    87  	}
    88  
    89  	// Now migrate the old u2f registrations to the new format
    90  	type u2fRegistration struct {
    91  		ID          int64 `xorm:"pk autoincr"`
    92  		Name        string
    93  		UserID      int64 `xorm:"INDEX"`
    94  		Raw         []byte
    95  		Counter     uint32             `xorm:"BIGINT"`
    96  		CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
    97  		UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
    98  	}
    99  
   100  	var start int
   101  	regs := make([]*u2fRegistration, 0, 50)
   102  	for {
   103  		err := x.OrderBy("id").Limit(50, start).Find(&regs)
   104  		if err != nil {
   105  			return err
   106  		}
   107  
   108  		err = func() error {
   109  			sess := x.NewSession()
   110  			defer sess.Close()
   111  			if err := sess.Begin(); err != nil {
   112  				return fmt.Errorf("unable to allow start session. Error: %w", err)
   113  			}
   114  			if x.Dialect().URI().DBType == schemas.MSSQL {
   115  				if _, err := sess.Exec("SET IDENTITY_INSERT `webauthn_credential` ON"); err != nil {
   116  					return fmt.Errorf("unable to allow identity insert on webauthn_credential. Error: %w", err)
   117  				}
   118  			}
   119  			for _, reg := range regs {
   120  				parsed := new(u2f.Registration)
   121  				err = parsed.UnmarshalBinary(reg.Raw)
   122  				if err != nil {
   123  					continue
   124  				}
   125  				pubKey, err := parsed.PubKey.ECDH()
   126  				if err != nil {
   127  					continue
   128  				}
   129  				remigrated := &webauthnCredential{
   130  					ID:              reg.ID,
   131  					Name:            reg.Name,
   132  					LowerName:       strings.ToLower(reg.Name),
   133  					UserID:          reg.UserID,
   134  					CredentialID:    base32.HexEncoding.EncodeToString(parsed.KeyHandle),
   135  					PublicKey:       pubKey.Bytes(),
   136  					AttestationType: "fido-u2f",
   137  					AAGUID:          []byte{},
   138  					SignCount:       reg.Counter,
   139  					UpdatedUnix:     reg.UpdatedUnix,
   140  					CreatedUnix:     reg.CreatedUnix,
   141  				}
   142  
   143  				has, err := sess.ID(reg.ID).Get(new(webauthnCredential))
   144  				if err != nil {
   145  					return fmt.Errorf("unable to get webauthn_credential[%d]. Error: %w", reg.ID, err)
   146  				}
   147  				if !has {
   148  					has, err := sess.Where("`lower_name`=?", remigrated.LowerName).And("`user_id`=?", remigrated.UserID).Exist(new(webauthnCredential))
   149  					if err != nil {
   150  						return fmt.Errorf("unable to check webauthn_credential[lower_name: %s, user_id: %d]. Error: %w", remigrated.LowerName, remigrated.UserID, err)
   151  					}
   152  					if !has {
   153  						_, err = sess.Insert(remigrated)
   154  						if err != nil {
   155  							return fmt.Errorf("unable to (re)insert webauthn_credential[%d]. Error: %w", reg.ID, err)
   156  						}
   157  
   158  						continue
   159  					}
   160  				}
   161  
   162  				_, err = sess.ID(remigrated.ID).AllCols().Update(remigrated)
   163  				if err != nil {
   164  					return fmt.Errorf("unable to update webauthn_credential[%d]. Error: %w", reg.ID, err)
   165  				}
   166  			}
   167  			return sess.Commit()
   168  		}()
   169  		if err != nil {
   170  			return err
   171  		}
   172  
   173  		if len(regs) < 50 {
   174  			break
   175  		}
   176  		start += 50
   177  		regs = regs[:0]
   178  	}
   179  
   180  	if x.Dialect().URI().DBType == schemas.POSTGRES {
   181  		if _, err := x.Exec("SELECT setval('webauthn_credential_id_seq', COALESCE((SELECT MAX(id)+1 FROM `webauthn_credential`), 1), false)"); err != nil {
   182  			return err
   183  		}
   184  	}
   185  
   186  	return nil
   187  }