Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

Any device supported by the TinyGo Bluetooth package can be used to create a beacon recognized by OpenHaystack.

## How to flash
## How to flash (static key)

```shell
tinygo flash -target nano-rp2040 -ldflags="-X main.PublicKey='SGVsbG8sIFdvcmxkIQ=='" .
tinygo flash -target nano-rp2040 -ldflags="-X main.AdvertisingKey='SGVsbG8sIFdvcmxkIQ=='" ./static
```

## How to flash (dynamic key derivation)

```shell
tinygo flash -target nano-rp2040 -ldflags="-X main.DerivationKey='SGVsbG8sIFdvcmxkIQ=='" ./derivation
```
52 changes: 52 additions & 0 deletions firmware/derivation/lfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"encoding/binary"
"io"
"machine"
"os"

"tinygo.org/x/tinyfs/littlefs"
)

var lfs = littlefs.New(machine.Flash)

func getIndex() uint64 {
f, err := lfs.Open("/haystack")
if err != nil {
return 0
}
defer f.Close()

var buf [8]byte
_, err = io.ReadFull(f, buf[:])
must("read index from file", err)
return binary.LittleEndian.Uint64(buf[:])
}

func writeIndex(i uint64) error {
f, err := lfs.OpenFile("/haystack", os.O_CREATE)
if err != nil {
return err
}
defer f.Close()

var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], i)
_, err = f.Write(buf[:])
return err
}

func init() {
config := littlefs.Config{
CacheSize: 64,
LookaheadSize: 32,
BlockCycles: 512,
}
lfs.Configure(&config)
if err := lfs.Mount(); err != nil {
must("format littlefs", lfs.Format())
must("mount littlefs", lfs.Mount())
}
println("littlefs mounted")
}
118 changes: 118 additions & 0 deletions firmware/derivation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Firmware to advertise a FindMy compatible device aka AirTag
// see https://github.com/biemster/FindMy for more information.
//
// To build:
// tinygo flash -target nano-rp2040 -ldflags="-X main.DerivationKey='SGVsbG8sIFdvcmxkIQ=='" .
package main

import (
"crypto/elliptic"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"io"
"machine"
"math/big"
"time"

"github.com/hybridgroup/go-haystack/lib/findmy"
"golang.org/x/crypto/hkdf"
"tinygo.org/x/bluetooth"
)

// The standard is to refresh the key pair every 15 minutes.
const rotateThreshold = 15 * time.Minute

var adapter = bluetooth.DefaultAdapter

func main() {
// wait for USB serial to be available
time.Sleep(2 * time.Second)

// Get and increment index. Ideally this would be done at the end of 15
// minutes. However writing seems to fail once advertising has started.
derivIndex := getIndex()
derivIndex++
must("write new index to file", writeIndex(derivIndex))
println("derivation secret is", DerivationSecret)
println("derivation index is", derivIndex)

priv, pub, err := getKeyData(derivIndex)
must("get key data", err)
println("private key is", base64.StdEncoding.EncodeToString(priv))
println("public key is", base64.StdEncoding.EncodeToString(pub))

opts := bluetooth.AdvertisementOptions{
AdvertisementType: bluetooth.AdvertisingTypeNonConnInd,
Interval: bluetooth.NewDuration(1285000 * time.Microsecond), // 1285ms
ManufacturerData: []bluetooth.ManufacturerDataElement{findmy.NewData(pub)},
}

must("enable BLE stack", adapter.Enable())

// Set the address to the first 6 bytes of the public key.
adapter.SetRandomAddress(bluetooth.MAC{pub[5], pub[4], pub[3], pub[2], pub[1], pub[0] | 0xC0})

println("configure advertising...")
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(opts))

println("start advertising...")
must("start adv", adv.Start())

boot := time.Now()
address, _ := adapter.Address()
for uptime := 0; ; uptime++ {
println("FindMy device using", address.MAC.String(), "uptime", uptime)
time.Sleep(time.Second)
if time.Since(boot) > rotateThreshold {
machine.CPUReset()
}
}
}

const keySize = 28

var curve = elliptic.P224()

func getKeyData(i uint64) ([]byte, []byte, error) {
secret, err := base64.StdEncoding.DecodeString(DerivationSecret)
if err != nil {
return nil, nil, err
}

info := make([]byte, 8)
binary.LittleEndian.PutUint64(info, i)
r := hkdf.New(sha256.New, secret, nil, info)
for {
priv := make([]byte, keySize)
if _, err := io.ReadFull(r, priv); err != nil {
return nil, nil, err
}

privInt := new(big.Int).SetBytes(priv)
n := curve.Params().N
if privInt.Sign() > 0 && privInt.Cmp(n) < 0 {
xInt, _ := curve.ScalarBaseMult(priv)
xBytes := xInt.Bytes()
x := make([]byte, keySize)
copy(x[keySize-len(xBytes):], xBytes)
return priv, x, nil
}
}
}

// must calls a function and fails if an error occurs.
func must(action string, err error) {
if err != nil {
fail("failed to " + action + ": " + err.Error())
}
}

// fail prints a message over and over forever.
func fail(msg string) {
for {
println(msg)
time.Sleep(time.Second)
}
}
6 changes: 6 additions & 0 deletions firmware/derivation/mcu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build tinygo

package main

// DerivationSecret is used to derive rotation keys. Must be base64 encoded.
var DerivationSecret string
2 changes: 2 additions & 0 deletions firmware/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ go 1.23.0

require (
github.com/hybridgroup/go-haystack v0.0.0-20250111073145-3778f18a1e4f
golang.org/x/crypto v0.12.0
tinygo.org/x/bluetooth v0.10.1-0.20250110080820-c6dfccb1a90b
tinygo.org/x/tinyfs v0.4.0
)

require (
Expand Down
4 changes: 4 additions & 0 deletions firmware/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -41,3 +43,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tinygo.org/x/bluetooth v0.10.1-0.20250110080820-c6dfccb1a90b h1:BVFpFhNd0umlK744qtzCfe4W7Dp20Tj2Eb+FVCpggCE=
tinygo.org/x/bluetooth v0.10.1-0.20250110080820-c6dfccb1a90b/go.mod h1:XLRopLvxWmIbofpZSXc7BGGCpgFOV5lrZ1i/DQN0BCw=
tinygo.org/x/tinyfs v0.4.0 h1:35/XmBXSZKz5eqAqkhe83i56qYLhyZ09JarforFoTNQ=
tinygo.org/x/tinyfs v0.4.0/go.mod h1:QM+MK9aXJKKgXZmHJHquzULUVB7h60nIJQmOyKDyA1E=
File renamed without changes.
File renamed without changes.
File renamed without changes.