Boundaries

Protecting WebUSB and Web Serial device code

Applications that talk to hardware from a browser bring a distinctive question to a protection tool, and the usual answers fit badly. The permission model is enforced somewhere your code cannot reach, the thing genuinely worth concealing is not a file but a body of knowledge, and the standard way of recovering that knowledge bypasses your bundle entirely. The result is a clear split between what protection does nothing for and what it does unusually well.

The permission prompt is the security boundary, and it is not yours

Browsers gained the ability to talk to hardware slowly and defensively, and every one of those interfaces carries the same structure. A page can request access to a USB interface, a serial port, a Bluetooth peripheral or a directory on the user's disk, but only from a secure context, only in response to a real user gesture, and only after the user picks a specific device or folder from a chooser the page cannot read or script. Until that happens, the page has nothing.

This has a consequence that surprises teams arriving with the wrong mental model. Protecting your code does not tighten that boundary and cannot loosen it. The browser enforces its checks against the origin making the call, not against how legible the calling code happens to be. A minified page, a protected page and a page with full source maps published all receive exactly the same authority: whatever the user granted, and nothing else.

So the question worth asking is not what obfuscation restricts. It is what, in a device-facing application, is genuinely worth concealing at all.

The asset is the protocol, and the protocol is knowledge

For most applications the valuable part of the codebase is the product logic. For hardware-facing applications it is usually something narrower and much more expensive: the accumulated understanding of how to talk to a particular device.

That understanding shows up in the code as unglamorous constants and sequences. Command opcodes. Packet framing rules and checksums. Sequence numbering. Calibration values arrived at through weeks of measurement. Timing requirements that are not in any datasheet. Initialisation handshakes that were recovered by watching a vendor's own tool. Service-mode entry sequences. Firmware update procedures.

None of it looks impressive in a diff, and all of it can represent months of laboratory work. It is entirely reasonable to want it protected. What matters is understanding how it is actually recovered, because that determines whether protection helps.

Recovery happens at the API boundary, not in your file

The methods that send data to a device are ordinary JavaScript functions living on ordinary prototypes. That is the whole vulnerability. Replacing one of them with a wrapper that records its argument and then calls the original captures every packet the page transmits, in exact bytes, in the order they were sent. It takes one line typed into a console, and it works identically whether the bundle was protected or not.

The same applies across the family. Serial ports expose readable and writable streams, and a writer's send method is just as replaceable. Bluetooth characteristics have write methods on a prototype. Directory and file handles return writable streams with the same shape. The one-line interception is not an exotic technique; it is how anybody debugs an unfamiliar device integration, including you.

The principle underneath is the same one that governs shader source and encrypted strings: a value that must be reconstructed in order to be used can be observed at the moment it is reconstructed. Your protocol bytes must arrive at the transmit call exactly right, or the device rejects them. That requirement is what makes them capturable.

Which means the string options are the wrong tool here

Moving literals into a table and encoding their characters does real work. Open the protected bundle and you will not find a tidy block of command bytes with helpful names beside them; you will find indices into a table and escape sequences. Somebody skimming casually gives up. That is a genuine benefit against the lazy path.

It is also the entire benefit, and a device protocol is precisely the kind of target where nobody stays on the lazy path. The person who wants your framing rules is willing to open a console. Weigh the transforms accordingly: keep them, because they cost little and raise the floor, but do not build a plan on them concealing an opcode table.

What the transforms do protect, and it is the better half

A captured trace tells somebody what your application sent. It does not tell them why it sent that, in that order, at that moment, or what it would have sent had the device answered differently.

That reasoning is the expensive part to reconstruct, and it lives in your JavaScript:

  • the state machine that decides which sequence applies to which device state
  • retry, backoff and recovery behaviour when a device responds unexpectedly
  • the calibration maths that turns raw readings into meaningful values
  • the error taxonomy and what each failure mode implies about the hardware
  • feature detection across device firmware revisions

Renaming, control-flow flattening and code reordering make all of that materially harder to lift. A competitor with a packet capture has a recording; a competitor with your readable source has an implementation. The distance between those two is where a protector earns its place in a hardware project, and it is a defensible claim precisely because it does not pretend the packets are hidden.

Put the authority in the device

The strongest move is architectural and does not involve the browser at all. Ask what happens if your protocol becomes public tomorrow. If the device performs a privileged action simply because a well-formed command arrived, then protocol secrecy was the only control, and it was always a weak one. If the device verifies a signature on commands that matter, checks an entitlement before entering a service mode, or refuses unsigned firmware, then a leaked protocol costs you design confidentiality rather than safety.

Signed firmware is the clearest example. A captured update sequence lets somebody watch an update happen; it does not let them push a modified image, because the device checks the signature and the signing key was never in the browser. That is the difference between an embarrassment and an incident, and it is decided in firmware rather than in a build setting.

Two practical traps

The first is browser coverage. These interfaces are implemented in Chromium-based browsers and not universally, so a device-facing application is already constrained. That intersects awkwardly with the browser lock option, which classifies the runtime from its identification string and treats anything it does not recognise as unknown. Unknown never appears in an allowlist, so the guard fails closed. Kiosk builds, embedded webviews and industrial panels with customised identification are exactly the deployments most likely to be running device code and most likely to be locked out by a guard intended to protect them.

The second is name-based dispatch. Device abstraction layers frequently build handler tables by reading function names, or look up a method by string to route an incoming report. Renaming breaks that, and the failure appears at run time against real hardware rather than in any build check. Reserved names exist for this: they are regular expressions for names to preserve, mapped to the variable exclusion list. Identify those names before the first protected build rather than after the first field failure.

A design that holds up

Assume the wire is observable, because it is. Put the checks that matter into the device, so that speaking the protocol and being authorised are different things. Keep the code transforms enabled for the application logic, where reconstruction is genuinely expensive, and treat the string options as a modest deterrent rather than a control. Exclude the names your own dispatch logic depends on. Then run the protected build against real hardware, because timing, startup order and permission flow are the parts a mock will never test.

What you end up with is honest: the browser decides what your page may touch, the device decides what it will accept, and the protector makes your accumulated engineering expensive to copy. Each control is doing something it can actually do.

Frequently asked questions

Does obfuscation restrict what a page can do with a connected device?

It does not change the permission model in any direction, because the browser is the gatekeeper rather than your code. Access to a USB interface, a serial port, a Bluetooth peripheral or a directory on disk requires a secure context, a genuine user gesture and an explicit selection by the user from a chooser the page cannot script. Those checks are enforced by the browser against the calling page, not against the readability of the calling code. Protected code and unprotected code receive exactly the same authority, which means the transformations neither add a restriction nor remove one.

Then what is actually worth protecting in a device-facing web application?

The protocol, and specifically the accumulated knowledge encoded in it. Command opcodes, packet framing, checksum and sequence rules, calibration constants, timing requirements, undocumented initialisation sequences and firmware update handshakes are frequently the most expensive thing a hardware team owns, because they were arrived at through lab work rather than downloaded from a specification. Everything else in the application is ordinary front-end code. That is the asset, and it is worth being precise that the asset is knowledge rather than secrecy of a particular file.

How would somebody recover our device protocol from a protected build?

By watching the boundary rather than reading the file, which takes one line typed into a console. The methods that transmit to a device are ordinary functions on ordinary prototypes, so replacing one with a wrapper that logs its argument and then calls through captures every packet the page sends, in exact bytes, regardless of how the values were stored in the bundle. The same applies to the readable and writable stream interfaces used for serial ports and to the characteristic write methods used for Bluetooth. Values that must be reconstructed to be used can be observed at the moment they are reconstructed.

Do the string transforms help with an opcode table?

Less than the effort suggests, and it is worth being blunt about why. Moving literals into a table and encoding their characters means somebody skimming your bundle will not see a neat block of command bytes, which raises the cost of casual reading. It does nothing about the interception described above, because the exact bytes still have to reach the transmit call. So the transforms buy protection against the lazy path and none against the direct one, and a device protocol is exactly the kind of target where somebody is willing to take the direct path.

Is there anything the transforms protect well in this setting?

Yes, and it is usually the more valuable half. Capturing traffic tells somebody what was sent; it does not tell them why, or what to send when the device replies unexpectedly. The state machine, the retry and recovery logic, the conditions that select one command sequence over another, the calibration maths and the error taxonomy all live in your JavaScript, and reconstructing them from an observed trace is genuinely laborious. That is where renaming, control-flow flattening and code reordering pay for themselves. Protect the reasoning and assume the packets are visible.

Can we keep firmware images or update payloads confidential this way?

Not through obfuscation, because the payload is delivered to the device through the same interceptable boundary. The control that actually works is on the device: sign firmware images and have the device verify the signature before accepting an update. That makes a captured protocol useful for observation but not for pushing modified firmware, which is the outcome that matters. It also means a leaked protocol degrades from a safety problem into a competitive annoyance, which is a much better place to be.

What is the risk if our protocol does leak?

It depends entirely on whether authority lives in the device or in the page, which is the design question this whole area comes down to. If the device performs any privileged action simply because it received a well-formed command, then anyone who can speak the protocol has that authority, and hiding the protocol was the only thing standing in the way. If the device requires signed commands, or checks an entitlement, or refuses to enter service modes without a challenge, then a leaked protocol reveals design detail without conferring capability. Move the check into the firmware and the confidentiality question becomes far less load-bearing.

Do these device APIs work in every browser?

No, and the gap matters more here than usual. The USB, serial and Bluetooth interfaces are available in Chromium-based browsers and are not implemented in every engine, which means a device-facing application is already constrained in where it runs. That has a direct interaction with the browser lock option, because a runtime that does not present a recognised signature classifies as unknown, and unknown never appears in an allowlist, so the guard fails closed. Embedded webviews and kiosk builds with customised identification are exactly the deployments that trip this.

What about the File System Access interface specifically?

It follows the same pattern with a sharper consequence, because the resource is the user's own disk. A handle is obtained through a picker the user drives, and the scope of what your page can touch is whatever they selected and nothing more. Obfuscating the code that requests or uses a handle does not widen or narrow that scope. What it can do is make an audit harder, so if your application writes to user directories it is worth keeping that code path legible in your own repository and documented, because somebody will eventually ask exactly what your page touches and why.

Does a permission prompt protect us from a malicious page copying our approach?

The prompt protects the user, not your design. It ensures a person consciously chose to connect a device to a specific origin, which is a meaningful control against drive-by access, and it is unaffected by how any page's code is written. It offers nothing against a competitor who wants to talk to the same hardware, because they will simply request the same permission from their own users and be granted it. Competitive protection here comes from the device side and from the reasoning in your application, not from the browser's consent flow.

Are there build settings that cause trouble with device code specifically?

Two areas deserve testing rather than assumption. Device work is timing-sensitive, and heavier structural transforms add startup and per-call overhead that can matter when a protocol has response windows, so measure against real hardware rather than a mock. And any code that reads its own function names at run time, or dispatches on them, is fragile under renaming; that pattern appears surprisingly often in device abstraction layers that build handler tables from function names. Reserved names exist for exactly this, and they are regular expressions mapped to the variable exclusion list.

What is the shortest sensible policy for a device-facing team?

Five points. Assume the wire protocol is observable and design so that observation is not the loss. Put authority in the device by signing commands and firmware, so speaking the protocol is not the same as being trusted. Keep the transforms enabled for the application logic, where the reconstruction cost genuinely lives, and do not expect the string options to conceal an opcode table. Exclude any names your dispatch logic looks up at run time. And test against real hardware with the protected build, because the timing and startup behaviour is the part a mock will not reveal.

Related reading