Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use same pair record structor as MacOS for remote pairing #322

Merged
merged 1 commit into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 106 additions & 64 deletions ios/tunnel/pairrecord.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,146 @@ package tunnel

import (
"crypto/rand"
"errors"
"fmt"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ed25519"
"howett.net/plist"
"io"
"os"
"path"
"strings"
)

type PairRecord struct {
Public ed25519.PublicKey
Private ed25519.PrivateKey
HostName string
type SelfIdentity struct {
Identifier string
Irk []byte
PrivateKey ed25519.PrivateKey
PublicKey ed25519.PublicKey
}

type pairRecordData struct {
Seed []byte
Hostname string
type selfIdentityInternal struct {
Identifier string `plist:"identifier"`
Irk []byte `plist:"irk"`
PrivateKey []byte `plist:"privateKey"`
PublicKey []byte `plist:"publicKey"`
}

func NewPairRecord() (PairRecord, error) {
hostname, err := os.Hostname()
type Device struct {
Identifier string `plist:"identifier"`
Info []byte `plist:"info"`
Irk []byte `plist:"irk"`
Model string `plist:"model"`
Name string `plist:"name"`
PublicKey []byte `plist:"publicKey"`
}

// PairRecordManager implements the same logic as macOS related to remote pair records. Those pair records are used
// whenever a tunnel gets created.
type PairRecordManager struct {
SelfId SelfIdentity
peersLocation string
}

// NewPairRecordManager creates a PairRecordManager that reads/stores the pair records information at the given path
// To use the same pair records as macOS does, this path should be /var/db/lockdown/RemotePairing/user_501
// (user_501 is the default for the root user)
func NewPairRecordManager(p string) (PairRecordManager, error) {
selfIdPath := path.Join(p, "selfIdentity.plist")
selfId, err := getOrCreateSelfIdentity(selfIdPath)
if err != nil {
log.WithError(err).Warn("could not get hostname. generate a random one")
return PairRecordManager{}, fmt.Errorf("NewPairRecordManager: failed to get self identity: %w", err)
}
hostname = uuid.New().String() // FIXME: this should be the hostname, but pairing fails with it
var pairRecord PairRecord
priv, pub, err := ed25519.GenerateKey(rand.Reader)
pairRecord.Public = priv
pairRecord.Private = pub
pairRecord.HostName = hostname
return PairRecordManager{
SelfId: selfId,
peersLocation: path.Join(p, "peers"),
}, nil
}

// StoreDeviceInfo stores the provided Device info as a plist encoded file in the `peers/` directory
func (p PairRecordManager) StoreDeviceInfo(d Device) error {
devicePath := path.Join(p.peersLocation, fmt.Sprintf("%s.plist", d.Identifier))
f, err := os.OpenFile(devicePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return PairRecord{}, err
return fmt.Errorf("StoreDeviceInfo: could open file for writing: %w", err)
}
log.WithField("hostname", pairRecord.HostName).Info("created new pair record")
return pairRecord, nil
}
defer f.Close()

func ParsePairRecord(r io.ReadSeeker) (PairRecord, error) {
dec := plist.NewDecoder(r)
var seed pairRecordData
err := dec.Decode(&seed)
enc := plist.NewEncoderForFormat(f, plist.BinaryFormat)
err = enc.Encode(d)
if err != nil {
return PairRecord{}, err
return fmt.Errorf("StoreDeviceInfo: could not encode device info: %w", err)
}
key := ed25519.NewKeyFromSeed(seed.Seed)
return PairRecord{
Public: key.Public().(ed25519.PublicKey),
Private: key,
HostName: seed.Hostname,
}, err
return nil
}

func StorePairRecord(w io.Writer, p PairRecord) error {
enc := plist.NewEncoderForFormat(w, plist.BinaryFormat)
return enc.Encode(pairRecordData{
Seed: p.Private.Seed(),
Hostname: p.HostName,
})
}
func readSelfIdentity(p string) (SelfIdentity, error) {
content, err := os.ReadFile(p)
if err != nil {
return SelfIdentity{}, fmt.Errorf("readSelfIdentity: could not read file: %w", err)
}
var s selfIdentityInternal
_, err = plist.Unmarshal(content, &s)
if err != nil {
return SelfIdentity{}, fmt.Errorf("readSelfIdentity: could not parse plist content: %w", err)
}

type PairRecordStore struct {
p string
private := ed25519.NewKeyFromSeed(s.PrivateKey)

return SelfIdentity{
Identifier: s.Identifier,
Irk: s.Irk,
PrivateKey: private,
PublicKey: s.PublicKey,
}, nil
}

func NewPairRecordStore(directory string) PairRecordStore {
return PairRecordStore{p: directory}
func getOrCreateSelfIdentity(p string) (SelfIdentity, error) {
info, err := os.Stat(p)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return createSelfIdentity(p)
} else {
return SelfIdentity{}, fmt.Errorf("getOrCreateSelfIdentity: failed to get file info: %w", err)
}
}
if info.IsDir() {
return SelfIdentity{}, fmt.Errorf("getOrCreateSelfIdentity: '%s' is a directory", p)
}
return readSelfIdentity(p)
}

func (p PairRecordStore) Load(udid string) (PairRecord, error) {
f, err := os.Open(p.pairRecordPath(udid))
func createSelfIdentity(p string) (SelfIdentity, error) {
irk := make([]byte, 16)
_, _ = rand.Read(irk)

pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return PairRecord{}, err
return SelfIdentity{}, fmt.Errorf("createSelfIdentity: failed to create key pair: %w", err)
}
defer f.Close()

return ParsePairRecord(f)
}
si := selfIdentityInternal{
Identifier: strings.ToUpper(uuid.New().String()),
Irk: irk,
PrivateKey: priv.Seed(),
PublicKey: pub,
}

func (p PairRecordStore) Store(udid string, pr PairRecord) error {
f, err := os.OpenFile(p.pairRecordPath(udid), os.O_CREATE|os.O_WRONLY, 0o666)
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
return SelfIdentity{}, fmt.Errorf("createSelfIdentity: failed to open file for writing: %w", err)
}
defer f.Close()
return StorePairRecord(f, pr)
}

func (p PairRecordStore) pairRecordPath(udid string) string {
return path.Join(p.p, fmt.Sprintf("%s.plist", udid))
}

func (p PairRecordStore) LoadOrCreate(udid string) (PairRecord, error) {
pr, err := p.Load(udid)
enc := plist.NewEncoderForFormat(f, plist.BinaryFormat)
err = enc.Encode(si)
if err != nil {
log.WithError(err).Info("could load pair record. creating new one")
return NewPairRecord()
return SelfIdentity{}, fmt.Errorf("createSelfIdentity: failed to encode self identity as plist: %w", err)
}
return pr, nil

return SelfIdentity{
Identifier: si.Identifier,
Irk: si.Irk,
PrivateKey: priv,
PublicKey: pub,
}, nil
}
37 changes: 37 additions & 0 deletions ios/tunnel/pairrecord_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tunnel

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ed25519"
"howett.net/plist"
"os"
"path"
"testing"
)

func TestPairRecordManager(t *testing.T) {
tmp := t.TempDir()

pm, err := NewPairRecordManager(tmp)

require.NoError(t, err)

t.Run("self identity is created", func(t *testing.T) {
_, err := os.Stat(path.Join(tmp, "selfIdentity.plist"))
assert.NoError(t, err)
})
t.Run("read key equals the stored one", func(t *testing.T) {
siPath := path.Join(tmp, "selfIdentity.plist")
b, err := os.ReadFile(siPath)
require.NoError(t, err)

var si selfIdentityInternal
_, err = plist.Unmarshal(b, &si)
require.NoError(t, err)

private := ed25519.NewKeyFromSeed(si.PrivateKey)

assert.True(t, private.Equal(pm.SelfId.PrivateKey))
})
}
48 changes: 26 additions & 22 deletions ios/tunnel/untrusted.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ import (

const UntrustedTunnelServiceName = "com.apple.internal.dt.coredevice.untrusted.tunnelservice"

func NewTunnelServiceWithXpc(xpcConn *xpc.Connection, c io.Closer) (*TunnelService, error) {

var pairRecord PairRecord
k := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
pairRecord.Private = k
pairRecord.Public = k.Public().(ed25519.PublicKey)

return &TunnelService{xpcConn: xpcConn, c: c, controlChannel: newControlChannelReadWriter(xpcConn)}, nil
func NewTunnelServiceWithXpc(xpcConn *xpc.Connection, c io.Closer, pairRecords PairRecordManager) (*TunnelService, error) {
return &TunnelService{
xpcConn: xpcConn,
c: c,
controlChannel: newControlChannelReadWriter(xpcConn),
pairRecords: pairRecords,
}, nil
}

type TunnelService struct {
Expand All @@ -37,13 +36,15 @@ type TunnelService struct {

controlChannel *controlChannelReadWriter
cipher *cipherStream

pairRecords PairRecordManager
}

func (t *TunnelService) Close() error {
return t.c.Close()
}

func (t *TunnelService) Pair(pr PairRecord) error {
func (t *TunnelService) Pair() error {
err := t.controlChannel.writeRequest(map[string]interface{}{
"handshake": map[string]interface{}{
"_0": map[string]interface{}{
Expand All @@ -64,7 +65,7 @@ func (t *TunnelService) Pair(pr PairRecord) error {
return err
}

err = t.verifyPair(pr)
err = t.verifyPair()
if err == nil {
return nil
}
Expand All @@ -80,7 +81,7 @@ func (t *TunnelService) Pair(pr PairRecord) error {
return err
}

err = t.exchangeDeviceInfo(pr, sessionKey)
err = t.exchangeDeviceInfo(sessionKey)
if err != nil {
return err
}
Expand Down Expand Up @@ -236,7 +237,7 @@ func (t *TunnelService) createUnlockKey() ([]byte, error) {
return nil, err
}

func (t *TunnelService) verifyPair(pr PairRecord) error {
func (t *TunnelService) verifyPair() error {
key, _ := ecdh.X25519().GenerateKey(rand.Reader)
tlv := NewTlvBuffer()
tlv.WriteByte(TypeState, PairStateStartRequest)
Expand All @@ -248,6 +249,8 @@ func (t *TunnelService) verifyPair(pr PairRecord) error {
startNewSession: true,
}

selfId := t.pairRecords.SelfId

err := t.controlChannel.writeEvent(&event)

var devP pairingData
Expand Down Expand Up @@ -284,14 +287,14 @@ func (t *TunnelService) verifyPair(pr PairRecord) error {

signBuf := bytes.NewBuffer(nil)
signBuf.Write(key.PublicKey().Bytes())
signBuf.Write([]byte(pr.HostName))
signBuf.Write([]byte(selfId.Identifier))
signBuf.Write(devicePublicKeyBytes)

signature := ed25519.Sign(pr.Private, signBuf.Bytes())
signature := ed25519.Sign(selfId.PrivateKey, signBuf.Bytes())

cTlv := NewTlvBuffer()
cTlv.WriteData(TypeSignature, signature)
cTlv.WriteData(TypeIdentifier, []byte(pr.HostName))
cTlv.WriteData(TypeIdentifier, []byte(selfId.Identifier))

nonce := make([]byte, 12)
copy(nonce[4:], "PV-Msg03")
Expand Down Expand Up @@ -396,17 +399,17 @@ func (t *TunnelService) setupSessionKey() ([]byte, error) {
return srp.SessionKey, nil
}

func (t *TunnelService) exchangeDeviceInfo(pr PairRecord, sessionKey []byte) error {
func (t *TunnelService) exchangeDeviceInfo(sessionKey []byte) error {
hkdfPairSetup := hkdf.New(sha512.New, sessionKey, []byte("Pair-Setup-Controller-Sign-Salt"), []byte("Pair-Setup-Controller-Sign-Info"))
buf := bytes.NewBuffer(nil)
io.CopyN(buf, hkdfPairSetup, 32)
buf.WriteString(pr.HostName)
buf.Write(pr.Public)
buf.WriteString(t.pairRecords.SelfId.Identifier)
buf.Write(t.pairRecords.SelfId.PublicKey)

signature := ed25519.Sign(pr.Private, buf.Bytes())
signature := ed25519.Sign(t.pairRecords.SelfId.PrivateKey, buf.Bytes())

deviceInfo, err := opack.Encode(map[string]interface{}{
"accountID": pr.HostName,
"accountID": t.pairRecords.SelfId.Identifier,
"altIRK": []byte{0x5e, 0xca, 0x81, 0x91, 0x92, 0x02, 0x82, 0x00, 0x11, 0x22, 0x33, 0x44, 0xbb, 0xf2, 0x4a, 0xc8},
"btAddr": "FF:DD:99:66:BB:AA",
"mac": []byte{0xff, 0x44, 0x88, 0x66, 0x33, 0x99},
Expand All @@ -417,8 +420,8 @@ func (t *TunnelService) exchangeDeviceInfo(pr PairRecord, sessionKey []byte) err

deviceInfoTlv := NewTlvBuffer()
deviceInfoTlv.WriteData(TypeSignature, signature)
deviceInfoTlv.WriteData(TypePublicKey, pr.Public)
deviceInfoTlv.WriteData(TypeIdentifier, []byte(pr.HostName))
deviceInfoTlv.WriteData(TypePublicKey, t.pairRecords.SelfId.PublicKey)
deviceInfoTlv.WriteData(TypeIdentifier, []byte(t.pairRecords.SelfId.Identifier))
deviceInfoTlv.WriteData(TypeInfo, deviceInfo)

sessionKeyBuf := bytes.NewBuffer(nil)
Expand Down Expand Up @@ -462,6 +465,7 @@ func (t *TunnelService) exchangeDeviceInfo(pr PairRecord, sessionKey []byte) err
}
copy(nonce[4:], "PS-Msg06")
// the device info response from the device is not needed. we just make sure that there's no error decrypting it
// TODO: decode the opack encoded data and persist it using the PairRecordManager.StoreDeviceInfo method
_, err = cipher.Open(nil, nonce, encrData, nil)
if err != nil {
return err
Expand Down
Loading