Skip to content

Commit 192f245

Browse files
committed
feature: implement firmware with dynamic key derivation
1 parent 2155fab commit 192f245

File tree

6 files changed

+188
-1
lines changed

6 files changed

+188
-1
lines changed

firmware/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

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

5-
## How to flash
5+
## How to flash (static key)
66

77
```shell
88
tinygo flash -target nano-rp2040 -ldflags="-X main.AdvertisingKey='SGVsbG8sIFdvcmxkIQ=='" ./static
99
```
10+
11+
## How to flash (dynamic key derivation)
12+
13+
```shell
14+
tinygo flash -target nano-rp2040 -ldflags="-X main.DerivationKey='SGVsbG8sIFdvcmxkIQ=='" ./derivation
15+
```

firmware/derivation/lfs.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"encoding/binary"
5+
"io"
6+
"machine"
7+
"os"
8+
9+
"tinygo.org/x/tinyfs/littlefs"
10+
)
11+
12+
var lfs = littlefs.New(machine.Flash)
13+
14+
func getIndex() uint64 {
15+
f, err := lfs.Open("/haystack")
16+
if err != nil {
17+
return 0
18+
}
19+
defer f.Close()
20+
21+
var buf [8]byte
22+
_, err = io.ReadFull(f, buf[:])
23+
must("read index from file", err)
24+
return binary.LittleEndian.Uint64(buf[:])
25+
}
26+
27+
func writeIndex(i uint64) error {
28+
f, err := lfs.OpenFile("/haystack", os.O_CREATE)
29+
if err != nil {
30+
return err
31+
}
32+
defer f.Close()
33+
34+
var buf [8]byte
35+
binary.LittleEndian.PutUint64(buf[:], i)
36+
_, err = f.Write(buf[:])
37+
return err
38+
}
39+
40+
func init() {
41+
config := littlefs.Config{
42+
CacheSize: 64,
43+
LookaheadSize: 32,
44+
BlockCycles: 512,
45+
}
46+
lfs.Configure(&config)
47+
if err := lfs.Mount(); err != nil {
48+
must("format littlefs", lfs.Format())
49+
must("mount littlefs", lfs.Mount())
50+
}
51+
println("littlefs mounted")
52+
}

firmware/derivation/main.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Firmware to advertise a FindMy compatible device aka AirTag
2+
// see https://github.com/biemster/FindMy for more information.
3+
//
4+
// To build:
5+
// tinygo flash -target nano-rp2040 -ldflags="-X main.DerivationKey='SGVsbG8sIFdvcmxkIQ=='" .
6+
package main
7+
8+
import (
9+
"crypto/elliptic"
10+
"machine"
11+
"crypto/sha256"
12+
"encoding/base64"
13+
"encoding/binary"
14+
"io"
15+
"math/big"
16+
"time"
17+
18+
"github.com/hybridgroup/go-haystack/lib/findmy"
19+
"golang.org/x/crypto/hkdf"
20+
"tinygo.org/x/bluetooth"
21+
)
22+
23+
var adapter = bluetooth.DefaultAdapter
24+
25+
func main() {
26+
// wait for USB serial to be available
27+
time.Sleep(2 * time.Second)
28+
29+
// Get and increment index. Ideally this would be done at the end of 15
30+
// minutes. However writing seems to fail once advertising has started.
31+
derivIndex := getIndex()
32+
derivIndex++
33+
must("write new index to file", writeIndex(derivIndex))
34+
println("derivation secret is", DerivationSecret)
35+
println("derivation index is", derivIndex)
36+
37+
priv, pub, err := getKeyData(derivIndex)
38+
must("get key data", err)
39+
println("private key is", base64.StdEncoding.EncodeToString(priv))
40+
println("public key is", base64.StdEncoding.EncodeToString(pub))
41+
42+
opts := bluetooth.AdvertisementOptions{
43+
AdvertisementType: bluetooth.AdvertisingTypeNonConnInd,
44+
Interval: bluetooth.NewDuration(1285000 * time.Microsecond), // 1285ms
45+
ManufacturerData: []bluetooth.ManufacturerDataElement{findmy.NewData(pub)},
46+
}
47+
48+
must("enable BLE stack", adapter.Enable())
49+
50+
// Set the address to the first 6 bytes of the public key.
51+
adapter.SetRandomAddress(bluetooth.MAC{pub[5], pub[4], pub[3], pub[2], pub[1], pub[0] | 0xC0})
52+
53+
println("configure advertising...")
54+
adv := adapter.DefaultAdvertisement()
55+
must("config adv", adv.Configure(opts))
56+
57+
println("start advertising...")
58+
must("start adv", adv.Start())
59+
60+
boot := time.Now()
61+
address, _ := adapter.Address()
62+
for uptime := 0; ; uptime++ {
63+
if uptime%100 == 0 {
64+
println("FindMy device using", address.MAC.String(), "uptime", uptime)
65+
}
66+
time.Sleep(time.Second)
67+
if time.Since(boot) > 15*time.Minute {
68+
machine.CPUReset()
69+
}
70+
}
71+
}
72+
73+
const keySize = 28
74+
75+
var curve = elliptic.P224()
76+
77+
func getKeyData(i uint64) ([]byte, []byte, error) {
78+
secret, err := base64.StdEncoding.DecodeString(DerivationSecret)
79+
if err != nil {
80+
return nil, nil, err
81+
}
82+
83+
info := make([]byte, 8)
84+
binary.LittleEndian.PutUint64(info, i)
85+
r := hkdf.New(sha256.New, secret, nil, info)
86+
for {
87+
priv := make([]byte, keySize)
88+
if _, err := io.ReadFull(r, priv); err != nil {
89+
return nil, nil, err
90+
}
91+
92+
privInt := new(big.Int).SetBytes(priv)
93+
n := curve.Params().N
94+
if privInt.Sign() > 0 && privInt.Cmp(n) < 0 {
95+
xInt, _ := curve.ScalarBaseMult(priv)
96+
xBytes := xInt.Bytes()
97+
x := make([]byte, keySize)
98+
copy(x[keySize-len(xBytes):], xBytes)
99+
return priv, x, nil
100+
}
101+
}
102+
}
103+
104+
// must calls a function and fails if an error occurs.
105+
func must(action string, err error) {
106+
if err != nil {
107+
fail("failed to " + action + ": " + err.Error())
108+
}
109+
}
110+
111+
// fail prints a message over and over forever.
112+
func fail(msg string) {
113+
for {
114+
println(msg)
115+
time.Sleep(time.Second)
116+
}
117+
}

firmware/derivation/mcu.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build tinygo
2+
3+
package main
4+
5+
// DerivationSecret is used to derive rotation keys. Must be base64 encoded.
6+
var DerivationSecret string

firmware/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ go 1.23.0
44

55
require (
66
github.com/hybridgroup/go-haystack v0.0.0-20250111073145-3778f18a1e4f
7+
golang.org/x/crypto v0.12.0
78
tinygo.org/x/bluetooth v0.10.1-0.20250110080820-c6dfccb1a90b
9+
tinygo.org/x/tinyfs v0.4.0
810
)
911

1012
require (

firmware/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU
2828
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
2929
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
3030
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
31+
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
32+
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
3133
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
3234
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
3335
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=
4143
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4244
tinygo.org/x/bluetooth v0.10.1-0.20250110080820-c6dfccb1a90b h1:BVFpFhNd0umlK744qtzCfe4W7Dp20Tj2Eb+FVCpggCE=
4345
tinygo.org/x/bluetooth v0.10.1-0.20250110080820-c6dfccb1a90b/go.mod h1:XLRopLvxWmIbofpZSXc7BGGCpgFOV5lrZ1i/DQN0BCw=
46+
tinygo.org/x/tinyfs v0.4.0 h1:35/XmBXSZKz5eqAqkhe83i56qYLhyZ09JarforFoTNQ=
47+
tinygo.org/x/tinyfs v0.4.0/go.mod h1:QM+MK9aXJKKgXZmHJHquzULUVB7h60nIJQmOyKDyA1E=

0 commit comments

Comments
 (0)