private void downloadMediaFileIfChanged(File mediaDir, MediaFile m, FormStatus fs)
      throws Exception {

    File mediaFile = new File(mediaDir, m.filename);

    if (m.hash.startsWith(MD5_COLON_PREFIX)) {
      // see if the file exists and has the same hash
      String hashToMatch = m.hash.substring(MD5_COLON_PREFIX.length());
      if (mediaFile.exists()) {
        String hash = FileSystemUtils.getMd5Hash(mediaFile);
        if (hash.equalsIgnoreCase(hashToMatch)) return;
        mediaFile.delete();
      }
    }

    if (isCancelled()) {
      fs.setStatusString("aborting fetch of media file...", true);
      EventBus.publish(new FormStatusEvent(fs));
      throw new TransmissionException("Transfer cancelled by user.");
    }

    AggregateUtils.commonDownloadFile(serverInfo, mediaFile, m.downloadUrl);
  }
  private static boolean decryptSubmissionFiles(
      String base64EncryptedSymmetricKey,
      FormInstanceMetadata fim,
      List<String> mediaNames,
      String encryptedSubmissionFile,
      String base64EncryptedElementSignature,
      PrivateKey rsaPrivateKey,
      File instanceDir,
      File unencryptedDir)
      throws FileSystemException, CryptoException, ParsingException {

    EncryptionInformation ei =
        new EncryptionInformation(base64EncryptedSymmetricKey, fim.instanceId, rsaPrivateKey);

    byte[] elementDigest;
    try {
      // construct the base64-encoded RSA-encrypted symmetric key
      Cipher pkCipher;
      pkCipher = Cipher.getInstance(ASYMMETRIC_ALGORITHM);
      // extract digest
      pkCipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);
      byte[] encryptedElementSignature = Base64.decodeBase64(base64EncryptedElementSignature);
      elementDigest = pkCipher.doFinal(encryptedElementSignature);
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      throw new CryptoException(
          "Error decrypting base64EncryptedElementSignature Cause: " + e.toString());
    } catch (NoSuchPaddingException e) {
      e.printStackTrace();
      throw new CryptoException(
          "Error decrypting base64EncryptedElementSignature Cause: " + e.toString());
    } catch (InvalidKeyException e) {
      e.printStackTrace();
      throw new CryptoException(
          "Error decrypting base64EncryptedElementSignature Cause: " + e.toString());
    } catch (IllegalBlockSizeException e) {
      e.printStackTrace();
      throw new CryptoException(
          "Error decrypting base64EncryptedElementSignature Cause: " + e.toString());
    } catch (BadPaddingException e) {
      e.printStackTrace();
      throw new CryptoException(
          "Error decrypting base64EncryptedElementSignature Cause: " + e.toString());
    }

    // NOTE: will decrypt only the files in the media list, plus the encryptedSubmissionFile

    File[] allFiles = instanceDir.listFiles();
    List<File> filesToProcess = new ArrayList<File>();
    for (File f : allFiles) {
      if (mediaNames.contains(f.getName())) {
        filesToProcess.add(f);
      } else if (encryptedSubmissionFile.equals(f.getName())) {
        filesToProcess.add(f);
      }
    }

    // should have all media files plus one submission.xml.enc file
    if (filesToProcess.size() != mediaNames.size() + 1) {
      // figure out what we're missing...
      int lostFileCount = 0;
      List<String> missing = new ArrayList<String>();
      for (String name : mediaNames) {
        if (name == null) {
          // this was lost due to an pre-ODK Aggregate 1.4.5 mark-as-complete action
          ++lostFileCount;
          continue;
        }
        File f = new File(instanceDir, name);
        if (!filesToProcess.contains(f)) {
          missing.add(name);
        }
      }
      StringBuilder b = new StringBuilder();
      for (String name : missing) {
        b.append(" ").append(name);
      }
      if (!filesToProcess.contains(new File(instanceDir, encryptedSubmissionFile))) {
        b.append(" ").append(encryptedSubmissionFile);
        throw new FileSystemException(
            "Error decrypting: " + instanceDir.getName() + " Missing files:" + b.toString());
      } else {
        // ignore the fact that we don't have the lost files
        if (filesToProcess.size() + lostFileCount != mediaNames.size() + 1) {
          throw new FileSystemException(
              "Error decrypting: " + instanceDir.getName() + " Missing files:" + b.toString());
        }
      }
    }

    // decrypt the media files IN ORDER.
    for (String mediaName : mediaNames) {
      String displayedName = (mediaName == null) ? "<missing .enc file>" : mediaName;
      File f = (mediaName == null) ? null : new File(instanceDir, mediaName);
      try {
        decryptFile(ei, f, unencryptedDir);
      } catch (InvalidKeyException e) {
        e.printStackTrace();
        throw new CryptoException("Error decrypting:" + displayedName + " Cause: " + e.toString());
      } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
        throw new CryptoException("Error decrypting:" + displayedName + " Cause: " + e.toString());
      } catch (InvalidAlgorithmParameterException e) {
        e.printStackTrace();
        throw new CryptoException("Error decrypting:" + displayedName + " Cause: " + e.toString());
      } catch (NoSuchPaddingException e) {
        e.printStackTrace();
        throw new CryptoException("Error decrypting:" + displayedName + " Cause: " + e.toString());
      } catch (IOException e) {
        e.printStackTrace();
        throw new FileSystemException(
            "Error decrypting:" + displayedName + " Cause: " + e.toString());
      }
    }

    // decrypt the submission file
    File f = new File(instanceDir, encryptedSubmissionFile);
    try {
      decryptFile(ei, f, unencryptedDir);
    } catch (InvalidKeyException e) {
      e.printStackTrace();
      throw new CryptoException("Error decrypting:" + f.getName() + " Cause: " + e.toString());
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      throw new CryptoException("Error decrypting:" + f.getName() + " Cause: " + e.toString());
    } catch (InvalidAlgorithmParameterException e) {
      e.printStackTrace();
      throw new CryptoException("Error decrypting:" + f.getName() + " Cause: " + e.toString());
    } catch (NoSuchPaddingException e) {
      e.printStackTrace();
      throw new CryptoException("Error decrypting:" + f.getName() + " Cause: " + e.toString());
    } catch (IOException e) {
      e.printStackTrace();
      throw new FileSystemException("Error decrypting:" + f.getName() + " Cause: " + e.toString());
    }

    // get the FIM for the decrypted submission file
    File submissionFile =
        new File(
            unencryptedDir,
            encryptedSubmissionFile.substring(0, encryptedSubmissionFile.lastIndexOf(".enc")));

    FormInstanceMetadata submissionFim;
    try {
      Document subDoc = XmlManipulationUtils.parseXml(submissionFile);
      submissionFim = XmlManipulationUtils.getFormInstanceMetadata(subDoc.getRootElement());
    } catch (ParsingException e) {
      e.printStackTrace();
      throw new FileSystemException(
          "Error decrypting: " + submissionFile.getName() + " Cause: " + e.toString());
    } catch (FileSystemException e) {
      e.printStackTrace();
      throw new FileSystemException(
          "Error decrypting: " + submissionFile.getName() + " Cause: " + e.getMessage());
    }

    boolean same = submissionFim.xparam.formId.equals(fim.xparam.formId);

    if (!same) {
      throw new FileSystemException(
          "Error decrypting:"
              + unencryptedDir.getName()
              + " Cause: form instance metadata differs from that in manifest");
    }

    // Construct the element signature string
    StringBuilder b = new StringBuilder();
    appendElementSignatureSource(b, fim.xparam.formId);
    if (fim.xparam.modelVersion != null) {
      appendElementSignatureSource(b, Long.toString(fim.xparam.modelVersion));
    }
    appendElementSignatureSource(b, base64EncryptedSymmetricKey);

    appendElementSignatureSource(b, fim.instanceId);

    boolean missingFile = false;
    for (String encFilename : mediaNames) {
      if (encFilename == null) {
        missingFile = true;
        continue;
      }
      File decryptedFile =
          new File(unencryptedDir, encFilename.substring(0, encFilename.lastIndexOf(".enc")));
      if (decryptedFile.getName().endsWith(".missing")) {
        // this is a missing file -- we will not be able to
        // confirm the signature of the submission.
        missingFile = true;
        continue;
      }
      String md5 = FileSystemUtils.getMd5Hash(decryptedFile);
      appendElementSignatureSource(b, decryptedFile.getName() + "::" + md5);
    }

    String md5 = FileSystemUtils.getMd5Hash(submissionFile);
    appendElementSignatureSource(b, submissionFile.getName() + "::" + md5);

    // compute the digest of the element signature string
    byte[] messageDigest;
    try {
      MessageDigest md = MessageDigest.getInstance("MD5");
      md.update(b.toString().getBytes("UTF-8"));
      messageDigest = md.digest();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      throw new CryptoException("Error computing xml signature Cause: " + e.toString());
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
      throw new CryptoException("Error computing xml signature Cause: " + e.toString());
    }

    same = true;
    for (int i = 0; i < messageDigest.length; ++i) {
      if (messageDigest[i] != elementDigest[i]) {
        same = false;
        break;
      }
    }

    return same;
  }