diff --git a/admin/accounthttp.go b/admin/accounthttp.go index 9402410..aa1e042 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -304,6 +304,12 @@ func totpConfirmHandler(app *model.AppState) http.Handler { return } + fmt.Printf( + "TOTP:\n\tName: %s\n\tSecret: %s\n", + totp.Name, + totp.Secret, + ) + confirmCode := controller.GenerateTOTP(totp.Secret, 0) if code != confirmCode { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) @@ -330,12 +336,11 @@ func totpDeleteHandler(app *model.AppState) http.Handler { return } - name := r.URL.Path - fmt.Printf("%s\n", name); - if len(name) == 0 { + if len(r.URL.Path) < 2 { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + name := r.URL.Path[1:] session := r.Context().Value("session").(*model.Session) diff --git a/admin/http.go b/admin/http.go index b6a71ee..42b7e46 100644 --- a/admin/http.go +++ b/admin/http.go @@ -19,6 +19,17 @@ import ( func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() + mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + qrB64Img, err := controller.GenerateQRCode([]byte("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family")) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Write([]byte("")) + })) + mux.Handle("/login", loginHandler(app)) mux.Handle("/logout", requireAccount(app, logoutHandler(app))) @@ -243,11 +254,6 @@ func loginHandler(app *model.AppState) http.Handler { return } - // new accounts won't have TOTP methods at first. there should be a - // second phase of login that prompts the user for a TOTP *only* - // if that account has a TOTP method. - // TODO: login phases (username & password -> TOTP) - type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` diff --git a/controller/qr.go b/controller/qr.go new file mode 100644 index 0000000..c4c2520 --- /dev/null +++ b/controller/qr.go @@ -0,0 +1,107 @@ +package controller + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "image" + "image/color" + "image/png" +) + +const margin = 4 + +type QRCodeECCLevel int64 +const ( + LOW QRCodeECCLevel = iota + MEDIUM + QUARTILE + HIGH +) + +func GenerateQRCode(data []byte) (string, error) { + version := 1 + + size := 0 + size = 21 + version * 4 + if version > 10 { + return "", errors.New(fmt.Sprintf("QR version %d not supported", version)) + } + + img := image.NewGray(image.Rect(0, 0, size + margin * 2, size + margin * 2)) + + // fill white + for y := range size + margin * 2 { + for x := range size + margin * 2 { + img.Set(x, y, color.White) + } + } + + // draw alignment squares + drawLargeAlignmentSquare(margin, margin, img) + drawLargeAlignmentSquare(margin, margin + size - 7, img) + drawLargeAlignmentSquare(margin + size - 7, margin, img) + drawSmallAlignmentSquare(size - 5, size - 5, img) + /* + if version > 4 { + space := version * 3 - 2 + end := size / space + for y := range size / space + 1 { + for x := range size / space + 1 { + if x == 0 && y == 0 { continue } + if x == 0 && y == end { continue } + if x == end && y == 0 { continue } + if x == end && y == end { continue } + drawSmallAlignmentSquare( + x * space + margin + 4, + y * space + margin + 4, + img, + ) + } + } + } + */ + + // draw timing bits + for i := margin + 6; i < size - 4; i++ { + if (i % 2 == 0) { + img.Set(i, margin + 6, color.Black) + img.Set(margin + 6, i, color.Black) + } + } + img.Set(margin + 8, size - 4, color.Black) + + var imgBuf bytes.Buffer + err := png.Encode(&imgBuf, img) + if err != nil { + return "", err + } + + base64Img := base64.StdEncoding.EncodeToString(imgBuf.Bytes()) + + return "data:image/png;base64," + base64Img, nil +} + +func drawLargeAlignmentSquare(x int, y int, img *image.Gray) { + for yi := range 7 { + for xi := range 7 { + if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) { + img.Set(x + xi, y + yi, color.Black) + } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) { + img.Set(x + xi, y + yi, color.Black) + } + } + } +} + +func drawSmallAlignmentSquare(x int, y int, img *image.Gray) { + for yi := range 5 { + for xi := range 5 { + if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) { + img.Set(x + xi, y + yi, color.Black) + } + } + } + img.Set(x + 2, y + 2, color.Black) +} diff --git a/controller/totp.go b/controller/totp.go index bc71747..dbbeec7 100644 --- a/controller/totp.go +++ b/controller/totp.go @@ -64,9 +64,9 @@ func GenerateTOTPURI(username string, secret string) string { query := url.Query() query.Set("secret", secret) query.Set("issuer", "arimelody.me") - query.Set("algorithm", "SHA1") - query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH)) - query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP)) + // query.Set("algorithm", "SHA1") + // query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH)) + // query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP)) url.RawQuery = query.Encode() return url.String() @@ -116,8 +116,9 @@ func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) { err := db.Get( &totp, "SELECT * FROM totp " + - "WHERE account=$1", + "WHERE account=$1 AND name=$2", accountID, + name, ) if err != nil { if strings.Contains(err.Error(), "no rows") { diff --git a/main.go b/main.go index 251d9a9..ac1de59 100644 --- a/main.go +++ b/main.go @@ -89,7 +89,6 @@ func main() { } username := os.Args[2] totpName := os.Args[3] - secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH) account, err := controller.GetAccountByUsername(app.DB, username) if err != nil { @@ -102,6 +101,7 @@ func main() { os.Exit(1) } + secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH) totp := model.TOTP { AccountID: account.ID, Name: totpName,