content: 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();
      }
      
  }
  