package server_test import ( "crypto/hmac" "crypto/sha1" "encoding/base32" "encoding/binary" "encoding/json" "fmt" "html" "net/http" "net/http/httptest" "net/url" "path/filepath" "regexp" "strings" "testing" "time" "misaki/db" "misaki/server" ) func code(secret string, offset int64) string { key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ReplaceAll(secret, " ", "")) if err != nil { panic(err) } var msg [8]byte binary.BigEndian.PutUint64(msg[:], uint64(time.Now().Unix()/30+offset)) m := hmac.New(sha1.New, key) m.Write(msg[:]) sum := m.Sum(nil) off := sum[len(sum)-1] & 0xf return fmt.Sprintf("%06d", (binary.BigEndian.Uint32(sum[off:off+4])&0x7fffffff)%1000000) } func cookieNamed(rr *httptest.ResponseRecorder, name string) *http.Cookie { for _, c := range rr.Result().Cookies() { if c.Name == name && c.Value != "" { return c } } return nil } func newTOTPServer(t *testing.T, smtp server.SMTPConfig) (http.Handler, *db.DB) { t.Helper() d := newTestDB(t) srv := server.New(d, server.ServiceURLs{}, nil, smtp, server.Config{ TOTPKeyFile: filepath.Join(t.TempDir(), "totp.key"), PublicURL: "https://misaki.test", }) return srv, d } var ( secretRe = regexp.MustCompile(`]*>([A-Z2-7 ]+)`) blobRe = regexp.MustCompile(`name="blob" value="([^"]+)"`) recoveryRe = regexp.MustCompile(`[a-z2-7]{4}-[a-z2-7]{4}-[a-z2-7]{4}-[a-z2-7]{4}`) ) // enable2FA turns on TOTP from the dashboard and returns the secret and recovery codes. func enable2FA(t *testing.T, srv http.Handler, c *http.Cookie) (string, []string) { t.Helper() rr := doPost(srv, "/dashboard/2fa/setup", nil, c) assertStatus(t, rr, http.StatusOK) body := rr.Body.String() sm, bm := secretRe.FindStringSubmatch(body), blobRe.FindStringSubmatch(body) if sm == nil || bm == nil { t.Fatalf("setup page missing secret/blob:\n%s", body) } if !strings.Contains(body, "otpauth://totp/misaki:") { t.Errorf("setup page has no otpauth link") } if !strings.Contains(body, `