fixed GetTOTP, started rough QR code implementation

GetTOTP handles TOTP method retrieval for confirmation and deletion.

QR code implementation looks like it's gonna suck, so might end up
using a library for this later.
This commit is contained in:
ari melody 2025-01-26 00:48:19 +00:00
parent ad39e68cd6
commit 1edc051ae2
Signed by: ari
GPG key ID: CF99829C92678188
5 changed files with 132 additions and 13 deletions

View file

@ -304,6 +304,12 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
return return
} }
fmt.Printf(
"TOTP:\n\tName: %s\n\tSecret: %s\n",
totp.Name,
totp.Secret,
)
confirmCode := controller.GenerateTOTP(totp.Secret, 0) confirmCode := controller.GenerateTOTP(totp.Secret, 0)
if code != confirmCode { if code != confirmCode {
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
@ -330,12 +336,11 @@ func totpDeleteHandler(app *model.AppState) http.Handler {
return return
} }
name := r.URL.Path if len(r.URL.Path) < 2 {
fmt.Printf("%s\n", name);
if len(name) == 0 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
name := r.URL.Path[1:]
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)

View file

@ -19,6 +19,17 @@ import (
func Handler(app *model.AppState) http.Handler { func Handler(app *model.AppState) http.Handler {
mux := http.NewServeMux() 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("<html><img style=\"image-rendering:pixelated;width:100%;height:100%;object-fit:contain\" src=\"" + qrB64Img + "\"/></html>"))
}))
mux.Handle("/login", loginHandler(app)) mux.Handle("/login", loginHandler(app))
mux.Handle("/logout", requireAccount(app, logoutHandler(app))) mux.Handle("/logout", requireAccount(app, logoutHandler(app)))
@ -243,11 +254,6 @@ func loginHandler(app *model.AppState) http.Handler {
return 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 { type LoginRequest struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`

107
controller/qr.go Normal file
View file

@ -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)
}

View file

@ -64,9 +64,9 @@ func GenerateTOTPURI(username string, secret string) string {
query := url.Query() query := url.Query()
query.Set("secret", secret) query.Set("secret", secret)
query.Set("issuer", "arimelody.me") query.Set("issuer", "arimelody.me")
query.Set("algorithm", "SHA1") // query.Set("algorithm", "SHA1")
query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH)) // query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH))
query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP)) // query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP))
url.RawQuery = query.Encode() url.RawQuery = query.Encode()
return url.String() return url.String()
@ -116,8 +116,9 @@ func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) {
err := db.Get( err := db.Get(
&totp, &totp,
"SELECT * FROM totp " + "SELECT * FROM totp " +
"WHERE account=$1", "WHERE account=$1 AND name=$2",
accountID, accountID,
name,
) )
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no rows") { if strings.Contains(err.Error(), "no rows") {

View file

@ -89,7 +89,6 @@ func main() {
} }
username := os.Args[2] username := os.Args[2]
totpName := os.Args[3] totpName := os.Args[3]
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
account, err := controller.GetAccountByUsername(app.DB, username) account, err := controller.GetAccountByUsername(app.DB, username)
if err != nil { if err != nil {
@ -102,6 +101,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
totp := model.TOTP { totp := model.TOTP {
AccountID: account.ID, AccountID: account.ID,
Name: totpName, Name: totpName,