package controller import ( "arimelody-web/model" "crypto/hmac" "crypto/sha1" "encoding/binary" "fmt" "math" "net/url" "strings" "time" "github.com/jmoiron/sqlx" ) const TIME_STEP int64 = 30 const CODE_LENGTH = 6 func GenerateTOTP(secret string, timeStepOffset int) string { counter := time.Now().Unix() / TIME_STEP - int64(timeStepOffset) counterBytes := make([]byte, 8) binary.BigEndian.PutUint64(counterBytes, uint64(counter)) mac := hmac.New(sha1.New, []byte(secret)) mac.Write(counterBytes) hash := mac.Sum(nil) offset := hash[len(hash) - 1] & 0x0f binaryCode := int32(binary.BigEndian.Uint32(hash[offset : offset + 4]) & 0x7FFFFFFF) code := binaryCode % int32(math.Pow10(CODE_LENGTH)) return fmt.Sprintf(fmt.Sprintf("%%0%dd", CODE_LENGTH), code) } func GenerateTOTPURI(username string, secret string) string { url := url.URL{ Scheme: "otpauth", Host: "totp", Path: url.QueryEscape("arimelody.me") + ":" + url.QueryEscape(username), } query := url.Query() query.Set("secret", secret) query.Set("issuer", "arimelody.me") query.Set("algorithm", "SHA1") query.Set("digits", fmt.Sprintf("%d", CODE_LENGTH)) query.Set("period", fmt.Sprintf("%d", TIME_STEP)) url.RawQuery = query.Encode() return url.String() } func GetTOTPsForAccount(db *sqlx.DB, accountID string) ([]model.TOTP, error) { totps := []model.TOTP{} err := db.Select( &totps, "SELECT * FROM totp " + "WHERE account=$1 " + "ORDER BY created_at ASC", accountID, ) if err != nil { return nil, err } return totps, nil } func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) { totp := model.TOTP{} err := db.Get( &totp, "SELECT * FROM totp " + "WHERE account=$1", accountID, ) if err != nil { if strings.Contains(err.Error(), "no rows") { return nil, nil } return nil, err } return &totp, nil } func CreateTOTP(db *sqlx.DB, totp *model.TOTP) error { _, err := db.Exec( "INSERT INTO totp (account, name, secret) " + "VALUES ($1,$2,$3)", totp.AccountID, totp.Name, totp.Secret, ) return err } func DeleteTOTP(db *sqlx.DB, accountID string, name string) error { _, err := db.Exec( "DELETE FROM totp WHERE account=$1 AND name=$2", accountID, name, ) return err }