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