From ec2a1a649bfade3e72fe00ae5878ed8710c67eaa Mon Sep 17 00:00:00 2001 From: David Missmann Date: Fri, 22 Dec 2023 14:18:32 +0100 Subject: [PATCH] PairRecordManager uses the same structure as MacOS to store remote pair records --- ios/tunnel/pairrecord.go | 170 +++++++++++++++++++++------------- ios/tunnel/pairrecord_test.go | 37 ++++++++ ios/tunnel/untrusted.go | 48 +++++----- main.go | 20 +--- 4 files changed, 173 insertions(+), 102 deletions(-) create mode 100644 ios/tunnel/pairrecord_test.go diff --git a/ios/tunnel/pairrecord.go b/ios/tunnel/pairrecord.go index 21c425e5..0d1c03f2 100644 --- a/ios/tunnel/pairrecord.go +++ b/ios/tunnel/pairrecord.go @@ -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 } diff --git a/ios/tunnel/pairrecord_test.go b/ios/tunnel/pairrecord_test.go new file mode 100644 index 00000000..84d4c45d --- /dev/null +++ b/ios/tunnel/pairrecord_test.go @@ -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)) + }) +} diff --git a/ios/tunnel/untrusted.go b/ios/tunnel/untrusted.go index d84abe1f..12ccdfe0 100644 --- a/ios/tunnel/untrusted.go +++ b/ios/tunnel/untrusted.go @@ -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 { @@ -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{}{ @@ -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 } @@ -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 } @@ -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) @@ -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 @@ -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") @@ -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}, @@ -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) @@ -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 diff --git a/main.go b/main.go index e27553b4..958cf500 100644 --- a/main.go +++ b/main.go @@ -1936,6 +1936,8 @@ func pairDevice(device ios.DeviceEntry, orgIdentityP12File string, p12Password s } func startTunnel(device ios.DeviceEntry) { + pm, err := tunnel.NewPairRecordManager("/var/db/lockdown/RemotePairing/user_501") + exitIfError("could not creat pair record manager", err) ctx := context.Background() findCtx, _ := context.WithTimeout(ctx, 10*time.Second) addr, err := ios.FindDeviceInterfaceAddress(findCtx, device) @@ -1947,13 +1949,6 @@ func startTunnel(device ios.DeviceEntry) { handshakeResponse, err := rsdService.Handshake() exitIfError("could not execute RSD handshake", err) - home, err := os.UserHomeDir() - exitIfError("", err) - pairRecordsDir := path.Join(home, ".go-ios") - pairRecords := tunnel.NewPairRecordStore(pairRecordsDir) - err = os.MkdirAll(pairRecordsDir, os.ModePerm) - exitIfError("could not create go-ios dir", err) - port := handshakeResponse.GetPort(tunnel.UntrustedTunnelServiceName) if port == 0 { log.Fatal("could net get port for untrusted tunnel service") @@ -1963,17 +1958,10 @@ func startTunnel(device ios.DeviceEntry) { xpcConn, err := ios.CreateXpcConnection(h) exitIfError("", err) - ts, err := tunnel.NewTunnelServiceWithXpc(xpcConn, h) - - pr, err := pairRecords.LoadOrCreate(device.Properties.SerialNumber) - exitIfError("", err) - if err != nil { - log.WithError(err).Warn("could not store pair record") - } + ts, err := tunnel.NewTunnelServiceWithXpc(xpcConn, h, pm) - err = ts.Pair(pr) + err = ts.Pair() exitIfError("", err) - _ = pairRecords.Store(device.Properties.SerialNumber, pr) tunnelInfo, err := ts.CreateTunnelListener() exitIfError("", err) err = tunnel.ConnectToTunnel(ctx, tunnelInfo, addr)