diff --git a/src/fruity/device-monitor-macos.vala b/src/fruity/device-monitor-macos.vala index 24ecf96b0..e73916c13 100644 --- a/src/fruity/device-monitor-macos.vala +++ b/src/fruity/device-monitor-macos.vala @@ -129,6 +129,7 @@ namespace Frida.Fruity { private ConnectionType _connection_type; private string _udid; private string _name; + private Bytes? _remote_unlock_host_key; private Promise? tunnel_request; @@ -153,6 +154,13 @@ namespace Frida.Fruity { var reader = new XpcObjectReader (device_info); _name = reader.read_member ("name").get_string_value (); reader.end_member (); + + if (reader.has_member ("remoteUnlockHostKey")) { + _remote_unlock_host_key = new Bytes (reader.read_member ("remoteUnlockHostKey").get_data_value ()); + reader.end_member (); + } else { + _remote_unlock_host_key = null; + } } private void on_state_changed (Object obj, ParamSpec pspec) { @@ -224,7 +232,7 @@ namespace Frida.Fruity { tunnel_request = new Promise (); try { - var tunnel = new MacOSTunnel (pairing_device); + var tunnel = new MacOSTunnel (pairing_device, _remote_unlock_host_key); yield tunnel.attach (cancellable); tunnel_request.resolve (tunnel); @@ -252,6 +260,12 @@ namespace Frida.Fruity { } } + public Bytes? remote_unlock_host_key { + get { + return _remote_unlock_host_key; + } + } + public Darwin.Xpc.Uuid? assertion_identifier { get { return _assertion_identifier; @@ -259,13 +273,15 @@ namespace Frida.Fruity { } private XpcClient pairing_device; + private Bytes? _remote_unlock_host_key; private Darwin.Xpc.Uuid? _assertion_identifier; private InetAddress? tunnel_device_address; private DiscoveryService? _discovery; private int64 _opened_at = -1; - public MacOSTunnel (XpcClient pairing_device) { + public MacOSTunnel (XpcClient pairing_device, Bytes? remote_unlock_host_key) { this.pairing_device = pairing_device; + _remote_unlock_host_key = remote_unlock_host_key; } public async void close (Cancellable? cancellable) throws IOError { diff --git a/src/fruity/device-monitor.vala b/src/fruity/device-monitor.vala index d4f99d5be..798620609 100644 --- a/src/fruity/device-monitor.vala +++ b/src/fruity/device-monitor.vala @@ -247,6 +247,9 @@ namespace Frida.Fruity { checkin.set_string ("Request", "RSDCheckin"); checkin.set_string ("Label", "Xcode"); checkin.set_string ("ProtocolVersion", "2"); + unowned Bytes? key = tunnel.remote_unlock_host_key; + if (key != null) + checkin.set_bytes ("EscrowBag", key); try { yield service.query (checkin, cancellable); @@ -496,6 +499,10 @@ namespace Frida.Fruity { get; } + public abstract Bytes? remote_unlock_host_key { + get; + } + public abstract async void close (Cancellable? cancellable) throws IOError; public abstract async IOStream open_tcp_connection (uint16 port, Cancellable? cancellable) throws Error, IOError; } @@ -1675,10 +1682,17 @@ namespace Frida.Fruity { } } + public Bytes? remote_unlock_host_key { + get { + return _remote_unlock_host_key; + } + } + private UsbNcmDriver? ncm; private TunnelConnection? tunnel_connection; private DiscoveryService? _discovery_service; private int64 _opened_at = -1; + private Bytes? _remote_unlock_host_key; public PortableUsbTunnel (UsbDevice device, NcmPeer peer, PairingStore store) { Object ( @@ -1707,11 +1721,11 @@ namespace Frida.Fruity { ); var pairing_transport = new XpcPairingTransport (yield netstack.open_tcp_connection (tunnel_endpoint, cancellable)); var pairing_service = yield PairingService.open (pairing_transport, pairing_store, cancellable); - TunnelConnection tc = yield pairing_service.open_tunnel (ncm_peer.ip, netstack, cancellable); tc.closed.connect (on_tunnel_connection_close); _opened_at = get_monotonic_time (); + _remote_unlock_host_key = pairing_service.established_peer.remote_unlock_host_key; var rsd_endpoint = (InetSocketAddress) Object.new (typeof (InetSocketAddress), address: tc.remote_address, @@ -1880,9 +1894,16 @@ namespace Frida.Fruity { } } + public Bytes? remote_unlock_host_key { + get { + return _remote_unlock_host_key; + } + } + private TunnelConnection? tunnel_connection; private DiscoveryService? _discovery_service; private int64 _opened_at = -1; + private Bytes? _remote_unlock_host_key; private const uint PAIRING_CONNECTION_TIMEOUT = 2000; @@ -1905,6 +1926,7 @@ namespace Frida.Fruity { TunnelConnection tc = yield pairing_service.open_tunnel (endpoint.get_address (), netstack, cancellable); _opened_at = get_monotonic_time (); + _remote_unlock_host_key = pairing_service.established_peer.remote_unlock_host_key; var rsd_endpoint = (InetSocketAddress) Object.new (typeof (InetSocketAddress), address: tc.remote_address, diff --git a/src/fruity/xpc.vala b/src/fruity/xpc.vala index 1d018caf4..e13701b1b 100644 --- a/src/fruity/xpc.vala +++ b/src/fruity/xpc.vala @@ -146,6 +146,11 @@ namespace Frida.Fruity { private set; } + public PairingPeer? established_peer { + get; + private set; + } + private Gee.Map> requests = new Gee.HashMap> (Numeric.uint64_hash, Numeric.uint64_equal); private uint64 next_control_sequence_number = 0; @@ -181,15 +186,35 @@ namespace Frida.Fruity { yield attempt_pair_verify (cancellable); - Bytes? shared_key = yield verify_manual_pairing (cancellable); - if (shared_key == null) { + PairingPeer? peer = yield verify_manual_pairing (cancellable); + if (peer == null) { if (!device_options.allows_pair_setup) throw new Error.NOT_SUPPORTED ("Device not paired and pairing not allowed on current transport"); - shared_key = yield setup_manual_pairing (cancellable); + peer = yield setup_manual_pairing (cancellable); } - client_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ClientEncrypt-main")); - server_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ServerEncrypt-main")); + if (peer.remote_unlock_host_key == null) { + bool peer_modified = false; + try { + peer.remote_unlock_host_key = yield create_remote_unlock_key (cancellable); + peer_modified = true; + } catch (Error e) { + if (e is Error.INVALID_OPERATION) { + store.forget_peer (peer); + throw e; + } + if (!(e is Error.NOT_SUPPORTED)) + throw e; + } + if (peer_modified) { + try { + store.save_peer (peer); + } catch (Error e) { + } + } + } + + established_peer = peer; return true; } @@ -377,7 +402,7 @@ namespace Frida.Fruity { } } - private async Bytes? verify_manual_pairing (Cancellable? cancellable) throws Error, IOError { + private async PairingPeer? verify_manual_pairing (Cancellable? cancellable) throws Error, IOError { Key host_keypair = make_keypair (X25519); uint8[] raw_host_pubkey = get_raw_public_key (host_keypair).get_data (); @@ -414,10 +439,18 @@ namespace Frida.Fruity { var cipher = new ChaCha20Poly1305 (operation_key); - // TODO: Verify signature using peer's public key. - /* var start_inner_response = */ new VariantReader (PairingParamsParser.parse (cipher.decrypt ( + var start_inner_response = new VariantReader (PairingParamsParser.parse (cipher.decrypt ( new Bytes.static ("\x00\x00\x00\x00PV-Msg02".data[:12]), start_response.read_member ("encrypted-data").get_data_value ()))); + string peer_identifier = start_inner_response + .read_member ("identifier") + .get_uuid_value (); + PairingPeer? peer = store.find_peer_by_identifier (peer_identifier); + if (peer == null) { + yield notify_pair_verify_failed (cancellable); + return null; + } + // TODO: Verify signature using peer's public key. unowned string host_identifier = store.self_identity.identifier; @@ -457,7 +490,9 @@ namespace Frida.Fruity { return null; } - return shared_key; + setup_main_encryption_keys (shared_key); + + return peer; } private async void notify_pair_verify_failed (Cancellable? cancellable) throws Error, IOError { @@ -476,7 +511,7 @@ namespace Frida.Fruity { .build (), cancellable); } - private async Bytes setup_manual_pairing (Cancellable? cancellable) throws Error, IOError { + private async PairingPeer setup_manual_pairing (Cancellable? cancellable) throws Error, IOError { Bytes start_params = new PairingParamsBuilder () .add_method (0) .add_state (1) @@ -612,9 +647,78 @@ namespace Frida.Fruity { inner_finish_response.end_member (); Bytes peer_info = inner_finish_response.read_member ("info").get_data_value (); inner_finish_response.end_member (); - store.add_peer (peer_identifier, peer_pubkey, peer_info); - return shared_key; + PairingPeer peer = store.add_peer (peer_identifier, peer_pubkey, peer_info); + + setup_main_encryption_keys (shared_key); + + return peer; + } + + private async Bytes? create_remote_unlock_key (Cancellable? cancellable) throws Error, IOError { + string request = Json.to_string ( + new Json.Builder () + .begin_object () + .set_member_name ("request") + .begin_object () + .set_member_name ("_0") + .begin_object () + .set_member_name ("createRemoteUnlockKey") + .begin_object () + .end_object () + .end_object () + .end_object () + .end_object () + .get_root (), false); + + string response = yield request_encrypted (request, cancellable); + + Json.Reader reader; + try { + reader = new Json.Reader (Json.from_string (response)); + } catch (GLib.Error e) { + throw new Error.PROTOCOL ("Invalid createRemoteUnlockKey response JSON"); + } + + reader.read_member ("response"); + reader.read_member ("_1"); + + if (reader.read_member ("errorExtended")) { + reader.read_member ("_0"); + + reader.read_member ("domain"); + unowned string? domain = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("code"); + int64 code = reader.get_int_value (); + reader.end_member (); + + reader.read_member ("userInfo"); + reader.read_member ("NSLocalizedDescription"); + unowned string? description = reader.get_string_value (); + + if (domain == null || description == null) + throw new Error.PROTOCOL ("Invalid createRemoteUnlockKey error response"); + + if (domain == "com.apple.CoreDevice.ControlChannelConnectionError" && code == 2) + throw new Error.INVALID_OPERATION ("%s", description); + + throw new Error.NOT_SUPPORTED ("%s", description); + } + reader.end_member (); + + reader.read_member ("createRemoteUnlockKey"); + reader.read_member ("hostKey"); + unowned string? key = reader.get_string_value (); + if (key == null) + throw new Error.PROTOCOL ("Malformed createRemoteUnlockKey response"); + return new Bytes (Base64.decode (key)); + } + + private void setup_main_encryption_keys (Bytes shared_key) { + client_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ClientEncrypt-main")); + server_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ServerEncrypt-main")); } private async ObjectReader request_pairing_data (Bytes payload, Cancellable? cancellable) throws Error, IOError { @@ -1383,7 +1487,7 @@ namespace Frida.Fruity { _peers = load_peers (); } - public void add_peer (string identifier, Bytes public_key, Bytes info) throws Error { + public PairingPeer add_peer (string identifier, Bytes public_key, Bytes info) throws Error { var r = new VariantReader (OpackParser.parse (info)); Bytes irk = r.read_member ("altIRK").get_data_value (); @@ -1406,6 +1510,7 @@ namespace Frida.Fruity { model = model, udid = udid, info = info, + remote_unlock_host_key = null, }; _peers.add (peer); @@ -1413,6 +1518,12 @@ namespace Frida.Fruity { save_peer (peer); } catch (Error e) { } + + return peer; + } + + public PairingPeer? find_peer_by_identifier (string identifier) { + return peers.first_match (p => p.identifier == identifier); } public PairingPeer? find_peer_matching_service (PairingServiceDetails service) { @@ -1480,6 +1591,10 @@ namespace Frida.Fruity { var r = new VariantReader (OpackParser.parse (info)); unowned string udid = r.read_member ("remotepairing_udid").get_string_value (); + Bytes? remote_unlock_host_key = null; + if (plist.has ("remoteUnlockHostKey")) + remote_unlock_host_key = plist.get_bytes ("remoteUnlockHostKey"); + peers.add (new PairingPeer () { identifier = plist.get_string ("identifier"), public_key = plist.get_bytes ("publicKey"), @@ -1488,6 +1603,7 @@ namespace Frida.Fruity { model = plist.get_string ("model"), udid = udid, info = info, + remote_unlock_host_key = remote_unlock_host_key, }); } } catch (GLib.Error e) { @@ -1496,7 +1612,7 @@ namespace Frida.Fruity { return peers; } - private static void save_peer (PairingPeer peer) throws Error { + public void save_peer (PairingPeer peer) throws Error { var plist = new Plist (); plist.set_string ("identifier", peer.identifier); plist.set_bytes ("publicKey", peer.public_key); @@ -1504,13 +1620,29 @@ namespace Frida.Fruity { plist.set_string ("name", peer.name); plist.set_string ("model", peer.model); plist.set_bytes ("info", peer.info); - save_plist (plist, query_peers_location ().get_child (peer.identifier + ".plist")); + if (peer.remote_unlock_host_key != null) + plist.set_bytes ("remoteUnlockHostKey", peer.remote_unlock_host_key); + + save_plist (plist, query_peer_location (peer)); + } + + public void forget_peer (PairingPeer peer) { + try { + query_peer_location (peer).delete (); + } catch (GLib.Error e) { + } + + _peers.remove (peer); } private static File query_self_identity_location () { return query_base_location ().get_child ("self-identity.plist"); } + private static File query_peer_location (PairingPeer peer) { + return query_peers_location ().get_child (peer.identifier + ".plist"); + } + private static File query_peers_location () { return query_base_location ().get_child ("peers"); } @@ -1557,6 +1689,7 @@ namespace Frida.Fruity { public string model; public string udid; public Bytes info; + public Bytes? remote_unlock_host_key; } public class PairingServiceMetadata {