adminui: show the enrolment link as a QR code
Enrolling a device that cannot reach the admin UI meant transcribing a 200-character link with a base64 pin in it — the step the link format exists to avoid, and the one where a pin wrong by one character fails later as an inscrutable TLS error. Rendered as inline SVG rather than a PNG data: URI, because the page's CSP is default-src 'none' and means it: a data: image would need img-src opened, markup needs nothing. One path rather than a rect per module, since a link this long encodes to about 60x60 and two thousand elements is a lot of DOM for a picture of a square. It is generated from the same validated value as the href, so a rejected link produces neither. This relaxes the stdlib-only rule, deliberately and recorded in CLAUDE.md. The rule bought one self-contained binary with no supply chain to audit, which one small pure-Go package barely dents; F-Droid never applied to the server, only the app ships there. A correct QR encoder is ~500 lines of Reed-Solomon that nobody should be hand-writing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5b02d40802
commit
40e76c52ca
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package adminui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// qrSVG renders text as an inline SVG QR code, or empty if it will not encode.
|
||||
//
|
||||
// Inline SVG rather than a PNG data: URI because the page's CSP is `default-src 'none'` and means
|
||||
// it. A data: image would need img-src opened up; markup needs nothing, and the QR is generated
|
||||
// here from a boolean matrix, so nothing a user supplied reaches the output.
|
||||
//
|
||||
// Drawn as one path rather than a rect per module: a link of this length encodes to roughly 60x60
|
||||
// modules, and two thousand elements is a lot of DOM for a picture of a square.
|
||||
func qrSVG(text string) template.HTML {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
// Medium recovery: a phone camera reading a screen has no dirt or creases to survive, and
|
||||
// lower recovery keeps the module count down, which keeps it scannable on a small display.
|
||||
q, err := qrcode.New(text, qrcode.Medium)
|
||||
if err != nil {
|
||||
return "" // too long to encode; the link text below it still works
|
||||
}
|
||||
bitmap := q.Bitmap()
|
||||
n := len(bitmap)
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var path strings.Builder
|
||||
for y, row := range bitmap {
|
||||
for x, dark := range row {
|
||||
if dark {
|
||||
fmt.Fprintf(&path, "M%d %dh1v1h-1z", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A quiet zone is part of the spec, not decoration: without it a scanner cannot find the
|
||||
// symbol's edges against whatever is next to it on the page.
|
||||
var out strings.Builder
|
||||
fmt.Fprintf(&out,
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" `+
|
||||
`width="240" height="240" shape-rendering="crispEdges" role="img" `+
|
||||
`aria-label="Enrolment link as a QR code">`+
|
||||
`<rect width="%d" height="%d" fill="#fff"/>`+
|
||||
`<path d="%s" fill="#000"/></svg>`,
|
||||
n, n, n, n, path.String())
|
||||
return template.HTML(out.String())
|
||||
}
|
||||
Reference in New Issue
Block a user