public void generateXmlListOfForms(PrintWriter output, CallingContext cc)
      throws IOException, ODKDatastoreException {
    Document d = new Document();
    d.setStandalone(true);
    d.setEncoding(HtmlConsts.UTF8_ENCODE);
    Element e = d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.XFORMS_TAG);
    e.setPrefix(null, XML_TAG_NAMESPACE);
    d.addChild(0, Node.ELEMENT, e);
    int idx = 0;
    e.addChild(idx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);

    // build XML table of form information
    for (IForm form : forms) {
      if (!form.getDownloadEnabled()) continue;

      idx = generateFormXmlEntry(d, e, idx, form, cc);
    }

    KXmlSerializer serializer = new KXmlSerializer();
    serializer.setOutput(output);
    // setting the response content type emits the xml header.
    // just write the body here...
    d.writeChildren(serializer);
    serializer.flush();
  }
  private static void writeSubmissionManifest(
      EncryptedFormInformation formInfo, File submissionXml, List<File> mediaFiles)
      throws EncryptionException {

    Document d = new Document();
    d.setStandalone(true);
    d.setEncoding(UTF_8);
    Element e = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, DATA);
    e.setPrefix(null, XML_ENCRYPTED_TAG_NAMESPACE);
    e.setAttribute(null, ID, formInfo.formId);
    if (formInfo.formVersion != null) {
      e.setAttribute(null, VERSION, formInfo.formVersion);
    }
    e.setAttribute(null, ENCRYPTED, "yes");
    d.addChild(0, Node.ELEMENT, e);

    int idx = 0;
    Element c;
    c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, BASE64_ENCRYPTED_KEY);
    c.addChild(0, Node.TEXT, formInfo.base64RsaEncryptedSymmetricKey);
    e.addChild(idx++, Node.ELEMENT, c);

    c = d.createElement(XML_OPENROSA_NAMESPACE, META);
    c.setPrefix("orx", XML_OPENROSA_NAMESPACE);
    {
      Element instanceTag = d.createElement(XML_OPENROSA_NAMESPACE, INSTANCE_ID);
      instanceTag.addChild(0, Node.TEXT, formInfo.instanceMetadata.instanceId);
      c.addChild(0, Node.ELEMENT, instanceTag);
    }
    e.addChild(idx++, Node.ELEMENT, c);
    e.addChild(idx++, Node.IGNORABLE_WHITESPACE, NEW_LINE);

    if (mediaFiles != null) {
      for (File file : mediaFiles) {
        c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, MEDIA);
        Element fileTag = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, FILE);
        fileTag.addChild(0, Node.TEXT, file.getName() + ".enc");
        c.addChild(0, Node.ELEMENT, fileTag);
        e.addChild(idx++, Node.ELEMENT, c);
        e.addChild(idx++, Node.IGNORABLE_WHITESPACE, NEW_LINE);
      }
    }

    c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, ENCRYPTED_XML_FILE);
    c.addChild(0, Node.TEXT, submissionXml.getName() + ".enc");
    e.addChild(idx++, Node.ELEMENT, c);

    c = d.createElement(XML_ENCRYPTED_TAG_NAMESPACE, BASE64_ENCRYPTED_ELEMENT_SIGNATURE);
    c.addChild(0, Node.TEXT, formInfo.getBase64EncryptedElementSignature());
    e.addChild(idx++, Node.ELEMENT, c);

    FileOutputStream fout = null;
    OutputStreamWriter writer = null;
    try {
      fout = new FileOutputStream(submissionXml);
      writer = new OutputStreamWriter(fout, UTF_8);

      KXmlSerializer serializer = new KXmlSerializer();
      serializer.setOutput(writer);
      // setting the response content type emits the xml header.
      // just write the body here...
      d.writeChildren(serializer);
      serializer.flush();
      writer.flush();
      fout.getChannel().force(true);
      writer.close();
    } catch (Exception ex) {
      ex.printStackTrace();
      String msg =
          "Error writing submission.xml for encrypted submission: "
              + submissionXml.getParentFile().getName();
      Log.e(t, msg);
      throw new EncryptionException(msg, ex);
    } finally {
      IOUtils.closeQuietly(writer);
      IOUtils.closeQuietly(fout);
    }
  }
  public static DecryptOutcome decryptAndValidateSubmission(
      Document doc, PrivateKey rsaPrivateKey, File instanceDir, File unEncryptedDir)
      throws ParsingException, FileSystemException, CryptoException {

    Element rootElement = doc.getRootElement();

    String base64EncryptedSymmetricKey;
    String instanceIdMetadata = null;
    List<String> mediaNames = new ArrayList<String>();
    String encryptedSubmissionFile;
    String base64EncryptedElementSignature;

    {
      Element base64Key = null;
      Element base64Signature = null;
      Element encryptedXml = null;
      for (int i = 0; i < rootElement.getChildCount(); ++i) {
        if (rootElement.getType(i) == Node.ELEMENT) {
          Element child = rootElement.getElement(i);
          String name = child.getName();
          if (name.equals("base64EncryptedKey")) {
            base64Key = child;
          } else if (name.equals("base64EncryptedElementSignature")) {
            base64Signature = child;
          } else if (name.equals("encryptedXmlFile")) {
            encryptedXml = child;
          } else if (name.equals("media")) {
            Element media = child;
            for (int j = 0; j < media.getChildCount(); ++j) {
              if (media.getType(j) == Node.ELEMENT) {
                Element mediaChild = media.getElement(j);
                String mediaFileElementName = mediaChild.getName();
                if (mediaFileElementName.equals("file")) {
                  String mediaName = XFormParser.getXMLText(mediaChild, true);
                  if (mediaName == null || mediaName.length() == 0) {
                    mediaNames.add(null);
                  } else {
                    mediaNames.add(mediaName);
                  }
                }
              }
            }
          }
        }
      }

      // verify base64Key
      if (base64Key == null) {
        throw new ParsingException("Missing base64EncryptedKey element in encrypted form.");
      }
      base64EncryptedSymmetricKey = XFormParser.getXMLText(base64Key, true);

      // get instanceID out of OpenRosa meta block
      instanceIdMetadata = XmlManipulationUtils.getOpenRosaInstanceId(rootElement);
      if (instanceIdMetadata == null) {
        throw new ParsingException("Missing instanceID within meta block of encrypted form.");
      }

      // get submission filename
      if (encryptedXml == null) {
        throw new ParsingException("Missing encryptedXmlFile element in encrypted form.");
      }
      encryptedSubmissionFile = XFormParser.getXMLText(encryptedXml, true);
      if (base64Signature == null) {
        throw new ParsingException(
            "Missing base64EncryptedElementSignature element in encrypted form.");
      }
      base64EncryptedElementSignature = XFormParser.getXMLText(base64Signature, true);
    }

    if (instanceIdMetadata == null
        || base64EncryptedSymmetricKey == null
        || base64EncryptedElementSignature == null
        || encryptedSubmissionFile == null) {
      throw new ParsingException("Missing one or more required elements of encrypted form.");
    }

    FormInstanceMetadata fim;
    try {
      fim = XmlManipulationUtils.getFormInstanceMetadata(rootElement);
    } catch (ParsingException e) {
      e.printStackTrace();
      throw new ParsingException(
          "Unable to extract form instance medatadata from submission manifest. Cause: "
              + e.toString());
    }

    if (!instanceIdMetadata.equals(fim.instanceId)) {
      throw new ParsingException(
          "InstanceID within metadata does not match that on top level element.");
    }

    boolean isValidated =
        FileSystemUtils.decryptSubmissionFiles(
            base64EncryptedSymmetricKey,
            fim,
            mediaNames,
            encryptedSubmissionFile,
            base64EncryptedElementSignature,
            rsaPrivateKey,
            instanceDir,
            unEncryptedDir);

    // and change doc to be the decrypted submission document
    File decryptedSubmission = new File(unEncryptedDir, "submission.xml");
    doc = XmlManipulationUtils.parseXml(decryptedSubmission);
    if (doc == null) {
      return null;
    }

    // verify that the metadata matches between the manifest and the submission
    rootElement = doc.getRootElement();
    FormInstanceMetadata sim = XmlManipulationUtils.getFormInstanceMetadata(rootElement);
    if (!fim.xparam.equals(sim.xparam)) {
      throw new ParsingException(
          "FormId or version in decrypted submission does not match that in manifest!");
    }
    if (!fim.instanceId.equals(sim.instanceId)) {
      throw new ParsingException(
          "InstanceId in decrypted submission does not match that in manifest!");
    }

    return new DecryptOutcome(doc, isValidated);
  }
  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;
  }
  private int generateFormXmlEntry(Document d, Element e, int idx, IForm form, CallingContext cc)
      throws ODKDatastoreException {

    int xfIdx = 0;
    Element xformElement = d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.XFORM_TAG);
    e.addChild(idx++, Node.ELEMENT, xformElement);

    Element formIdElement = d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.FORM_ID_TAG);
    xformElement.addChild(xfIdx++, Node.ELEMENT, formIdElement);
    formIdElement.addChild(
        0,
        Node.TEXT,
        form.getFormId()
            .replace(ParserConsts.FORWARD_SLASH_SUBSTITUTION, ParserConsts.FORWARD_SLASH));
    xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);

    Element formNameElement = d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.FORM_NAME_TAG);
    xformElement.addChild(xfIdx++, Node.ELEMENT, formNameElement);
    formNameElement.addChild(0, Node.TEXT, form.getViewableName());
    xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);

    // transitional -- 1.1.6 and 1.1.7
    Element majorMinorVersionElement =
        d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.MAJOR_MINOR_VERSION_TAG);
    xformElement.addChild(xfIdx++, Node.ELEMENT, majorMinorVersionElement);
    majorMinorVersionElement.addChild(0, Node.TEXT, form.getMajorMinorVersionString());
    xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);

    // conforming OpenRosa 1.0
    Element versionElement = d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.VERSION_TAG);
    xformElement.addChild(xfIdx++, Node.ELEMENT, versionElement);
    versionElement.addChild(0, Node.TEXT, form.getOpenRosaVersionString());
    xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);

    Element hashElement = d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.HASH_TAG);
    xformElement.addChild(xfIdx++, Node.ELEMENT, hashElement);
    hashElement.addChild(0, Node.TEXT, form.getXFormFileHash(cc));
    xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);

    String description = form.getDescription();
    if (description != null && verbose) {
      Element descriptionElement =
          d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.DESCRIPTION_TEXT_TAG);
      xformElement.addChild(xfIdx++, Node.ELEMENT, descriptionElement);
      descriptionElement.addChild(0, Node.TEXT, description);
      xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);
    }
    String descriptionUrl = form.getDescriptionUrl();
    if (descriptionUrl != null && verbose) {
      Element descriptionUrlElement =
          d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.DESCRIPTION_URL_TAG);
      xformElement.addChild(xfIdx++, Node.ELEMENT, descriptionUrlElement);
      descriptionUrlElement.addChild(0, Node.TEXT, descriptionUrl);
      xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);
    }

    {
      Map<String, String> properties = new HashMap<String, String>();
      properties.put(ServletConsts.FORM_ID, form.getFormId());
      String urlLink = HtmlUtil.createLinkWithProperties(downloadRequestURL, properties);

      Element downloadUrlElement =
          d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.DOWNLOAD_URL_TAG);
      xformElement.addChild(xfIdx++, Node.ELEMENT, downloadUrlElement);
      downloadUrlElement.addChild(0, Node.TEXT, urlLink);
      xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);
    }

    if (form.hasManifestFileset(cc)) {
      Map<String, String> properties = new HashMap<String, String>();
      properties.put(ServletConsts.FORM_ID, form.getFormId());
      String urlLink = HtmlUtil.createLinkWithProperties(manifestRequestURL, properties);

      Element manifestUrlElement =
          d.createElement(XML_TAG_NAMESPACE, XFormsTableConsts.MANIFEST_URL_TAG);
      xformElement.addChild(xfIdx++, Node.ELEMENT, manifestUrlElement);
      manifestUrlElement.addChild(0, Node.TEXT, urlLink);
      xformElement.addChild(xfIdx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);
    }
    e.addChild(idx++, Node.IGNORABLE_WHITESPACE, BasicConsts.NEW_LINE);
    return idx;
  }