19 package org.sleuthkit.autopsy.datasourceprocessors.xry;
 
   21 import java.io.IOException;
 
   22 import java.nio.file.Path;
 
   23 import java.time.format.DateTimeParseException;
 
   24 import java.util.ArrayList;
 
   25 import java.util.Collection;
 
   26 import java.util.HashSet;
 
   27 import java.util.List;
 
   28 import java.util.Objects;
 
   29 import java.util.Optional;
 
   31 import java.util.logging.Level;
 
   47 final class XRYMessagesFileParser 
implements XRYFileParser {
 
   49     private static final Logger logger = Logger.getLogger(
 
   50             XRYMessagesFileParser.class.getName());
 
   52     private static final String PARSER_NAME = 
"XRY DSP";
 
  105             } 
catch (IllegalArgumentException ex) {
 
  122             String normalizedName = name.trim().toLowerCase();
 
  124                 if (normalizedName.equals(keyChoice.name)) {
 
  129             throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found." 
  130                     + 
" All keys should be tested with contains.", name));
 
  157         public static boolean contains(String xryNamespace) {
 
  161             } 
catch (IllegalArgumentException ex) {
 
  179             String normalizedNamespace = xryNamespace.trim().toLowerCase();
 
  181                 if (normalizedNamespace.equals(keyChoice.name)) {
 
  186             throw new IllegalArgumentException(String.format(
"Namespace [%s] was not found." 
  187                     + 
" All namespaces should be tested with contains.", xryNamespace));
 
  220             } 
catch (IllegalArgumentException ex) {
 
  237             String normalizedName = name.trim().toLowerCase();
 
  239                 if (normalizedName.equals(keyChoice.name)) {
 
  244             throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found." 
  245                     + 
" All keys should be tested with contains.", name));
 
  272         Path reportPath = reader.getReportPath();
 
  273         logger.log(Level.INFO, String.format(
"[XRY DSP] Processing report at" 
  274                 + 
" [ %s ]", reportPath.toString()));
 
  277         Set<Integer> referenceNumbersSeen = 
new HashSet<>();
 
  279         while (reader.hasNextEntity()) {
 
  280             String xryEntity = reader.nextEntity();
 
  283             List<XRYKeyValuePair> pairs = getXRYKeyValuePairs(xryEntity, reader, referenceNumbersSeen);
 
  287             final String messageType = PARSER_NAME;
 
  289             String senderId = null;
 
  290             final List<String> recipientIdsList = 
new ArrayList<>();
 
  293             final String subject = null;
 
  295             final String threadId = null;
 
  296             final Collection<BlackboardAttribute> otherAttributes = 
new ArrayList<>();
 
  298             for (XRYKeyValuePair pair : pairs) {
 
  299                 XryNamespace 
namespace = XryNamespace.NONE;
 
  300                 if (XryNamespace.contains(pair.getNamespace())) {
 
  301                     namespace = XryNamespace.fromDisplayName(pair.getNamespace());
 
  303                 XryKey key = XryKey.fromDisplayName(pair.getKey());
 
  304                 String normalizedValue = pair.getValue().toLowerCase().trim();
 
  309                         if (!XRYUtils.isPhoneValid(pair.getValue())) {
 
  314                         if (
namespace == XryNamespace.FROM || direction == CommunicationDirection.
INCOMING) {
 
  315                             senderId = pair.getValue();
 
  316                         } 
else if (
namespace == XryNamespace.TO || direction == CommunicationDirection.
OUTGOING) {
 
  317                             recipientIdsList.add(pair.getValue());
 
  320                                 currentCase.getCommunicationsManager().createAccountFileInstance(
 
  321                                         Account.Type.PHONE, pair.getValue(), PARSER_NAME, parent, null, null);
 
  322                             } 
catch (InvalidAccountIDException ex) {
 
  323                                 logger.log(Level.WARNING, String.format(
"Invalid account identifier %s", pair.getValue()), ex);
 
  326                             otherAttributes.add(
new BlackboardAttribute(
 
  327                                     BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
 
  328                                     PARSER_NAME, pair.getValue()));
 
  334                         if (!XRYUtils.isPhoneValid(pair.getValue())) {
 
  338                         senderId = pair.getValue();
 
  341                         if (!XRYUtils.isPhoneValid(pair.getValue())) {
 
  345                         recipientIdsList.add(pair.getValue());
 
  350                         long dateTimeSinceInEpoch = XRYUtils.calculateSecondsSinceEpoch(pair.getValue());
 
  351                         dateTime = dateTimeSinceInEpoch;
 
  352                     } 
catch (DateTimeParseException ex) {
 
  353                         logger.log(Level.WARNING, String.format(
"[%s] Assumption" 
  354                                 + 
" about the date time formatting of messages is " 
  355                                 + 
"not right. Here is the pair [ %s ]", PARSER_NAME, pair), ex);
 
  359                         switch (normalizedValue) {
 
  361                                 direction = CommunicationDirection.
INCOMING;
 
  364                                 direction = CommunicationDirection.
OUTGOING;
 
  368                             case "status report":
 
  372                                 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized " 
  373                                         + 
" value for key pair [ %s ].", PARSER_NAME, pair));
 
  377                         switch (normalizedValue) {
 
  379                                 readStatus = MessageReadStatus.
READ;
 
  382                                 readStatus = MessageReadStatus.
UNREAD;
 
  385                                 otherAttributes.add(
new BlackboardAttribute(
 
  386                                         BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED,
 
  387                                         PARSER_NAME, pair.getValue()));
 
  389                             case "sending failed":
 
  395                                 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized " 
  396                                         + 
" value for key pair [ %s ].", PARSER_NAME, pair));
 
  401                         text = pair.getValue();
 
  404                         switch (normalizedValue) {
 
  406                                 direction = CommunicationDirection.
INCOMING;
 
  409                                 direction = CommunicationDirection.
OUTGOING;
 
  412                                 direction = CommunicationDirection.
UNKNOWN;
 
  417                         if (!XRYUtils.isPhoneValid(pair.getValue())) {
 
  421                         otherAttributes.add(
new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
 
  422                                 PARSER_NAME, pair.getValue()));
 
  427                         if (key.getType() != null) {
 
  428                             otherAttributes.add(
new BlackboardAttribute(key.getType(),
 
  429                                     PARSER_NAME, pair.getValue()));
 
  431                             logger.log(Level.INFO, String.format(
"[%s] Key value pair " 
  432                                     + 
"(in brackets) [ %s ] was recognized but " 
  433                                     + 
"more data or time is needed to finish implementation. Discarding... ",
 
  439             CommunicationArtifactsHelper helper = 
new CommunicationArtifactsHelper(
 
  440                     currentCase, PARSER_NAME, parent, Account.Type.PHONE, null);
 
  442             helper.addMessage(messageType, direction, senderId, recipientIdsList,
 
  443                     dateTime, readStatus, subject, text, threadId, otherAttributes);
 
  451     private List<XRYKeyValuePair> getXRYKeyValuePairs(String xryEntity,
 
  452             XRYFileReader reader, Set<Integer> referenceValues) 
throws IOException {
 
  453         String[] xryLines = xryEntity.split(
"\n");
 
  455         logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ]", xryLines[0]));
 
  457         List<XRYKeyValuePair> pairs = 
new ArrayList<>();
 
  460         int keyCount = getCountOfKeyValuePairs(xryLines);
 
  461         for (
int i = 1; i <= keyCount; i++) {
 
  464             XRYKeyValuePair pair = getKeyValuePairByIndex(xryLines, i).get();
 
  465             if (XryMetaKey.contains(pair.getKey())) {
 
  470             if (!XryKey.contains(pair.getKey())) {
 
  471                 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key, " 
  472                         + 
"value pair (in brackets) [ %s ], " 
  473                         + 
"was not recognized. Discarding...", pair));
 
  477             if (pair.getValue().isEmpty()) {
 
  478                 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key " 
  479                         + 
"(in brackets) [ %s ] was recognized, but the value " 
  480                         + 
"was empty. Discarding...", pair.getKey()));
 
  486             if (pair.hasKey(XryKey.TEXT.getDisplayName())
 
  487                     || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
 
  488                 String segmentedText = getSegmentedText(xryLines, reader, referenceValues);
 
  489                 pair = 
new XRYKeyValuePair(pair.getKey(),
 
  491                         pair.getValue() + 
" " + segmentedText,
 
  492                         pair.getNamespace());
 
  505     private Integer getCountOfKeyValuePairs(String[] xryEntity) {
 
  507         for (
int i = 1; i < xryEntity.length; i++) {
 
  508             if (XRYKeyValuePair.isPair(xryEntity[i])) {
 
  528     private String getSegmentedText(String[] xryEntity, XRYFileReader reader,
 
  529             Set<Integer> referenceNumbersSeen) 
throws IOException {
 
  530         Optional<Integer> referenceNumber = getMetaKeyValue(xryEntity, XryMetaKey.REFERENCE_NUMBER);
 
  532         if (!referenceNumber.isPresent()) {
 
  536         logger.log(Level.INFO, String.format(
"[XRY DSP] Message entity " 
  537                 + 
"appears to be segmented with reference number [ %d ]", referenceNumber.get()));
 
  539         if (referenceNumbersSeen.contains(referenceNumber.get())) {
 
  540             logger.log(Level.SEVERE, String.format(
"[XRY DSP] This reference [ %d ] has already " 
  541                     + 
"been seen. This means that the segments are not " 
  542                     + 
"contiguous. Any segments contiguous with this " 
  543                     + 
"one will be aggregated and another " 
  544                     + 
"(otherwise duplicate) artifact will be created.", referenceNumber.get()));
 
  547         referenceNumbersSeen.add(referenceNumber.get());
 
  549         Optional<Integer> segmentNumber = getMetaKeyValue(xryEntity, XryMetaKey.SEGMENT_NUMBER);
 
  550         if (!segmentNumber.isPresent()) {
 
  551             logger.log(Level.SEVERE, String.format(
"No segment " 
  552                     + 
"number was found on the message entity" 
  553                     + 
"with reference number [%d]", referenceNumber.get()));
 
  557         StringBuilder segmentedText = 
new StringBuilder();
 
  559         int currentSegmentNumber = segmentNumber.get();
 
  560         while (reader.hasNextEntity()) {
 
  562             String nextEntity = reader.peek();
 
  563             String[] nextEntityLines = nextEntity.split(
"\n");
 
  564             Optional<Integer> nextReferenceNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.REFERENCE_NUMBER);
 
  566             if (!nextReferenceNumber.isPresent()
 
  567                     || !Objects.equals(nextReferenceNumber, referenceNumber)) {
 
  576             Optional<Integer> nextSegmentNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.SEGMENT_NUMBER);
 
  578             logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ] " 
  579                     + 
"segment with reference number [ %d ]", nextEntityLines[0], referenceNumber.get()));
 
  581             if (!nextSegmentNumber.isPresent()) {
 
  582                 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Segment with reference" 
  583                         + 
" number [ %d ] did not have a segment number associated with it." 
  584                         + 
" It cannot be determined if the reconstructed text will be in order.", referenceNumber.get()));
 
  585             } 
else if (nextSegmentNumber.get() != currentSegmentNumber + 1) {
 
  586                 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Contiguous " 
  587                         + 
"segments are not ascending incrementally. Encountered " 
  588                         + 
"segment [ %d ] after segment [ %d ]. This means the reconstructed " 
  589                         + 
"text will be out of order.", nextSegmentNumber.get(), currentSegmentNumber));
 
  592             int keyCount = getCountOfKeyValuePairs(nextEntityLines);
 
  593             for (
int i = 1; i <= keyCount; i++) {
 
  594                 XRYKeyValuePair pair = getKeyValuePairByIndex(nextEntityLines, i).get();
 
  595                 if (pair.hasKey(XryKey.TEXT.getDisplayName())
 
  596                         || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
 
  597                     segmentedText.append(pair.getValue()).append(
' ');
 
  601             if (nextSegmentNumber.isPresent()) {
 
  602                 currentSegmentNumber = nextSegmentNumber.get();
 
  607         if (segmentedText.length() > 0) {
 
  608             segmentedText.setLength(segmentedText.length() - 1);
 
  611         return segmentedText.toString();
 
  622     private Optional<Integer> getMetaKeyValue(String[] xryLines, XryMetaKey metaKey) {
 
  623         for (String xryLine : xryLines) {
 
  624             if (!XRYKeyValuePair.isPair(xryLine)) {
 
  628             XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
 
  629             if (pair.hasKey(metaKey.getDisplayName())) {
 
  631                     return Optional.of(Integer.parseInt(pair.getValue()));
 
  632                 } 
catch (NumberFormatException ex) {
 
  633                     logger.log(Level.SEVERE, String.format(
"[XRY DSP] Value [ %s ] for " 
  634                             + 
"meta key [ %s ] was not an integer.", pair.getValue(), metaKey), ex);
 
  638         return Optional.empty();
 
  652     private Optional<XRYKeyValuePair> getKeyValuePairByIndex(String[] xryLines, 
int index) {
 
  654         String 
namespace = "";
 
  655         for (
int i = 1; i < xryLines.length; i++) {
 
  656             String xryLine = xryLines[i];
 
  657             if (XryNamespace.contains(xryLine)) {
 
  658                 namespace = xryLine.trim();
 
  662             if (!XRYKeyValuePair.isPair(xryLine)) {
 
  663                 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Expected a key value " 
  664                         + 
"pair on this line (in brackets) [ %s ], but one was not detected." 
  665                         + 
" Discarding...", xryLine));
 
  669             XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
 
  670             String value = pair.getValue();
 
  672             for (; (i + 1) < xryLines.length
 
  673                     && !XRYKeyValuePair.isPair(xryLines[i + 1])
 
  674                     && !XryNamespace.contains(xryLines[i + 1]); i++) {
 
  675                 String continuedValue = xryLines[i + 1].trim();
 
  677                 value = value + 
" " + continuedValue;
 
  680             pair = 
new XRYKeyValuePair(pair.getKey(), value, 
namespace);
 
  682             if (pairsParsed == index) {
 
  683                 return Optional.of(pair);
 
  687         return Optional.empty();
 
final BlackboardAttribute.ATTRIBUTE_TYPE type
XryNamespace(String name)
XryKey(String name, BlackboardAttribute.ATTRIBUTE_TYPE type)
static boolean contains(String xryNamespace)
BlackboardAttribute.ATTRIBUTE_TYPE getType()
static XryNamespace fromDisplayName(String xryNamespace)
static boolean contains(String name)
static XryKey fromDisplayName(String name)