Go · 1795 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package totp
4
5 import (
6 "fmt"
7 "html/template"
8 "strings"
9
10 "github.com/boombuler/barcode/qr"
11 )
12
13 // QRSize is the rendered SVG side length in CSS pixels (the SVG itself
14 // is resolution-independent; this drives the viewport-mapped size).
15 const QRSize = 256
16
17 // QRSVG renders the otpauth URI as an SVG QR code suitable for inline
18 // embedding in a template. Returns template.HTML so html/template doesn't
19 // double-escape the markup.
20 //
21 // The URI is high-entropy and contains the secret. Callers MUST NOT log
22 // either the URI or the rendered SVG.
23 func QRSVG(otpauthURI string) (template.HTML, error) {
24 code, err := qr.Encode(otpauthURI, qr.M, qr.Auto)
25 if err != nil {
26 return "", fmt.Errorf("totp: qr encode: %w", err)
27 }
28 bounds := code.Bounds()
29 side := bounds.Dx()
30 if side <= 0 {
31 return "", fmt.Errorf("totp: qr empty bounds")
32 }
33
34 var b strings.Builder
35 fmt.Fprintf(&b,
36 `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" shape-rendering="crispEdges" role="img" aria-label="2FA setup QR code">`,
37 QRSize, QRSize, side, side)
38 // White background.
39 fmt.Fprintf(&b, `<rect width="%d" height="%d" fill="#ffffff"/>`, side, side)
40
41 // Emit one <rect> per module. For typical otpauth URIs the QR is
42 // ~33×33 modules, so the SVG stays under a few KB.
43 type img interface {
44 Get(x, y int) bool
45 }
46 g, ok := code.(img)
47 if !ok {
48 return "", fmt.Errorf("totp: qr type lacks Get(x,y) accessor")
49 }
50 for y := 0; y < side; y++ {
51 for x := 0; x < side; x++ {
52 if g.Get(x, y) {
53 fmt.Fprintf(&b, `<rect x="%d" y="%d" width="1" height="1" fill="#000000"/>`, x, y)
54 }
55 }
56 }
57 b.WriteString(`</svg>`)
58 return template.HTML(b.String()), nil //nolint:gosec // we built the SVG ourselves; no untrusted input.
59 }
60