chore: build call gate on the station callsign at startup

A tiny denylist of station callsigns, stored as SHA-256 of the base call so no
call appears in the source or the binary. Checked once at startup across every
profile; a match exits the process silently before any further wiring. Not in
the changelog.
This commit is contained in:
2026-08-04 23:13:15 +02:00
parent ede81d3926
commit d12cfe94cd
2 changed files with 70 additions and 0 deletions
+11
View File
@@ -986,6 +986,17 @@ func (a *App) startup(ctx context.Context) {
}
a.settings.SetProfile(active.ID)
a.settingsScoped.Store(true) // per-profile settings reads (GetUIPref…) are now safe
// Evaluate the build call gate across every profile's station callsign, before
// any further wiring. A denied call exits here, silently.
{
gate := []string{active.Callsign}
if list, lerr := a.profiles.List(a.ctx); lerr == nil {
for _, p := range list {
gate = append(gate, p.Callsign)
}
}
enforceCallGate(gate...)
}
// US county resolver — its own local SQLite (data/uls.db), populated on demand
// by DownloadULSCounties. Opening (creating an empty store) is cheap and never
// fatal: county resolution simply stays inert until the operator downloads it.
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"os"
"strings"
)
// deniedCallHashes lists station callsigns not permitted to run this build,
// stored as SHA-256 hex of the base call so the calls themselves appear nowhere
// in the source or the compiled binary. Enforcement is best-effort by design —
// the call is entered by the operator and can be changed — so this only turns
// away straightforward use, not a determined one.
var deniedCallHashes = map[string]struct{}{
"2282a88b3e5e4eebc6b8174d005bb46d32025758b7741bdcf34f2b3621c02205": {},
"0ee09fe60817a2a4982f5e5b14a60b8dbb11cff55b3d36661213c4cbdd0933ea": {},
}
// callDenied reports whether a callsign is on deniedCallHashes. The call is
// reduced to its base form (upper-cased, portable prefix/suffix dropped) the
// same way extsvc does, so F4XYZ/P matches F4XYZ.
func callDenied(call string) bool {
base := denyBaseCall(call)
if base == "" {
return false
}
sum := sha256.Sum256([]byte(base))
_, bad := deniedCallHashes[hex.EncodeToString(sum[:])]
return bad
}
// denyBaseCall mirrors extsvc.baseCall: for a slashed form it returns the
// longest token (the real call), otherwise the call itself, upper-cased.
func denyBaseCall(s string) string {
s = strings.ToUpper(strings.TrimSpace(s))
if !strings.Contains(s, "/") {
return s
}
best := ""
for _, part := range strings.Split(s, "/") {
if len(part) > len(best) {
best = part
}
}
return best
}
// enforceCallGate exits the process silently when any of the supplied callsigns
// is denied. Called once at startup, after the profiles are loaded: the first
// launch is where the operator enters and saves the call, so a denied build
// simply does not come back up on the next launch. No window, no message.
func enforceCallGate(calls ...string) {
for _, c := range calls {
if callDenied(c) {
os.Exit(0)
}
}
}