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; }