penguin/homekey-go

homekey-go

internal/blegateway/relay.go

raw ยท 4271 bytes

// SPDX-License-Identifier: Apache-2.0
package blegateway

import (
	"context"
	"encoding/binary"
	"fmt"
	"time"

	"github.com/godbus/dbus/v5"
)

const StatusUUID = "7a6b0003-5b21-4f36-8e5d-9c3a26d74210"
const APDUUUID = "7a6b0004-5b21-4f36-8e5d-9c3a26d74210"
const headerSize = 12
const maxAPDU = 240

type Card struct {
	Session uint32
	UID     []byte
	SAK     byte
}
type Handler func(context.Context, Card, *Relay) error

// The gateway goroutine serializes all requests, including PING and release.
type Relay struct {
	characteristic dbus.BusObject
	session        uint32
	seq            uint16
	writeSize      int
}

func packet(kind byte, session uint32, seq uint16, offset, total int, data []byte) []byte {
	b := make([]byte, headerSize+len(data))
	b[0] = 1
	b[1] = kind
	binary.BigEndian.PutUint32(b[2:], session)
	binary.BigEndian.PutUint16(b[6:], seq)
	binary.BigEndian.PutUint16(b[8:], uint16(offset))
	binary.BigEndian.PutUint16(b[10:], uint16(total))
	copy(b[12:], data)
	return b
}
func parseCard(b []byte) (Card, error) {
	if len(b) != 18 || b[0] != 1 || b[1] > 1 {
		return Card{}, fmt.Errorf("invalid NFC status")
	}
	if b[1] == 0 {
		return Card{}, nil
	}
	n := int(b[2])
	if n != 4 && n != 7 && n != 10 {
		return Card{}, fmt.Errorf("invalid NFC UID length")
	}
	sid := binary.BigEndian.Uint32(b[4:])
	if sid == 0 {
		return Card{}, fmt.Errorf("invalid NFC session")
	}
	return Card{Session: sid, SAK: b[3], UID: append([]byte(nil), b[8:8+n]...)}, nil
}
func parseResponse(b []byte, sid uint32, seq uint16) ([]byte, bool, error) {
	if len(b) == 0 {
		return nil, false, nil
	}
	if len(b) < headerSize || b[0] != 1 {
		return nil, false, fmt.Errorf("invalid relay response")
	}
	if binary.BigEndian.Uint32(b[2:]) != sid || binary.BigEndian.Uint16(b[6:]) != seq {
		return nil, false, nil
	}
	n := int(binary.BigEndian.Uint16(b[10:]))
	if n > maxAPDU || len(b) != headerSize+n || binary.BigEndian.Uint16(b[8:]) != 0 {
		return nil, false, fmt.Errorf("invalid relay response length")
	}
	switch b[1] {
	case 0x10:
		if n != 0 {
			return nil, false, fmt.Errorf("invalid pending response")
		}
		return nil, false, nil
	case 0x11:
		if n < 2 {
			return nil, false, fmt.Errorf("missing APDU status")
		}
		return append([]byte(nil), b[headerSize:]...), true, nil
	case 0x12:
		if n != 1 {
			return nil, false, fmt.Errorf("invalid relay error")
		}
		return nil, false, fmt.Errorf("PN532 exchange failed (code %d)", b[headerSize])
	default:
		return nil, false, fmt.Errorf("unknown relay response")
	}
}
func (r *Relay) write(ctx context.Context, b []byte) error {
	return r.characteristic.CallWithContext(ctx, "org.bluez.GattCharacteristic1.WriteValue", 0, b, map[string]dbus.Variant{"type": dbus.MakeVariant("request")}).Err
}
func (r *Relay) Exchange(ctx context.Context, apdu []byte) ([]byte, error) {
	if len(apdu) < 4 || len(apdu) > maxAPDU {
		return nil, fmt.Errorf("APDU length outside 4..%d", maxAPDU)
	}
	if r.seq == 65535 {
		return nil, fmt.Errorf("APDU sequence exhausted")
	}
	r.seq++
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	limit := r.writeSize - headerSize
	if limit < 1 {
		return nil, fmt.Errorf("BLE MTU too small")
	}
	for off := 0; off < len(apdu); {
		end := off + limit
		if end > len(apdu) {
			end = len(apdu)
		}
		if err := r.write(ctx, packet(1, r.session, r.seq, off, len(apdu), apdu[off:end])); err != nil {
			return nil, err
		}
		off = end
	}
	for ctx.Err() == nil {
		var b []byte
		if err := r.characteristic.CallWithContext(ctx, "org.bluez.GattCharacteristic1.ReadValue", 0, map[string]dbus.Variant{}).Store(&b); err != nil {
			return nil, err
		}
		data, ready, err := parseResponse(b, r.session, r.seq)
		if err != nil || ready {
			return data, err
		}
		if !wait(ctx, 20*time.Millisecond) {
			break
		}
	}
	return nil, ctx.Err()
}
func (r *Relay) Release(ctx context.Context) error {
	return r.write(ctx, packet(2, r.session, 0, 0, 0, nil))
}
func readCard(ctx context.Context, ch dbus.BusObject) (Card, error) {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()
	var b []byte
	if err := ch.CallWithContext(ctx, "org.bluez.GattCharacteristic1.ReadValue", 0, map[string]dbus.Variant{}).Store(&b); err != nil {
		return Card{}, err
	}
	return parseCard(b)
}