Device identity
An Ed25519 keypair generated on the device at its first start. The private half never leaves it; the control plane knows only the public half, from enrolment. Nothing is told to the device that it could be told differently.
An ArdaForm device sits on a counter, in a warehouse or in a reception area. Strangers can touch it, unplug it, open it and walk off with the disk. Everything below starts from that assumption rather than pretending otherwise — and is specific about what each control does and does not buy.
Being specific about this is more useful than a page of adjectives. Here is what the design takes seriously, and what it doesn't claim to solve.
| Threat | What happens |
|---|---|
| Someone copies the disk to read your source | The application is a sealed .arda container (AES-256-GCM), opened in memory and never unpacked to disk. The content key arrived inside a signed instruction addressed to that device, and is stored sealed under the device's own key. |
| Someone clones the SD card into another board | Every store on the device — datasets, outbox, settings, the files index — is sealed with a key derived from the device's identity and an identifier of the board. A copied card in another machine opens nothing. Be exact: this is anti-cloning, not anti-theft. |
| The whole device is stolen | Whoever holds the board can run code on it, so encryption at rest is not the control. The wipe is: an operator issues it from the portal and it lands the moment the device reconnects, erasing identity, app and data before anything else happens. |
| A device deleted in the portal is switched on again | It is wiped the moment it connects — within a minute if it is connected when you delete it — and has to be claimed again in person, from the code on its screen. Deleting a device's record (Forget completely) or its whole project does not let it go on running what it had: before the record goes, ArdaForm signs the wipe that device is owed and keeps it apart from the record, for a year. A device removed with Remove is owed its wipe for a year too. |
| Someone substitutes a modified application | An update is a signed, device-bound instruction naming a SHA-256. The device verifies the signature first, checks the bytes it fetched against the hash, and only then opens the container. A mismatch is discarded and reported; the running app is untouched. |
| An instruction is replayed at another device | The device's id is inside the signed bytes, ids are idempotency keys, expiry is judged against server time, and the same id carrying a different instruction is refused. |
| Someone scans the customer's network for your device | Every device runs a firewall and answers nothing on its network that it did not ask for. All communication is one authenticated outbound connection the device initiates, fronted by Cloudflare. A device does not even announce its hostname to the network's DHCP server. |
| Someone tries to log in over SSH from the shop's network | External SSH is closed on every device. An operator opens it from the device's SSH tab — for 30 minutes, 5 hours, until closed, or while that screen is open — and the device shuts it on time by itself, even if the portal never says goodbye. Keys only, no root login, modern key exchange only, no version banner. Keys are installed from the portal and can expire after a day or a month; a user created that way has no password and no sudo, and root is a tick that always comes with an expiry. |
| Someone with a login on the device reads your app or its data | Every port the player opens on the device itself — the app, its bridge, the device view, the shell — checks which user is connecting, from the kernel's own socket table, and refuses anyone but root and the player. A user added from the portal to debug is let in to the app and the device view, never the shell, and the device view's operations on the machine need root. A page cannot open the device view or the shell, and device builds carry no page debugger. Be exact: root on the device can read anything, as root can on any machine. |
| A support tunnel is abused to reach the LAN | The control plane may only name a target from the device's own allow-list — ssh, view, shell — never a host and port. A plane that could name any target would turn every device into a port scanner on its owner's network, so it cannot. |
| A stranger walks up and decommissions a device | Removing a device at the machine needs its device password. The device holds only a salted PBKDF2 hash of it; the word itself lives in the portal, where a person reads it. |
| A phone in range reconfigures a device over Bluetooth | The Bluetooth join surface exists only while a device is unregistered, or has been offline for five minutes — a lifeline, not a door. Wi-Fi credentials cross the air under an ECDH-agreed AES-GCM key. A password gate for an always-on surface is in progress. |
| A dependency in your bundle misbehaves | The page reaches the device only through window.Arda — a fixed list of things a device does. There is no filesystem, no shell and no general-purpose socket: the only places the player connects to for a page are the control plane and the cameras the app names. |
| A webhook's key is lifted from a device | There is none to lift. The app names a webhook by its slug and ArdaForm's servers make the call, so the key stays with the control plane — and the API never hands it back. A delivery to a webhook with a key is signed (HMAC-SHA256 over the timestamp and the body) and carries the key as a bearer token; with Verify on it can also be confirmed through the API. Loopback, link-local and private addresses are refused, so a webhook cannot be pointed back into the plane's own network. |
| An app's settings page tries to reach the operator's account | The page an app may bring for its settings runs in a sandboxed frame on gui.ardaform.com, a separate site from ardaform.net, where the portal is. It has no network — fetch, XHR, WebSocket and outside images and fonts are refused — no cookies and no local storage, no popups, forms or downloads, and one page only: navigating away closes it. It is shown only inside the portal; opened in a tab of its own it is refused. Its one way out is a private channel to the portal, and over it the page can only propose values: the portal checks each against the app's manifest, and nothing is saved until the operator presses Update. Secret fields are never sent to it — it is told only which rows have one, and may replace a secret but never read it. |
Each one is useful alone. The point is that an attacker has to beat several of them in sequence.
An Ed25519 keypair generated on the device at its first start. The private half never leaves it; the control plane knows only the public half, from enrolment. Nothing is told to the device that it could be told differently.
Everything the player keeps on disk is sealed with a key derived from the identity and a board identifier. A file the key cannot open is quarantined and that piece starts empty — datasets are a cache of server truth and resync.
Lock, unlock, wipe, update, password, player update: each is a signed message addressed to one device, verified before it is parsed, acknowledged when it is done, redelivered until it is.
The page gets window.Arda and nothing else. It is one definition, injected before the page's scripts, over loopback with a host guard — not engine bindings, not Node.
One authenticated connection, initiated by the device, over TLS through Cloudflare. No listening socket for anything on the customer's LAN to find. Frames carrying a credential are never compressed.
Nothing answers from the network unless the device asked for it. SSH is shut until an operator opens it from the portal for a set time, and the device closes it again on the dot. The portal's own shell and device view need no open port at all.
Acknowledged first, a bounded flush of records not yet delivered, then everything erased and the device restarted as a stranger. A marker written before the ack means a power cut mid-wipe finishes at the next boot.
There is no key for you to generate, store or lose. Packing happens on the control plane, so the signature on an update proves it came from ArdaForm for your project and arrived unaltered — not that it was produced on your own machine. If you need that second guarantee, say so; it is a different design, and one we would rather build with you than guess at.
| Key | Proves | Private half held by |
|---|---|---|
| Device identity | That this device is the one that enrolled — every handshake, every tunnel | The device. Generated there; never leaves |
| Intent key | That an instruction (update, wipe, lock…) came from the control plane, for this device | ArdaForm's control plane; the public half is pinned in every player build |
| Release key | That a new ArdaPlayer binary came from the release pipeline | The release pipeline; independent of the intent key |
| Content key | Opens one sealed .arda | Rides inside the signed, device-bound update; stored sealed on the device; held by the plane only while packing |
| Webhook key | That a call to your server came from ArdaForm, for your project, unaltered | A secret shared by your server and the control plane. Never sent to a device; never returned by the API |
.env files and symlinks — but what your page does with its own data is yours. Keep secrets out of the app folder; a kiosk can be photographed.
// 1 — the instruction
signature valid for the pinned key ✓
addressed to THIS device ✓
not expired, judged by server time ✓
id not seen with different content ✓
// 2 — the bytes
fetched over HTTPS ✓
SHA-256 matches the instruction ✓
// 3 — the container
AES-GCM tag verifies with the key ✓
opens in memory, entry file present ✓
carries no developer settings file ✓
// 4 — staged, READY marker written last;
// the app is asked; the swap is ordered and
// idempotent; the old container is kept until it is over
Log capture, the screen view, an expression in the page and a shell are genuinely powerful. Powerful things need boundaries around them.
A session starts because a person asked for it, against a named device, from an authenticated portal account. Nothing is open on its own.
A tunnel request names ssh, view or shell. The device answers only for names on its own list, and the token says which request; the handshake says which device.
Who connected, to which device, what they did — log capture, a screenshot, a shell — recorded on the device's screen in the portal rather than reconstructed.
Log capture expires on its own, and so does an opening of External SSH; an installed key can carry an expiry. A device that has been offline says so with a reason when it left on purpose. A removed device fades from the list rather than vanishing.
Tills, kiosks, pick stations, room panels, pay stations, line panels and queue boards — the shapes ArdaForm was designed around.