public static final List<RemoteFormDefinition> retrieveAvailableFormsFromServer( ServerConnectionInfo serverInfo, TerminationFuture terminationFuture) throws XmlDocumentFetchException, ParsingException { AggregateUtils.DocumentFetchResult result = fetchFormList(serverInfo, true, terminationFuture); List<RemoteFormDefinition> formDefs = XmlManipulationUtils.parseFormListResponse(result.isOpenRosaResponse, result.doc); return formDefs; }
private String downloadManifestAndMediaFiles(File mediaDir, FormStatus fs) { RemoteFormDefinition fd = (RemoteFormDefinition) fs.getFormDefinition(); if (fd.getManifestUrl() == null) return null; fs.setStatusString("Fetching form manifest", true); EventBus.publish(new FormStatusEvent(fs)); List<MediaFile> files = new ArrayList<MediaFile>(); AggregateUtils.DocumentFetchResult result; try { DocumentDescription formManifestDescription = new DocumentDescription( "Fetch of manifest failed. Detailed reason: ", "Fetch of manifest failed ", "form manifest", terminationFuture); result = AggregateUtils.getXmlDocument( fd.getManifestUrl(), serverInfo, false, formManifestDescription, null); } catch (XmlDocumentFetchException e) { return e.getMessage(); } try { files = XmlManipulationUtils.parseFormManifestResponse(result.isOpenRosaResponse, result.doc); } catch (ParsingException e) { return e.getMessage(); } // OK we now have the full set of files to download... logger.info("Downloading " + files.size() + " media files."); int mCount = 0; if (files.size() > 0) { for (MediaFile m : files) { ++mCount; fs.setStatusString( String.format(" (getting %1$d of %2$d media files)", mCount, files.size()), true); EventBus.publish(new FormStatusEvent(fs)); try { downloadMediaFileIfChanged(mediaDir, m, fs); } catch (Exception e) { return e.getLocalizedMessage(); } } } return null; }
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 void downloadSubmission( File formInstancesDir, DatabaseUtils formDatabase, BriefcaseFormDefinition lfd, FormStatus fs, String uri) throws Exception { if (formDatabase.hasRecordedInstance(uri) != null) { logger.info("already present - skipping fetch: " + uri); return; } String formId = lfd.getSubmissionKey(uri); if (isCancelled()) { fs.setStatusString("aborting fetch of submission...", true); EventBus.publish(new FormStatusEvent(fs)); throw new SubmissionDownloadException("Transfer cancelled by user."); } String baseUrl = serverInfo.getUrl() + "/view/downloadSubmission"; Map<String, String> params = new HashMap<String, String>(); params.put("formId", formId); String fullUrl = WebUtils.createLinkWithProperties(baseUrl, params); AggregateUtils.DocumentFetchResult result; try { DocumentDescription submissionDescription = new DocumentDescription( "Fetch of a submission failed. Detailed error: ", "Fetch of a submission failed.", "submission", terminationFuture); result = AggregateUtils.getXmlDocument(fullUrl, serverInfo, false, submissionDescription, null); } catch (XmlDocumentFetchException e) { throw new SubmissionDownloadException(e.getMessage()); } // and parse the document... SubmissionManifest submissionManifest; try { submissionManifest = XmlManipulationUtils.parseDownloadSubmissionResponse(result.doc); } catch (ParsingException e) { throw new SubmissionDownloadException(e.getMessage()); } String msg = "Fetched instanceID=" + submissionManifest.instanceID; logger.info(msg); if (FileSystemUtils.hasFormSubmissionDirectory( formInstancesDir, submissionManifest.instanceID)) { // create instance directory... File instanceDir = FileSystemUtils.assertFormSubmissionDirectory( formInstancesDir, submissionManifest.instanceID); // fetch attachments for (MediaFile m : submissionManifest.attachmentList) { downloadMediaFileIfChanged(instanceDir, m, fs); } // write submission file -- we rely on instanceId being unique... File submissionFile = new File(instanceDir, "submission.xml"); OutputStreamWriter fo = new OutputStreamWriter(new FileOutputStream(submissionFile), "UTF-8"); fo.write(submissionManifest.submissionXml); fo.close(); // if we get here and it was a legacy server (0.9.x), we don't // actually know whether the submission was complete. Otherwise, // if we get here, we know that this is a completed submission // (because it was in /view/submissionList) and that we safely // copied it into the storage area (because we didn't get any // exceptions). if (serverInfo.isOpenRosaServer()) { formDatabase.assertRecordedInstanceDirectory(uri, instanceDir); } } else { // create instance directory... File instanceDir = FileSystemUtils.assertFormSubmissionDirectory( formInstancesDir, submissionManifest.instanceID); // fetch attachments for (MediaFile m : submissionManifest.attachmentList) { downloadMediaFileIfChanged(instanceDir, m, fs); } // write submission file File submissionFile = new File(instanceDir, "submission.xml"); OutputStreamWriter fo = new OutputStreamWriter(new FileOutputStream(submissionFile), "UTF-8"); fo.write(submissionManifest.submissionXml); fo.close(); // if we get here and it was a legacy server (0.9.x), we don't // actually know whether the submission was complete. Otherwise, // if we get here, we know that this is a completed submission // (because it was in /view/submissionList) and that we safely // copied it into the storage area (because we didn't get any // exceptions). if (serverInfo.isOpenRosaServer()) { formDatabase.assertRecordedInstanceDirectory(uri, instanceDir); } } }
private boolean downloadAllSubmissionsForForm( File formInstancesDir, DatabaseUtils formDatabase, BriefcaseFormDefinition lfd, FormStatus fs) { boolean allSuccessful = true; RemoteFormDefinition fd = (RemoteFormDefinition) fs.getFormDefinition(); int count = 1; String baseUrl = serverInfo.getUrl() + "/view/submissionList"; String oldWebsafeCursorString = "not-empty"; String websafeCursorString = ""; for (; !oldWebsafeCursorString.equals(websafeCursorString); ) { if (isCancelled()) { fs.setStatusString("aborting fetching submissions...", true); EventBus.publish(new FormStatusEvent(fs)); return false; } fs.setStatusString("retrieving next chunk of instances from server...", true); EventBus.publish(new FormStatusEvent(fs)); Map<String, String> params = new HashMap<String, String>(); params.put("numEntries", Integer.toString(MAX_ENTRIES)); params.put("formId", fd.getFormId()); params.put("cursor", websafeCursorString); String fullUrl = WebUtils.createLinkWithProperties(baseUrl, params); oldWebsafeCursorString = websafeCursorString; // remember what we had... AggregateUtils.DocumentFetchResult result; try { DocumentDescription submissionChunkDescription = new DocumentDescription( "Fetch of submission download chunk failed. Detailed error: ", "Fetch of submission download chunk failed.", "submission download chunk", terminationFuture); result = AggregateUtils.getXmlDocument( fullUrl, serverInfo, false, submissionChunkDescription, null); } catch (XmlDocumentFetchException e) { fs.setStatusString( "NOT ALL SUBMISSIONS RETRIEVED: Error fetching list of submissions: " + e.getMessage(), false); EventBus.publish(new FormStatusEvent(fs)); return false; } SubmissionDownloadChunk chunk; try { chunk = XmlManipulationUtils.parseSubmissionDownloadListResponse(result.doc); } catch (ParsingException e) { fs.setStatusString( "NOT ALL SUBMISSIONS RETRIEVED: Error parsing the list of submissions: " + e.getMessage(), false); EventBus.publish(new FormStatusEvent(fs)); return false; } websafeCursorString = chunk.websafeCursorString; for (String uri : chunk.uriList) { if (isCancelled()) { fs.setStatusString("aborting fetching submissions...", true); EventBus.publish(new FormStatusEvent(fs)); return false; } try { fs.setStatusString("fetching instance " + count++ + " ...", true); EventBus.publish(new FormStatusEvent(fs)); downloadSubmission(formInstancesDir, formDatabase, lfd, fs, uri); } catch (Exception e) { e.printStackTrace(); allSuccessful = false; fs.setStatusString( "SUBMISSION NOT RETRIEVED: Error fetching submission uri: " + uri + " details: " + e.getMessage(), false); EventBus.publish(new FormStatusEvent(fs)); // but try to get the next one... } } } return allSuccessful; }