Skip to content

Instantly share code, notes, and snippets.

@pbnjay
Created August 22, 2016 04:08
Show Gist options
  • Save pbnjay/a2c6100f87d0e4c8fb0a629f67e2c856 to your computer and use it in GitHub Desktop.
Save pbnjay/a2c6100f87d0e4c8fb0a629f67e2c856 to your computer and use it in GitHub Desktop.
Code to control a Locktron Bolt using bluetooth on OSX
// Written by Jeremy Jay <[email protected]>
// github.com/pbnjay
//
// This uses a bunch of copy-pasta from around the web for BLE support in Go, and
// behind-the-scenes bluetooth snooping and reverse engineering to get working.
// I had to do some more hacking on github.com/paypal/gatt to get it running on
// the Wink Hub, but it isn't very reliable yet (it has issues with connection state).
//
// USAGE: ./golockitron -id <BLUETOOTH-PERIPHERIAL-ID> -key <LOCKITRON-ACCESS-KEY> unlock|lock
//
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"flag"
"fmt"
"log"
"strings"
"time"
"github.com/paypal/gatt"
"github.com/paypal/gatt/examples/option"
)
type LockitronLockStatus byte
const (
UNLOCKED LockitronLockStatus = 0
LOCKED LockitronLockStatus = 1
)
var (
lockOrUnlock LockitronLockStatus
lockID = flag.String("id", "", "lockitron peripheral id")
userKey = flag.String("key", "", "lockitron user access key")
// zero iv
zeroIV = make([]byte, 16)
done = make(chan struct{})
)
func onStateChanged(d gatt.Device, s gatt.State) {
fmt.Println("State:", s)
switch s {
case gatt.StatePoweredOn:
fmt.Println("Scanning...")
d.Scan([]gatt.UUID{}, false)
return
default:
d.StopScanning()
}
}
func onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
id := strings.ToUpper(*lockID)
if strings.ToUpper(p.ID()) != id {
return
}
p.Device().StopScanning()
p.Device().Connect(p)
}
func onPeriphConnected(p gatt.Peripheral, err error) {
fmt.Println("Connected to Lockitron!")
defer p.Device().CancelConnection(p)
if err := p.SetMTU(23); err != nil {
fmt.Printf("Failed to set MTU, err: %s\n", err)
}
// Discover services
ss, err := p.DiscoverServices(nil)
if err != nil {
fmt.Printf("Failed to discover services, err: %s\n", err)
return
}
toWatch := make(map[string]*gatt.Characteristic)
for _, s := range ss {
fmt.Println(s.UUID().String())
if s.UUID().String() != "a1a51a187b7747d291db34a48dcd3de9" {
continue
}
// Discover characteristics
cs, err := p.DiscoverCharacteristics(nil, s)
if err != nil {
fmt.Printf("Failed to discover characteristics, err: %s\n", err)
continue
}
for _, c := range cs {
cname := "unknown"
switch c.UUID().String() {
case "1a53e10758f747e5a919acc9e05a908b":
cname = "CLCK"
case "c2bea3d2ae334e9fabeee05377f8623f":
cname = "STAT"
case "26397326157c4364acade7441b43e3fc":
cname = "CRYP"
case "562e4701c08e4547a7b0908823260df3": // not readable...
cname = "CMDS"
default:
continue
}
fmt.Println(cname, "==>>", c.UUID().String())
fmt.Printf(" %+v", c)
toWatch[cname] = c
}
}
//////////////////////////
// read the per-connection nonce
nonce, err := p.ReadCharacteristic(toWatch["CRYP"])
if err != nil {
fmt.Println("initial auth failed:", err)
return
}
fmt.Printf("got handshake nonce: %x\n", nonce)
reqIDBytes := make([]byte, 2)
rand.Read(reqIDBytes)
handshake := prepareHandshakePayload(reqIDBytes, nonce)
fmt.Printf("sending handshake: %x\n", handshake)
err = p.WriteCharacteristic(toWatch["CMDS"], handshake, true)
if err != nil {
fmt.Println("handshake failed:", err)
return
}
time.Sleep(time.Second * 2)
resp, err := p.ReadCharacteristic(toWatch["CLCK"])
if err != nil {
fmt.Println("handshake failed2:", err)
return
}
fmt.Printf("handshake response: %x\n", resp)
time.Sleep(time.Second * 2)
// handshake complete
/////////////////////
changeLockState := prepareLockChangePayload(reqIDBytes, lockOrUnlock == UNLOCKED)
err = p.WriteCharacteristic(toWatch["CMDS"], changeLockState, true)
if err != nil {
fmt.Println("lock change failed:", err)
return
}
time.Sleep(time.Second * 10)
}
func onPeriphDisconnected(p gatt.Peripheral, err error) {
close(done)
}
////////////////////////////
// do the initial connection handshake (e.g. confirm we have user access key)
func prepareHandshakePayload(reqID, nonce []byte) []byte {
mac := hmac.New(sha1.New, []byte(*userKey))
mac.Write(nonce)
userKeyHMAC := mac.Sum(nil)
handshake := make([]byte, 4)
handshake[0] = 0x50 // command = wakeup / authentication?
handshake[1] = reqID[0] // 16-bit request ID
handshake[2] = reqID[1]
handshake[3] = byte(len(userKeyHMAC))
handshake = append(handshake, userKeyHMAC...)
crc := getCRC16(handshake)
handshake = append(handshake, byte((crc>>8)&0xFF), byte(crc&0xFF))
return handshake
}
// 2-stage payload. internal payload is encrypted with userKey
func prepareLockChangePayload(reqID []byte, unlockLock bool, preprepared ...[]byte) []byte {
// 13 bytes of randomness, +1 byte for the on/off command
changeLockCommand := make([]byte, 14, 16)
if len(preprepared) == 0 {
rand.Read(changeLockCommand)
if unlockLock {
changeLockCommand[5] = 0
} else {
changeLockCommand[5] = 1
}
} else {
changeLockCommand = append(changeLockCommand[:0], preprepared[0][:14]...)
}
// +2 bytes CRC16 = 16 byte internal payload
crc := getCRC16(changeLockCommand)
changeLockCommand = append(changeLockCommand, byte((crc>>8)&0xFF), byte(crc&0xFF))
// encrypt the internal payload with user key
cipherCommand := encrypt([]byte(*userKey), changeLockCommand)
fmt.Printf("before: %x\nafter: %x\n", changeLockCommand, cipherCommand)
changeLockState := make([]byte, 4, 22)
rand.Read(changeLockState)
changeLockState[0] = 0x1c // command = change lock state
changeLockState[1] = reqID[0] // requestID
changeLockState[2] = reqID[1]
changeLockState[3] = byte(len(changeLockCommand)) // payload count
// append the internal encrypted payload
changeLockState = append(changeLockState, cipherCommand...)
crc = getCRC16(changeLockState)
changeLockState = append(changeLockState, byte((crc>>8)&0xFF), byte(crc&0xFF))
return changeLockState
}
func getCRC16(data []byte) uint16 {
var crc uint16
for _, d := range data {
crc = uint16(d) ^ crc
for i := 0; i < 8; i++ {
if (crc & 1) == 1 {
crc = ((crc >> 1) & 0x7FFF) ^ 40961
} else {
crc = ((crc >> 1) & 0x7FFF)
}
}
}
return crc
}
func encrypt(key, payload []byte) []byte {
aesBlock, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
stream := cipher.NewCBCEncrypter(aesBlock, zeroIV)
result := make([]byte, len(payload))
stream.CryptBlocks(result, payload)
return result
}
func main() {
flag.Parse()
switch flag.Arg(0) {
case "lock":
lockOrUnlock = LOCKED
case "unlock":
lockOrUnlock = UNLOCKED
default:
log.Fatalln(`last argument should be "lock" or "unlock"`)
}
d, err := gatt.NewDevice(option.DefaultClientOptions...)
if err != nil {
log.Fatalf("Failed to open device, err: %s\n", err)
return
}
// Register handlers.
d.Handle(
gatt.PeripheralDiscovered(onPeriphDiscovered),
gatt.PeripheralConnected(onPeriphConnected),
gatt.PeripheralDisconnected(onPeriphDisconnected),
)
d.Init(onStateChanged)
<-done
fmt.Println("Done!")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment