package com.alexhulbert.icewind; import com.alexhulbert.icewind.autocol.EasyProto; import com.alexhulbert.icewind.autocol.InvalidResponseException; import com.alexhulbert.icewind.autocol.ProtoBuilder; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.FileUtils; import xmlwise.Plist; import xmlwise.XmlParseException; import org.catacombae.hfsexplorer.iphone.Keybag; /** * * @author Taconut */ public class iCloud { private static String fsep = File.separator; //Writing "File.separator" takes too long private Integer dsPrsID; private String mmeAuthToken; private String mobileBackupUrl; private String contentUrl; private LoadingBar prog = null; public iCloud(String appleID, String password) throws InvalidResponseException { this.authenticate(appleID, password); this.getAccountSettings(); } public iCloud(String appleID, String password, LoadingBar progressBar) throws InvalidResponseException { this.authenticate(appleID, password); this.getAccountSettings(); this.prog = progressBar; } /** * Restores a decrypted iCloud backup onto the device (Just a Proof of Concept for now) * @param sshPath path to where the device is mounted * @param backupPath path to where the iCloud backup is stored * @param listColor type of list used: false=blacklist, true=whitelist * @param aList app name blacklist/whitelist (depending on listColor) */ private static void restore(String sshPath, String backupPath, boolean listColor, List<String> aList) { for (File F_UID : new File(sshPath + "/var/mobile/Applications".replace("/", fsep)).listFiles()) { for (File plist : new File(F_UID.getPath() + "/Library/Prefrences".replace("/", fsep)).listFiles()) { String bid = plist.getName().substring(0, plist.getName().length() - 4); if (new File(backupPath + fsep + "AppDomain-" + bid).exists()) { Log.log("App found: " + bid + "!"); String shortName = ""; //Example: "Stable" for chrome or "Safari" for Safari for (File aPart : F_UID.listFiles()) { if (aPart.getName().endsWith(".app")) { shortName = aPart.getName().substring(0, aPart.getName().length() - 4); break; } } if (listColor == aList.contains(shortName)) { Log.log("Restoring data for \"" + shortName + ".app\"..."); try { FileUtils.copyDirectory(new File(backupPath + fsep + "AppDomain-" + bid), F_UID); } catch (IOException ex) { Log.log("ERROR: Could not copy data for \"" + shortName + ".app\""); ex.printStackTrace(); } Log.log("Done!"); } else { Log.log("Not restoring data for \"" + shortName + ".app.\""); } } else { Log.log("Match not found for \"" + bid + ",\" ignoring."); } } } } /** * Authenticates you with apple * @param appleID Username/Email * @param password Password for your AppleID * @return A plist containing your mmeAuthToken and dsPrsID */ private void authenticate(String appleID, String password) throws InvalidResponseException { ProtoBuilder pb = new ProtoBuilder(); pb.setHost("setup.icloud.com"); pb.setPath("/setup/authenticate/" + appleID); pb.addHeader("Authorization", "Basic " + Utils.encode(appleID, password)); String plist = pb.getResponse(); Map<String, Object> properties; try { properties = Plist.fromXml(plist); } catch(XmlParseException xpex) { throw new InvalidResponseException("Response was not properly encoded in XML", xpex); } dsPrsID = (Integer)((Map<String, Object>) properties.get("appleAccountInfo")).get("dsPrsID"); //Parse the dsPrsID out of the auth plist mmeAuthToken = (String)((Map<String, Object>)properties.get("tokens")).get("mmeAuthToken"); //Parse the mmeAuthToken out of the authentication plist } /** * Gets info on your account * @param dsPrsID The Destination Signaling Identifier * @param mmeAuthToken The Mobile Me Authentication Token * @return Information about your account */ private void getAccountSettings() throws InvalidResponseException { ProtoBuilder pb = new ProtoBuilder(); pb.setHost("setup.icloud.com"); pb.setPath("/setup/get_account_settings"); pb.addHeader("Authorization", "Basic " + Utils.encode(dsPrsID.toString(), mmeAuthToken)); String accountInfo = pb.getResponse(); Map<String, Object> properties; try { properties = Plist.fromXml(accountInfo); } catch (XmlParseException xpex) { throw new InvalidResponseException("Response was not properly encoded in XML", xpex); } Map<String, Object> mobileMe = (Map<String, Object>) properties.get("com.apple.mobileme"); mobileBackupUrl = ((Map<String, Object>) mobileMe.get("com.apple.Dataclass.Backup")).get("url").toString() .replaceAll("https://(p[0-9]+-mobilebackup.icloud.com):443$", "$1"); //Get MobileBackup URL contentUrl = ((Map<String, Object>) mobileMe.get("com.apple.Dataclass.Content")).get("url").toString() .replaceAll("https://(p[0-9]+-content.icloud.com):443$", "$1"); //Get Content URL } /** * Gets the udids of the devices that have made backups on the account * @return A list of udids linked with the account (encoded with Protocol Buffers) * @throws com.alexhulbert.icewind.autocol.InvalidResponseException Invalid response to the protobuf */ public Protocol.DeviceUdids listDevices() throws InvalidResponseException { ProtoBuilder pb = new ProtoBuilder(); pb.setHost(mobileBackupUrl); pb.setPath("/mbs/" + dsPrsID); pb.addHeader("Authorization", "Basic " + Utils.encode(dsPrsID.toString(), mmeAuthToken)); try { return pb.build(Protocol.DeviceUdids.PARSER).parse(); } catch (InvalidProtocolBufferException ipbe) { throw new InvalidResponseException("Response was not properly encoded in Protobuf format", ipbe); } } /** * Gets device UDID with specific index. Useful for iterating through all the devices * @param index the index at which to retrieve the UDID. Starts at zero. * @return A UDID at that index * @throws InvalidResponseException */ public String listDevices(int index) throws InvalidResponseException { return Utils.bytesToHex(listDevices().getUdids(index).toByteArray()); } /** * Gets information about the specified UDID * @param backupUDID The udid of the device to retrieve info on * @throws com.alexhulbert.icewind.autocol.InvalidResponseException Invalid response to the protobuf * @return Information of the device (encoded with protobuf) */ public Protocol.Device getBackup(String backupUDID) throws InvalidResponseException { ProtoBuilder pb = new ProtoBuilder(); pb.setHost(mobileBackupUrl); pb.setPath("/mbs/" + dsPrsID + "/" + backupUDID); pb.addHeader("Authorization", "X-MobileMe-AuthToken " + Utils.encode(dsPrsID.toString(), mmeAuthToken)); try { return pb.build(Protocol.Device.PARSER).parse(); } catch (InvalidProtocolBufferException ipbe) { throw new InvalidResponseException("Response was not properly encoded in Protobuf format", ipbe); } } /** * Gets the keys that will used for decrypting iCloud data * @param backupUDID The udid of the device to retrieve info on * @return Keys for decrypting icloud data (encoded with protobuf) * @throws com.alexhulbert.icewind.autocol.InvalidResponseException Invalid response to the protobuf */ public Protocol.Keys getKeys(String backupUDID) throws InvalidResponseException { ProtoBuilder pb = new ProtoBuilder(); pb.setHost(mobileBackupUrl); pb.setPath("/mbs/" + dsPrsID + "/" + backupUDID + "/getKeys"); pb.addHeader("Authorization", "X-MobileMe-AuthToken " + Utils.encode(dsPrsID.toString(), mmeAuthToken)); try { return pb.build(Protocol.Keys.PARSER).parse(); } catch (InvalidProtocolBufferException ipbe) { throw new InvalidResponseException("Response was not properly encoded in Protobuf format", ipbe); } } /** * Gets Uris for the iCloud backup data * @param backupUDID The UDID of the device to retrieve info on * @param snapshotID The ID of the backup to get * @param offset Where to start * @param limit Max length * @throws com.alexhulbert.icewind.autocol.InvalidResponseException Invalid response to the protobuf * @return A list of files to download */ public Protocol.File[] listFiles(String backupUDID, int snapshotID, int offset, Long limit) throws InvalidResponseException { ProtoBuilder pb = new ProtoBuilder(); pb.setHost(mobileBackupUrl); pb.setPath(String.format("/mbs/%s/%s/%s/listFiles?offset=%s&limit=%s", dsPrsID.toString(), backupUDID, snapshotID, offset, (limit == null) ? "65536" : String.valueOf(limit))); pb.addHeader("Authorization", "X-MobileMe-AuthToken " + Utils.encode(dsPrsID.toString(), mmeAuthToken)); EasyProto<Protocol.File> ep = pb.build(Protocol.File.PARSER); if (prog != null) { prog.activate(true); prog.intermediate(); prog.status("Calculating File Size..."); //Just like Windows Explorer! Protocol.File[] response = ep.parseVarint(prog, "Parsing File List").toArray(new Protocol.File[0]); prog.activate(false); return response; } else { return ep.parseVarint().toArray(new Protocol.File[0]); } } /** * Gets keys, info, etc. from files * @param auch GetFiles Response * @param hashDict Hash Dictionary * @return Invalid response to the protobuf * @throws com.alexhulbert.icewind.autocol.InvalidResponseException Invalid response to the protobuf */ public Protocol.AuthorizeGet authorizeGet(Protocol.AuthChunk[] auch, Map<ByteString, ByteString> hashDict) throws InvalidResponseException { Protocol.FileAuth.Builder builder = Protocol.FileAuth.newBuilder(); for (Protocol.AuthChunk file : auch) { Protocol.AuthChunk.Builder subBuilder = Protocol.AuthChunk.newBuilder(); subBuilder.setAuthToken(file.getAuthToken()); subBuilder.setChecksum(hashDict.get(file.getChecksum())); builder.addMain(subBuilder.build()); } ProtoBuilder pb = new ProtoBuilder(); pb.setHost(contentUrl); pb.setPath("/" + dsPrsID + "/authorizeGet"); pb.setBody(builder.build().toByteArray()); pb.addHeader("Accept", "*/*"); pb.addHeader("User-Agent", "MobileBackup/5.1.1 (9B206; iPhone4,1)"); pb.addHeader("X-Apple-Request-UUID", "900DFACE-BABE-C001-A550-B00B1E52C0DE"); //Now that's what I call magic hex. pb.addHeader("X-Apple-mmcs-Proto-Version", "3.3"); pb.addHeader("X-Apple-mme-dsid", dsPrsID.toString()); pb.addHeader("X-Apple-mmcs-DataClass", "com.apple.Dataclass.Backup"); pb.addHeader("X-mme-Client-Info", "<iPhone4,1> <iPhone OS;5.1.1;9B206> <com.apple.AppleAccount/1.0 (com.apple.backupd/(null))>"); pb.addHeader("X-Apple-mmcs-auth", Utils.bytesToHex(hashDict.get(auch[0].getChecksum()).toByteArray()).concat(" ") + auch[0].getAuthToken()); try { return pb.build(Protocol.AuthorizeGet.PARSER).parse(); } catch (InvalidProtocolBufferException ipbe) { throw new InvalidResponseException("Response was not properly encoded in Protobuf format", ipbe); } } /** * Gets Auth tokens for files (used for verification in authorizeGet) * @param files ListFiles Response * @param backupUDID UDID of the device * @param snapshotID Id of the backup * @return A list of file auth tokens * @throws com.alexhulbert.icewind.autocol.InvalidResponseException Invalid response to the protobuf */ public Protocol.AuthChunk[] getFiles(Protocol.File[] files, String backupUDID, int snapshotID) throws InvalidResponseException { ByteArrayOutputStream oust = new ByteArrayOutputStream(); for (Protocol.File f : files) { if (f == null || f.getFileSize() == 0) { continue; } Protocol.GetFiles.Builder instance = Protocol.GetFiles.newBuilder(); instance.setHash(f.getFileName()); try { instance.build().writeDelimitedTo(oust); } catch (IOException e) { e.printStackTrace(); //errorhandle: I guess user's not getting their flappy bird save. } } ProtoBuilder pb = new ProtoBuilder(); pb.setHost(mobileBackupUrl); pb.setPath(String.format("/mbs/%s/%s/%s/getFiles", dsPrsID, backupUDID, snapshotID)); pb.addHeader("Authorization", "Basic " + Utils.encode(dsPrsID.toString(), mmeAuthToken)); pb.setBody(oust.toByteArray()); return pb.build(Protocol.AuthChunk.PARSER).parseVarint().toArray(new Protocol.AuthChunk[0]); } public static Map<ByteString, ByteString> buildHashDictionary(Protocol.File[] sources) { Map<ByteString, ByteString> dict = new HashMap<ByteString, ByteString>(); //A HashMap of mapped Hashes for (Protocol.File sauce : sources) { dict.put(sauce.getFileName(), sauce.getAltFileName()); } return dict; } public void downloadBackup(String backupUDID, String outputFolder) throws Exception { int sid = this.getBackup(backupUDID).getBackup(0).getSnapshotID(); Protocol.File[] files = this.listFiles( backupUDID, sid, 0, (long) (1<<16 - 1) ); Protocol.Keys keys = this.getKeys(backupUDID); Keybag kbag = new Keybag(keys.getKeySet(1).getDataBytes().toByteArray()); byte[] passcode = keys.getKeySet(0).getDataBytes().toByteArray(); kbag.unlockBackupKeybagWithPasscode(passcode); Utils.noop(); } }