From f69d3464a74dc176bb0c423bceb9dfcb3bb3263d Mon Sep 17 00:00:00 2001 From: 28allday Date: Wed, 27 May 2026 20:44:55 +0100 Subject: [PATCH] Generate a random friendly alias on first run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the advertised device name defaulted to the machine hostname. Since the alias is broadcast in plaintext over multicast to the whole subnet every few seconds while the TUI is open, that leaks the hostname to anyone on the network — a privacy problem on a laptop joining untrusted Wi-Fi (the reason LocalSend randomises its aliases). Now first run generates a random "Colour Object" name from public-domain word lists (e.g. "Crimson Quasar"): 56 colours x 44 celestial objects = 2464 combinations. It is generated once and persisted, so a device keeps the same name across restarts. The hostname is still carried in DeviceModel, and the alias remains overridable via --alias or Settings. Word lists are original and use only generic colours and public-domain astronomy terms, so there is no copyright or trademark exposure. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 11 +++++- internal/config/config.go | 10 ++++-- internal/config/names.go | 59 ++++++++++++++++++++++++++++++++ internal/config/names_test.go | 64 +++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 internal/config/names.go create mode 100644 internal/config/names_test.go diff --git a/README.md b/README.md index 7bef7da..68901ac 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ downloading. ```sh omarchy-send # uses config / sensible defaults -omarchy-send --alias my-server # override the advertised name for this run +omarchy-send --alias my-server # override the advertised device name omarchy-send --port 53317 # override the listen port omarchy-send --dir ~/Downloads # override the receive directory omarchy-send --auto-accept # accept incoming transfers without a prompt @@ -67,6 +67,15 @@ On Omarchy, the TUI reads the active theme's `~/.config/omarchy/current/theme/co and matches it. Elsewhere (headless / over SSH) it falls back to **ANSI palette colours**, so it tracks whatever colour scheme the connecting terminal uses. +### Device name + +On first run, each device is given a random, friendly display name such as +**"Crimson Quasar"** (a colour + a celestial object), generated once and saved. +This avoids broadcasting your machine's hostname to everyone on the network — +handy on a laptop joining untrusted Wi-Fi. The hostname is still carried in the +device-model field, and you can set any name you like with `--alias` or the `e` +key in the Settings tab. + Config (including the generated TLS identity) is stored at `~/.config/omarchy-send/config.json`. Received files default to `~/Omarchy-Send/`. diff --git a/internal/config/config.go b/internal/config/config.go index f5f40d5..a42d38e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,7 +49,11 @@ func defaults() Config { } home, _ := os.UserHomeDir() return Config{ - Alias: host, + // Alias is intentionally empty here; Load generates a random sci-fi + // alias once on first run (see randomAlias) and persists it. The + // hostname is still carried in DeviceModel so the machine stays + // identifiable to peers that look past the display name. + Alias: "", Port: protocol.DefaultPort, ReceiveDir: filepath.Join(home, "Omarchy-Send"), DeviceModel: host, @@ -85,8 +89,10 @@ func Load() (Config, error) { // Backfill anything still empty after unmarshalling an older/partial file. d := defaults() + // Generate a sci-fi alias once on first run (no file, or a file with no + // alias). It is persisted below, so the name stays stable across restarts. if cfg.Alias == "" { - cfg.Alias = d.Alias + cfg.Alias = randomAlias() } if cfg.Port == 0 { cfg.Port = d.Port diff --git a/internal/config/names.go b/internal/config/names.go new file mode 100644 index 0000000..5faff6e --- /dev/null +++ b/internal/config/names.go @@ -0,0 +1,59 @@ +package config + +import ( + "crypto/rand" + "math/big" +) + +// Colour + celestial-object name parts, all copyright-safe. Colours are generic +// English; the objects are public-domain astronomical terms — types of stars +// and galaxies (Nova, Quasar, Pulsar, Spiral, …). No trademarked or franchise +// names are used, so a generated alias reads like a celestial body ("Crimson +// Nova") with no IP exposure. +// +// This exists because the alias is broadcast in plaintext over multicast to the +// whole subnet every few seconds while the TUI is open. On a roaming laptop the +// machine hostname would leak to strangers on untrusted networks; a randomised +// alias avoids that, matching LocalSend's behaviour. The hostname is still +// carried in DeviceModel and any alias is overridable via --alias or Settings. +var ( + aliasColours = []string{ + "Crimson", "Scarlet", "Azure", "Cobalt", "Emerald", "Amber", + "Ivory", "Onyx", "Silver", "Graphite", "Burgundy", "Teal", + "Indigo", "Bronze", "Pearl", "Slate", "Olive", "Maroon", + "Turquoise", "Gold", "Copper", "Jade", "Ruby", "Sapphire", + "Midnight", "Sand", "Charcoal", "Cream", + "Coral", "Violet", "Lavender", "Plum", "Rose", "Salmon", + "Peach", "Saffron", "Mustard", "Lime", "Mint", "Sage", + "Forest", "Cyan", "Sky", "Navy", "Steel", "Pewter", + "Ash", "Ebony", "Obsidian", "Cherry", "Rust", "Sienna", + "Tan", "Khaki", "Brass", "Platinum", + } + aliasObjects = []string{ + "Nova", "Supernova", "Pulsar", "Quasar", "Magnetar", "Nebula", + "Comet", "Meteor", "Aurora", "Corona", "Eclipse", "Cosmos", + "Galaxy", "Halo", "Spiral", "Starburst", "Dwarf", "Giant", + "Supergiant", "Hypergiant", "Cluster", "Drift", + "Blazar", "Protostar", "Neutron", "Binary", "Cepheid", "Andromeda", + "Whirlpool", "Pinwheel", "Sombrero", "Triangulum", "Sunflower", "Flare", + "Plasma", "Photon", "Zenith", "Apogee", "Solstice", "Equinox", + "Meridian", "Vortex", "Ember", "Beacon", + } +) + +// randomAlias returns a colour + celestial-object display name, e.g. +// "Crimson Nova". It is generated once on first run and persisted by Load, so a +// device keeps the same name across restarts. +func randomAlias() string { + return pick(aliasColours) + " " + pick(aliasObjects) +} + +// pick chooses a uniformly random element using crypto/rand (no seeding, always +// unpredictable). It falls back to the first element only if the RNG errors. +func pick(s []string) string { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(s)))) + if err != nil { + return s[0] + } + return s[n.Int64()] +} diff --git a/internal/config/names_test.go b/internal/config/names_test.go new file mode 100644 index 0000000..c42a88e --- /dev/null +++ b/internal/config/names_test.go @@ -0,0 +1,64 @@ +package config + +import ( + "strings" + "testing" +) + +// randomAlias must always be a "Colour Object" pair drawn from the curated, +// copyright-safe word lists. +func TestRandomAliasFormat(t *testing.T) { + colours := make(map[string]bool, len(aliasColours)) + for _, w := range aliasColours { + colours[w] = true + } + objects := make(map[string]bool, len(aliasObjects)) + for _, w := range aliasObjects { + objects[w] = true + } + + for i := 0; i < 500; i++ { + a := randomAlias() + parts := strings.SplitN(a, " ", 2) + if len(parts) != 2 { + t.Fatalf("alias %q is not two words", a) + } + if !colours[parts[0]] { + t.Fatalf("alias %q: colour %q not in aliasColours", a, parts[0]) + } + if !objects[parts[1]] { + t.Fatalf("alias %q: object %q not in aliasObjects", a, parts[1]) + } + } +} + +// The word lists must contain no duplicates, or the real pool is smaller than +// it looks. +func TestAliasWordsUnique(t *testing.T) { + for _, list := range []struct { + name string + words []string + }{ + {"aliasColours", aliasColours}, + {"aliasObjects", aliasObjects}, + } { + seen := make(map[string]bool, len(list.words)) + for _, w := range list.words { + if seen[w] { + t.Errorf("%s: duplicate word %q", list.name, w) + } + seen[w] = true + } + } +} + +// Sanity check that the generator actually varies (not stuck on one value). +func TestRandomAliasVaries(t *testing.T) { + seen := make(map[string]struct{}) + for i := 0; i < 200; i++ { + seen[randomAlias()] = struct{}{} + } + if len(seen) < 10 { + t.Fatalf("randomAlias produced only %d distinct values over 200 draws", len(seen)) + } +}