diff --git a/firmware/README.md b/firmware/README.md index 189c9e1..3865344 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -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 ``` diff --git a/firmware/derivation/lfs.go b/firmware/derivation/lfs.go new file mode 100644 index 0000000..6ef5286 --- /dev/null +++ b/firmware/derivation/lfs.go @@ -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") +} diff --git a/firmware/derivation/main.go b/firmware/derivation/main.go new file mode 100644 index 0000000..0b6749f --- /dev/null +++ b/firmware/derivation/main.go @@ -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) + } +} diff --git a/firmware/derivation/mcu.go b/firmware/derivation/mcu.go new file mode 100644 index 0000000..a0129c4 --- /dev/null +++ b/firmware/derivation/mcu.go @@ -0,0 +1,6 @@ +//go:build tinygo + +package main + +// DerivationSecret is used to derive rotation keys. Must be base64 encoded. +var DerivationSecret string diff --git a/firmware/go.mod b/firmware/go.mod index 68a4331..2b7f028 100644 --- a/firmware/go.mod +++ b/firmware/go.mod @@ -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 ( diff --git a/firmware/go.sum b/firmware/go.sum index 569f803..435249f 100644 --- a/firmware/go.sum +++ b/firmware/go.sum @@ -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= @@ -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= diff --git a/firmware/main.go b/firmware/static/main.go similarity index 100% rename from firmware/main.go rename to firmware/static/main.go diff --git a/firmware/mcu.go b/firmware/static/mcu.go similarity index 100% rename from firmware/mcu.go rename to firmware/static/mcu.go diff --git a/firmware/os.go b/firmware/static/os.go similarity index 100% rename from firmware/os.go rename to firmware/static/os.go