Making our collar listen only to its owner

Our prototype collar obeyed any phone in Bluetooth range. Why ordinary pairing could not fix that on a collar with no buttons, and the per-collar key and signed commands we built instead.

While checking a post for this build log in October, we read our own collar firmware again and found a hole: the collar obeyed any phone in Bluetooth range. Nobody needed our app or an account. A stranger nearby could have cleared the collar's stored activity, changed its settings or started a firmware update. This post covers how we closed it in firmware 0.10.0.

What was wrong

The collar exposes a few Bluetooth characteristics: one streams activity, one reports battery and status, one takes commands. The command channel accepted every write without asking who sent it. Three things followed from that:

The collar is not on sale yet, so the only collars exposed were our own prototypes. We fixed it before any collar reaches a customer, and waited until the fix was running on our collars before publishing this.

Why not normal Bluetooth pairing

The obvious fix is the pairing your phone does with headphones. On a device with no screen and no keypad, that pairing uses a mode called "Just Works", which encrypts the link but proves nothing about who is on the other end. Anyone can pair. It also needs a way to say "pair now", and our only button is reset. And it would have broken a feature we like: a second family phone that shares the dog joins the collar automatically, with no setup.

What we built: a key per collar

Each collar now has its own 256-bit secret key.

Four implementations, one set of test vectors

The protocol lives in four places: the collar firmware (C), the iPhone app (Swift), the Android app (Kotlin) and our bench tool (Python). A one-byte mismatch between any two means a collar that silently refuses its owner. So we wrote the spec first, generated a fixed set of test vectors from it, and made every implementation reproduce them before it touched a real collar. The bench tool also runs the attacks: an unsigned command, a wrong key, a replayed command and a forged one. All four must fail, and the stranger's connection must drop.

The key handshake costs the battery almost nothing: a few hashes per connection. Waking the collar's main loop for it uses a task notification instead of polling, so a phone gets an answer straight away even while the collar is asleep.

What is still open

If you are building something similar

  • Assume anyone in radio range can write to every characteristic you expose, because they can.
  • "Just Works" pairing encrypts the link but authenticates nobody.
  • Write the spec and the test vectors before the code, and make every implementation pass them.
  • Test the attacks, not only the happy path.

Have a thought?

We build in public to learn. If you've solved this on nRF52 before, let us know.

Reply by email

Denys Zarubin

Founder, Spain

Investors

Request the deck

Join the
early list.

Prototype phase. Not certified. Not for sale. No payment required.

You’re on the list. We’ll email you when the collar is ready. In the meantime, download the free Fudini app.