passkey.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. package model
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "time"
  9. "github.com/QuantumNous/new-api/common"
  10. "github.com/go-webauthn/webauthn/protocol"
  11. "github.com/go-webauthn/webauthn/webauthn"
  12. "gorm.io/gorm"
  13. )
  14. var (
  15. ErrPasskeyNotFound = errors.New("passkey credential not found")
  16. ErrFriendlyPasskeyNotFound = errors.New("Passkey 验证失败,请重试或联系管理员")
  17. )
  18. type PasskeyCredential struct {
  19. ID int `json:"id" gorm:"primaryKey"`
  20. UserID int `json:"user_id" gorm:"uniqueIndex;not null"`
  21. CredentialID string `json:"credential_id" gorm:"type:varchar(512);uniqueIndex;not null"` // base64 encoded
  22. PublicKey string `json:"public_key" gorm:"type:text;not null"` // base64 encoded
  23. AttestationType string `json:"attestation_type" gorm:"type:varchar(255)"`
  24. AAGUID string `json:"aaguid" gorm:"type:varchar(512)"` // base64 encoded
  25. SignCount uint32 `json:"sign_count" gorm:"default:0"`
  26. CloneWarning bool `json:"clone_warning"`
  27. UserPresent bool `json:"user_present"`
  28. UserVerified bool `json:"user_verified"`
  29. BackupEligible bool `json:"backup_eligible"`
  30. BackupState bool `json:"backup_state"`
  31. Transports string `json:"transports" gorm:"type:text"`
  32. Attachment string `json:"attachment" gorm:"type:varchar(32)"`
  33. LastUsedAt *time.Time `json:"last_used_at"`
  34. CreatedAt time.Time `json:"created_at"`
  35. UpdatedAt time.Time `json:"updated_at"`
  36. DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
  37. }
  38. func (p *PasskeyCredential) TransportList() []protocol.AuthenticatorTransport {
  39. if p == nil || strings.TrimSpace(p.Transports) == "" {
  40. return nil
  41. }
  42. var transports []string
  43. if err := json.Unmarshal([]byte(p.Transports), &transports); err != nil {
  44. return nil
  45. }
  46. result := make([]protocol.AuthenticatorTransport, 0, len(transports))
  47. for _, transport := range transports {
  48. result = append(result, protocol.AuthenticatorTransport(transport))
  49. }
  50. return result
  51. }
  52. func (p *PasskeyCredential) SetTransports(list []protocol.AuthenticatorTransport) {
  53. if len(list) == 0 {
  54. p.Transports = ""
  55. return
  56. }
  57. stringList := make([]string, len(list))
  58. for i, transport := range list {
  59. stringList[i] = string(transport)
  60. }
  61. encoded, err := json.Marshal(stringList)
  62. if err != nil {
  63. return
  64. }
  65. p.Transports = string(encoded)
  66. }
  67. func (p *PasskeyCredential) ToWebAuthnCredential() webauthn.Credential {
  68. flags := webauthn.CredentialFlags{
  69. UserPresent: p.UserPresent,
  70. UserVerified: p.UserVerified,
  71. BackupEligible: p.BackupEligible,
  72. BackupState: p.BackupState,
  73. }
  74. credID, _ := base64.StdEncoding.DecodeString(p.CredentialID)
  75. pubKey, _ := base64.StdEncoding.DecodeString(p.PublicKey)
  76. aaguid, _ := base64.StdEncoding.DecodeString(p.AAGUID)
  77. return webauthn.Credential{
  78. ID: credID,
  79. PublicKey: pubKey,
  80. AttestationType: p.AttestationType,
  81. Transport: p.TransportList(),
  82. Flags: flags,
  83. Authenticator: webauthn.Authenticator{
  84. AAGUID: aaguid,
  85. SignCount: p.SignCount,
  86. CloneWarning: p.CloneWarning,
  87. Attachment: protocol.AuthenticatorAttachment(p.Attachment),
  88. },
  89. }
  90. }
  91. func NewPasskeyCredentialFromWebAuthn(userID int, credential *webauthn.Credential) *PasskeyCredential {
  92. if credential == nil {
  93. return nil
  94. }
  95. passkey := &PasskeyCredential{
  96. UserID: userID,
  97. CredentialID: base64.StdEncoding.EncodeToString(credential.ID),
  98. PublicKey: base64.StdEncoding.EncodeToString(credential.PublicKey),
  99. AttestationType: credential.AttestationType,
  100. AAGUID: base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID),
  101. SignCount: credential.Authenticator.SignCount,
  102. CloneWarning: credential.Authenticator.CloneWarning,
  103. UserPresent: credential.Flags.UserPresent,
  104. UserVerified: credential.Flags.UserVerified,
  105. BackupEligible: credential.Flags.BackupEligible,
  106. BackupState: credential.Flags.BackupState,
  107. Attachment: string(credential.Authenticator.Attachment),
  108. }
  109. passkey.SetTransports(credential.Transport)
  110. return passkey
  111. }
  112. func (p *PasskeyCredential) ApplyValidatedCredential(credential *webauthn.Credential) {
  113. if credential == nil || p == nil {
  114. return
  115. }
  116. p.CredentialID = base64.StdEncoding.EncodeToString(credential.ID)
  117. p.PublicKey = base64.StdEncoding.EncodeToString(credential.PublicKey)
  118. p.AttestationType = credential.AttestationType
  119. p.AAGUID = base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID)
  120. p.SignCount = credential.Authenticator.SignCount
  121. p.CloneWarning = credential.Authenticator.CloneWarning
  122. p.UserPresent = credential.Flags.UserPresent
  123. p.UserVerified = credential.Flags.UserVerified
  124. p.BackupEligible = credential.Flags.BackupEligible
  125. p.BackupState = credential.Flags.BackupState
  126. p.Attachment = string(credential.Authenticator.Attachment)
  127. p.SetTransports(credential.Transport)
  128. }
  129. func GetPasskeyByUserID(userID int) (*PasskeyCredential, error) {
  130. if userID == 0 {
  131. common.SysLog("GetPasskeyByUserID: empty user ID")
  132. return nil, ErrFriendlyPasskeyNotFound
  133. }
  134. var credential PasskeyCredential
  135. if err := DB.Where("user_id = ?", userID).First(&credential).Error; err != nil {
  136. if errors.Is(err, gorm.ErrRecordNotFound) {
  137. // 未找到记录是正常情况(用户未绑定),返回 ErrPasskeyNotFound 而不记录日志
  138. return nil, ErrPasskeyNotFound
  139. }
  140. // 只有真正的数据库错误才记录日志
  141. common.SysLog(fmt.Sprintf("GetPasskeyByUserID: database error for user %d: %v", userID, err))
  142. return nil, ErrFriendlyPasskeyNotFound
  143. }
  144. return &credential, nil
  145. }
  146. func GetPasskeyByCredentialID(credentialID []byte) (*PasskeyCredential, error) {
  147. if len(credentialID) == 0 {
  148. common.SysLog("GetPasskeyByCredentialID: empty credential ID")
  149. return nil, ErrFriendlyPasskeyNotFound
  150. }
  151. credIDStr := base64.StdEncoding.EncodeToString(credentialID)
  152. var credential PasskeyCredential
  153. if err := DB.Where("credential_id = ?", credIDStr).First(&credential).Error; err != nil {
  154. if errors.Is(err, gorm.ErrRecordNotFound) {
  155. common.SysLog(fmt.Sprintf("GetPasskeyByCredentialID: passkey not found for credential ID length %d", len(credentialID)))
  156. return nil, ErrFriendlyPasskeyNotFound
  157. }
  158. common.SysLog(fmt.Sprintf("GetPasskeyByCredentialID: database error for credential ID: %v", err))
  159. return nil, ErrFriendlyPasskeyNotFound
  160. }
  161. return &credential, nil
  162. }
  163. func UpsertPasskeyCredential(credential *PasskeyCredential) error {
  164. if credential == nil {
  165. common.SysLog("UpsertPasskeyCredential: nil credential provided")
  166. return fmt.Errorf("Passkey 保存失败,请重试")
  167. }
  168. return DB.Transaction(func(tx *gorm.DB) error {
  169. // 使用Unscoped()进行硬删除,避免唯一索引冲突
  170. if err := tx.Unscoped().Where("user_id = ?", credential.UserID).Delete(&PasskeyCredential{}).Error; err != nil {
  171. common.SysLog(fmt.Sprintf("UpsertPasskeyCredential: failed to delete existing credential for user %d: %v", credential.UserID, err))
  172. return fmt.Errorf("Passkey 保存失败,请重试")
  173. }
  174. if err := tx.Create(credential).Error; err != nil {
  175. common.SysLog(fmt.Sprintf("UpsertPasskeyCredential: failed to create credential for user %d: %v", credential.UserID, err))
  176. return fmt.Errorf("Passkey 保存失败,请重试")
  177. }
  178. return nil
  179. })
  180. }
  181. func DeletePasskeyByUserID(userID int) error {
  182. if userID == 0 {
  183. common.SysLog("DeletePasskeyByUserID: empty user ID")
  184. return fmt.Errorf("删除失败,请重试")
  185. }
  186. // 使用Unscoped()进行硬删除,避免唯一索引冲突
  187. if err := DB.Unscoped().Where("user_id = ?", userID).Delete(&PasskeyCredential{}).Error; err != nil {
  188. common.SysLog(fmt.Sprintf("DeletePasskeyByUserID: failed to delete passkey for user %d: %v", userID, err))
  189. return fmt.Errorf("删除失败,请重试")
  190. }
  191. return nil
  192. }