otp.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package user
  2. import (
  3. "bytes"
  4. "crypto/sha1"
  5. "encoding/base64"
  6. "encoding/hex"
  7. "fmt"
  8. "github.com/0xJacky/Nginx-UI/api"
  9. "github.com/0xJacky/Nginx-UI/internal/crypto"
  10. "github.com/0xJacky/Nginx-UI/query"
  11. "github.com/0xJacky/Nginx-UI/settings"
  12. "github.com/gin-gonic/gin"
  13. "github.com/pquerna/otp"
  14. "github.com/pquerna/otp/totp"
  15. "github.com/uozi-tech/cosy"
  16. "image/jpeg"
  17. "net/http"
  18. "strings"
  19. )
  20. func GenerateTOTP(c *gin.Context) {
  21. u := api.CurrentUser(c)
  22. issuer := fmt.Sprintf("Nginx UI %s", settings.NodeSettings.Name)
  23. issuer = strings.TrimSpace(issuer)
  24. otpOpts := totp.GenerateOpts{
  25. Issuer: issuer,
  26. AccountName: u.Name,
  27. Period: 30, // seconds
  28. Digits: otp.DigitsSix,
  29. Algorithm: otp.AlgorithmSHA1,
  30. }
  31. otpKey, err := totp.Generate(otpOpts)
  32. if err != nil {
  33. api.ErrHandler(c, err)
  34. return
  35. }
  36. qrCode, err := otpKey.Image(512, 512)
  37. if err != nil {
  38. api.ErrHandler(c, err)
  39. return
  40. }
  41. // Encode the image to a buffer
  42. var buf []byte
  43. buffer := bytes.NewBuffer(buf)
  44. err = jpeg.Encode(buffer, qrCode, nil)
  45. if err != nil {
  46. fmt.Println("Error encoding image:", err)
  47. return
  48. }
  49. // Convert the buffer to a base64 string
  50. base64Str := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes())
  51. c.JSON(http.StatusOK, gin.H{
  52. "secret": otpKey.Secret(),
  53. "qr_code": base64Str,
  54. })
  55. }
  56. func EnrollTOTP(c *gin.Context) {
  57. cUser := api.CurrentUser(c)
  58. if cUser.EnabledOTP() {
  59. c.JSON(http.StatusBadRequest, gin.H{
  60. "message": "User already enrolled",
  61. })
  62. return
  63. }
  64. if settings.NodeSettings.Demo {
  65. c.JSON(http.StatusBadRequest, gin.H{
  66. "message": "This feature is disabled in demo mode",
  67. })
  68. return
  69. }
  70. var json struct {
  71. Secret string `json:"secret" binding:"required"`
  72. Passcode string `json:"passcode" binding:"required"`
  73. }
  74. if !cosy.BindAndValid(c, &json) {
  75. return
  76. }
  77. if ok := totp.Validate(json.Passcode, json.Secret); !ok {
  78. c.JSON(http.StatusNotAcceptable, gin.H{
  79. "message": "Invalid passcode",
  80. })
  81. return
  82. }
  83. ciphertext, err := crypto.AesEncrypt([]byte(json.Secret))
  84. if err != nil {
  85. api.ErrHandler(c, err)
  86. return
  87. }
  88. u := query.User
  89. _, err = u.Where(u.ID.Eq(cUser.ID)).Update(u.OTPSecret, ciphertext)
  90. if err != nil {
  91. api.ErrHandler(c, err)
  92. return
  93. }
  94. recoveryCode := sha1.Sum(ciphertext)
  95. c.JSON(http.StatusOK, gin.H{
  96. "message": "ok",
  97. "recovery_code": hex.EncodeToString(recoveryCode[:]),
  98. })
  99. }
  100. func ResetOTP(c *gin.Context) {
  101. var json struct {
  102. RecoveryCode string `json:"recovery_code"`
  103. }
  104. if !cosy.BindAndValid(c, &json) {
  105. return
  106. }
  107. recoverCode, err := hex.DecodeString(json.RecoveryCode)
  108. if err != nil {
  109. api.ErrHandler(c, err)
  110. return
  111. }
  112. cUser := api.CurrentUser(c)
  113. k := sha1.Sum(cUser.OTPSecret)
  114. if !bytes.Equal(k[:], recoverCode) {
  115. c.JSON(http.StatusBadRequest, gin.H{
  116. "message": "Invalid recovery code",
  117. })
  118. return
  119. }
  120. u := query.User
  121. _, err = u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null())
  122. if err != nil {
  123. api.ErrHandler(c, err)
  124. return
  125. }
  126. c.JSON(http.StatusOK, gin.H{
  127. "message": "ok",
  128. })
  129. }