Created
August 22, 2016 04:08
-
-
Save pbnjay/a2c6100f87d0e4c8fb0a629f67e2c856 to your computer and use it in GitHub Desktop.
Code to control a Locktron Bolt using bluetooth on OSX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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