Skip to content

Commit d33fe67

Browse files
committed
feature: preliminary firmware with dynamic key rotation
Signed-off-by: Xudong Zheng <[email protected]>
1 parent 85f10ce commit d33fe67

File tree

4 files changed

+110
-19
lines changed

4 files changed

+110
-19
lines changed

firmware/go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ module github.com/hybridgroup/go-haystack/firmware
22

33
go 1.23.0
44

5-
require tinygo.org/x/bluetooth v0.10.1-0.20250109131232-43edf72c9496
5+
require (
6+
golang.org/x/crypto v0.12.0
7+
tinygo.org/x/bluetooth v0.10.1-0.20250109131232-43edf72c9496
8+
tinygo.org/x/tinyfs v0.4.0
9+
)
610

711
require (
812
github.com/go-ole/go-ole v1.2.6 // indirect

firmware/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU
2626
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
2727
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
2828
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
29+
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
30+
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
2931
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
3032
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
3133
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -39,3 +41,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3941
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4042
tinygo.org/x/bluetooth v0.10.1-0.20250109131232-43edf72c9496 h1:shMYCTiAt5JvqyxKH45NVenfvj3uwy428NO78nITc2A=
4143
tinygo.org/x/bluetooth v0.10.1-0.20250109131232-43edf72c9496/go.mod h1:XLRopLvxWmIbofpZSXc7BGGCpgFOV5lrZ1i/DQN0BCw=
44+
tinygo.org/x/tinyfs v0.4.0 h1:35/XmBXSZKz5eqAqkhe83i56qYLhyZ09JarforFoTNQ=
45+
tinygo.org/x/tinyfs v0.4.0/go.mod h1:QM+MK9aXJKKgXZmHJHquzULUVB7h60nIJQmOyKDyA1E=

firmware/main.go

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,91 @@
99
package main
1010

1111
import (
12+
"crypto/elliptic"
13+
"crypto/sha256"
1214
"encoding/base64"
13-
"errors"
15+
"encoding/binary"
16+
"io"
17+
"machine"
18+
"math/big"
19+
"os"
1420
"time"
1521

22+
"golang.org/x/crypto/hkdf"
1623
"tinygo.org/x/bluetooth"
24+
"tinygo.org/x/tinyfs/littlefs"
1725
)
1826

19-
var adapter = bluetooth.DefaultAdapter
27+
var (
28+
adapter = bluetooth.DefaultAdapter
29+
lfs = littlefs.New(machine.Flash)
30+
)
31+
32+
func getIndex() uint64 {
33+
f, err := lfs.Open("/haystack")
34+
if err != nil {
35+
return 0
36+
}
37+
defer f.Close()
38+
39+
var buf [8]byte
40+
_, err = io.ReadFull(f, buf[:])
41+
must("read index from file", err)
42+
return binary.LittleEndian.Uint64(buf[:])
43+
}
44+
45+
func writeIndex(i uint64) error {
46+
f, err := lfs.OpenFile("/haystack", os.O_CREATE)
47+
if err != nil {
48+
return err
49+
}
50+
defer f.Close()
51+
52+
var buf [8]byte
53+
binary.LittleEndian.PutUint64(buf[:], i)
54+
_, err = f.Write(buf[:])
55+
return err
56+
}
2057

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

25-
key, err := getKeyData()
26-
if err != nil {
27-
fail("failed to get key data: " + err.Error())
62+
config := littlefs.Config{
63+
CacheSize: 64,
64+
LookaheadSize: 32,
65+
BlockCycles: 512,
66+
}
67+
lfs.Configure(&config)
68+
if err := lfs.Mount(); err != nil {
69+
must("format littlefs", lfs.Format())
70+
must("mount littlefs", lfs.Mount())
2871
}
29-
println("key is", AdvertisingKey, "(", len(key), "bytes)")
72+
println("littlefs mounted")
73+
74+
// Get and increment index. Ideally this would be done at the end of 15
75+
// minutes. However writing seems to fail once advertising has started.
76+
derivIndex := getIndex()
77+
derivIndex++
78+
must("write new index to file", writeIndex(derivIndex))
79+
println("derivation secret is", DerivationSecret)
80+
println("derivation index is", derivIndex)
81+
82+
priv, pub, err := getKeyData(derivIndex)
83+
must("get key data", err)
84+
println("private key is", base64.StdEncoding.EncodeToString(priv))
85+
println("public key is", base64.StdEncoding.EncodeToString(pub))
3086

3187
opts := bluetooth.AdvertisementOptions{
3288
AdvertisementType: bluetooth.AdvertisingTypeNonConnInd,
3389
Interval: bluetooth.NewDuration(1285000 * time.Microsecond), // 1285ms
34-
ManufacturerData: []bluetooth.ManufacturerDataElement{findMyData(key)},
90+
ManufacturerData: []bluetooth.ManufacturerDataElement{findMyData(pub)},
3591
}
3692

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

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

4298
println("configure advertising...")
4399
adv := adapter.DefaultAdvertisement()
@@ -46,24 +102,48 @@ func main() {
46102
println("start advertising...")
47103
must("start adv", adv.Start())
48104

105+
boot := time.Now()
49106
address, _ := adapter.Address()
50-
for {
51-
println("FindMy device using", address.MAC.String())
107+
for uptime := 0; ; uptime++ {
108+
if uptime%100 == 0 {
109+
println("FindMy device using", address.MAC.String(), "uptime", uptime)
110+
}
52111
time.Sleep(time.Second)
112+
if time.Since(boot) > 15*time.Minute {
113+
machine.CPUReset()
114+
}
53115
}
54116
}
55117

56-
// getKeyData returns the public key data from the base64 encoded string.
57-
func getKeyData() ([]byte, error) {
58-
val, err := base64.StdEncoding.DecodeString(AdvertisingKey)
118+
const keySize = 28
119+
120+
var curve = elliptic.P224()
121+
122+
func getKeyData(i uint64) ([]byte, []byte, error) {
123+
secret, err := base64.StdEncoding.DecodeString(DerivationSecret)
59124
if err != nil {
60-
return nil, err
61-
}
62-
if len(val) != 28 {
63-
return nil, errors.New("public key must be 28 bytes long")
125+
return nil, nil, err
64126
}
65127

66-
return val, nil
128+
info := make([]byte, 8)
129+
binary.LittleEndian.PutUint64(info, i)
130+
r := hkdf.New(sha256.New, secret, nil, info)
131+
for {
132+
priv := make([]byte, keySize)
133+
if _, err := io.ReadFull(r, priv); err != nil {
134+
return nil, nil, err
135+
}
136+
137+
privInt := new(big.Int).SetBytes(priv)
138+
n := curve.Params().N
139+
if privInt.Sign() > 0 && privInt.Cmp(n) < 0 {
140+
xInt, _ := curve.ScalarBaseMult(priv)
141+
xBytes := xInt.Bytes()
142+
x := make([]byte, keySize)
143+
copy(x[keySize-len(xBytes):], xBytes)
144+
return priv, x, nil
145+
}
146+
}
67147
}
68148

69149
const (

firmware/mcu.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ package main
44

55
// AdvertisingKey is the public key of the device. Must be base64 encoded.
66
var AdvertisingKey string
7+
8+
// DerivationSecret is used to derive rotation keys. Must be base64 encoded.
9+
var DerivationSecret string

0 commit comments

Comments
 (0)