""" Autopsy Forensic Browser Copyright 2019 Basis Technology Corp. Contact: carrier sleuthkit org Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from java.io import File from java.lang import Class from java.lang import ClassNotFoundException from java.lang import Long from java.lang import String from java.sql import ResultSet from java.sql import SQLException from java.sql import Statement from java.util.logging import Level from java.util import ArrayList from org.apache.commons.codec.binary import Base64 from org.sleuthkit.autopsy.casemodule import Case from org.sleuthkit.autopsy.coreutils import Logger from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil from org.sleuthkit.autopsy.coreutils import AppSQLiteDB from org.sleuthkit.autopsy.datamodel import ContentUtils from org.sleuthkit.autopsy.ingest import IngestJobContext from org.sleuthkit.datamodel import AbstractFile from org.sleuthkit.datamodel import BlackboardArtifact from org.sleuthkit.datamodel import BlackboardAttribute from org.sleuthkit.datamodel import Content from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection from TskMessagesParser import TskMessagesParser from TskContactsParser import TskContactsParser from TskCallLogsParser import TskCallLogsParser import traceback import general class SkypeAnalyzer(general.AndroidComponentAnalyzer): """ Parses the Skype App databases for TSK contacts, message and calllog artifacts. About version 8.15.0.428 (9/17/2019) Skype database: - There are 4 tables this parser uses: 1) person - this table appears to hold all contacts known to the user. 2) user - this table holds information pertaining to the user. 3) particiapnt - Yes, that is not a typo. This table maps group chat ids to skype ids (1 to many). 4) chatItem - This table contains all messages. It maps the group id or skype id (for 1 to 1 communication) to the message content and metadata. Either the group id or skype id is stored in a column named 'conversation_link'. More info and implementation details: - The person table does not include groups. To get all 1 to 1 communications, we could simply join the person and chatItem tables. This would mean we'd need to do a second pass to get all the group information as they would be excluded in the join. Since the chatItem table stores both the group id or skype_id in one column, an implementation decision was made to union the person and particiapnt table together so that all rows are matched in one join with chatItem. This result is consistently labeled contact_list_with_groups in the following queries. - In order to keep the formatting of the name consistent throughout each query, a _format_user_name() function was created to encapsulate the CASE statement that was being shared across them. Refer to the method for more details. """ def __init__(self): self._logger = Logger.getLogger(self.__class__.__name__) self._SKYPE_PACKAGE_NAME = "com.skype.raider" self._PARSER_NAME = "Skype Parser" self._VERSION = "8.15.0.428" def get_user_account(self, skype_db): account_query_result = skype_db.runQuery( """ SELECT entry_id, """+_format_user_name()+""" AS name FROM user """ ) if account_query_result is not None and account_query_result.next(): return Account.Address(account_query_result.getString("entry_id"), account_query_result.getString("name")) return None def analyze(self, dataSource, fileManager, context): #Skype databases are of the form: live:XYZ.db, where #XYZ is the skype id of the user. The following search #does a generic substring match for 'live' in the skype #package. skype_dbs = AppSQLiteDB.findAppDatabases(dataSource, "live:", False, self._SKYPE_PACKAGE_NAME) for skype_db in skype_dbs: try: #Attempt to get the user account id from the database user_account_instance = None try: user_account_instance = self.get_user_account(skype_db) except SQLException as ex: self._logger.log(Level.WARNING, "Error query for the user account in the Skype db.", ex) self._logger.log(Level.WARNING, traceback.format_exc()) current_case = Case.getCurrentCaseThrows() if user_account_instance is None: helper = CommunicationArtifactsHelper( current_case.getSleuthkitCase(), self._PARSER_NAME, skype_db.getDBFile(), Account.Type.SKYPE ) else: helper = CommunicationArtifactsHelper( current_case.getSleuthkitCase(), self._PARSER_NAME, skype_db.getDBFile(), Account.Type.SKYPE, Account.Type.SKYPE, user_account_instance ) #Query for contacts and iterate row by row adding #each contact artifact contacts_parser = SkypeContactsParser(skype_db) while contacts_parser.next(): helper.addContact( contacts_parser.get_account_name(), contacts_parser.get_contact_name(), contacts_parser.get_phone(), contacts_parser.get_home_phone(), contacts_parser.get_mobile_phone(), contacts_parser.get_email() ) contacts_parser.close() #Query for call logs and iterate row by row adding #each call log artifact calllog_parser = SkypeCallLogsParser(skype_db) while calllog_parser.next(): helper.addCalllog( calllog_parser.get_call_direction(), calllog_parser.get_phone_number_from(), calllog_parser.get_phone_number_to(), calllog_parser.get_call_start_date_time(), calllog_parser.get_call_end_date_time(), calllog_parser.get_call_type() ) calllog_parser.close() #Query for messages and iterate row by row adding #each message artifact messages_parser = SkypeMessagesParser(skype_db) while messages_parser.next(): helper.addMessage( messages_parser.get_message_type(), messages_parser.get_message_direction(), messages_parser.get_phone_number_from(), messages_parser.get_phone_number_to(), messages_parser.get_message_date_time(), messages_parser.get_message_read_status(), messages_parser.get_message_subject(), messages_parser.get_message_text(), messages_parser.get_thread_id() ) messages_parser.close() except SQLException as ex: #Error parsing Skype db self._logger.log(Level.WARNING, "Error parsing Skype Databases", ex) self._logger.log(Level.WARNING, traceback.format_exc()) except (TskCoreException, BlackboardException) as ex: #Severe error trying to add to case database.. case is not complete. #These exceptions are thrown by the CommunicationArtifactsHelper. self._logger.log(Level.SEVERE, "Failed to add message artifacts to the case database.", ex) self._logger.log(Level.SEVERE, traceback.format_exc()) except NoCurrentCaseException as ex: self._logger.log(Level.WARNING, "No case currently open.", ex) self._logger.log(Level.WARNING, traceback.format_exc()) finally: skype_db.close() class SkypeCallLogsParser(TskCallLogsParser): """ Extracts TSK_CALLLOG information from the Skype database. TSK_CALLLOG fields that are not in the Skype database are given a default value inherited from the super class. """ def __init__(self, calllog_db): """ Big picture: The query below creates a contacts_list_with_groups table, which represents the recipient info. A chatItem record holds ids for both the recipient and sender. The first join onto chatItem fills in the blanks for the recipients. The second join back onto person handles the sender info. The result is a table with all of the communication details. Implementation details: - message_type w/ value 3 appeared to be the call type, regardless of if it was audio or video. """ super(SkypeCallLogsParser, self).__init__(calllog_db.runQuery( """ SELECT contacts_list_with_groups.conversation_id, contacts_list_with_groups.participant_ids, contacts_list_with_groups.participants, time, duration, is_sender_me, person_id as sender_id, sender_name.name as sender_name FROM (SELECT conversation_id, Group_concat(person_id) AS participant_ids, Group_concat("""+_format_user_name()+""") AS participants FROM particiapnt AS PART JOIN person AS P ON PART.person_id = P.entry_id GROUP BY conversation_id UNION SELECT entry_id, NULL, """+_format_user_name()+""" AS participant FROM person) AS contacts_list_with_groups JOIN chatitem AS C ON C.conversation_link = contacts_list_with_groups.conversation_id JOIN (SELECT entry_id as id, """+_format_user_name()+""" AS name FROM person UNION SELECT entry_id as id, """+_format_user_name()+""" AS name FROM user) AS sender_name ON sender_name.id = C.person_id WHERE message_type == 3 """ ) ) self._INCOMING_CALL_TYPE = 0 self._OUTGOING_CALL_TYPE = 1 def get_phone_number_from(self): if self.get_call_direction() == self.INCOMING_CALL: return Account.Address(self.result_set.getString("sender_id"), self.result_set.getString("sender_name")) def get_phone_number_to(self): if self.get_call_direction() == self.OUTGOING_CALL: group_ids = self.result_set.getString("participant_ids") name = self.result_set.getString("participants") if group_ids is not None: group_ids = group_ids.split(",") name = name.split(",") recipients = [] for person_id, person_name in zip(group_ids, name): recipients.append(Account.Address(person_id, person_name)) return recipients return Account.Address(self.result_set.getString("conversation_id"), name) return super(SkypeCallLogsParser, self).get_phone_number_to() def get_call_direction(self): direction = self.result_set.getInt("is_sender_me") if direction == self._INCOMING_CALL_TYPE: return self.INCOMING_CALL if direction == self._OUTGOING_CALL_TYPE: return self.OUTGOING_CALL return super(SkypeCallLogsParser, self).get_call_direction() def get_call_start_date_time(self): return self.result_set.getLong("time") / 1000 def get_call_end_date_time(self): start = self.get_call_start_date_time() duration = self.result_set.getInt("duration") / 1000 return start + duration class SkypeContactsParser(TskContactsParser): """ Extracts TSK_CONTACT information from the Skype database. TSK_CONTACT fields that are not in the Skype database are given a default value inherited from the super class. """ def __init__(self, contact_db): super(SkypeContactsParser, self).__init__(contact_db.runQuery( """ SELECT entry_id, """+_format_user_name()+""" AS name FROM person """ ) ) def get_account_name(self): return self.result_set.getString("entry_id") def get_contact_name(self): return self.result_set.getString("name") class SkypeMessagesParser(TskMessagesParser): """ Extract TSK_MESSAGE information from the Skype database. TSK_CONTACT fields that are not in the Skype database are given a default value inherited from the super class. """ def __init__(self, message_db): """ This query is very similar to the call logs query, the only difference is it grabs more columns in the SELECT and excludes message_types which have the call type value (3). """ super(SkypeMessagesParser, self).__init__(message_db.runQuery( """ SELECT contacts_list_with_groups.conversation_id, contacts_list_with_groups.participant_ids, contacts_list_with_groups.participants, time, content, file_name, device_gallery_path, is_sender_me, person_id as sender_id, sender_name.name AS sender_name FROM (SELECT conversation_id, Group_concat(person_id) AS participant_ids, Group_concat("""+_format_user_name()+""") AS participants FROM particiapnt AS PART JOIN person AS P ON PART.person_id = P.entry_id GROUP BY conversation_id UNION SELECT entry_id as conversation_id, NULL, """+_format_user_name()+""" AS participant FROM person) AS contacts_list_with_groups JOIN chatitem AS C ON C.conversation_link = contacts_list_with_groups.conversation_id JOIN (SELECT entry_id as id, """+_format_user_name()+""" AS name FROM person UNION SELECT entry_id as id, """+_format_user_name()+""" AS name FROM user) AS sender_name ON sender_name.id = C.person_id WHERE message_type != 3 """ ) ) self._SKYPE_MESSAGE_TYPE = "Skype Message" self._OUTGOING_MESSAGE_TYPE = 1 self._INCOMING_MESSAGE_TYPE = 0 def get_message_type(self): return self._SKYPE_MESSAGE_TYPE def get_phone_number_from(self): if self.get_message_direction() == self.INCOMING: return Account.Address(self.result_set.getString("sender_id"), self.result_set.getString("sender_name")) return super(SkypeMessagesParser, self).get_phone_number_from() def get_message_direction(self): direction = self.result_set.getInt("is_sender_me") if direction == self._OUTGOING_MESSAGE_TYPE: return self.OUTGOING if direction == self._INCOMING_MESSAGE_TYPE: return self.INCOMING return super(SkypeMessagesParser, self).get_message_direction() def get_phone_number_to(self): if self.get_message_direction() == self.OUTGOING: group_ids = self.result_set.getString("participant_ids") names = self.result_set.getString("participants") if group_ids is not None: group_ids = group_ids.split(",") names = names.split(",") recipients = [] for participant_id, participant_name in zip(group_ids, names): recipients.append(Account.Address(participant_id, participant_name)) return recipients return Account.Address(self.result_set.getString("conversation_id"), names) return super(SkypeMessagesParser, self).get_phone_number_to() def get_message_date_time(self): date = self.result_set.getLong("time") return date / 1000 def get_message_text(self): content = self.result_set.getString("content") if content is not None: file_name = self.result_set.getString("file_name") file_path = self.result_set.getString("device_gallery_path") #if a file name and file path are associated with a message, append it if file_name is not None and file_path is not None: attachment = "File Name: "+file_name +"\n"+ "File Path: "+file_path return general.appendAttachmentList(content, [attachment]) return content return super(SkypeMessagesParser, self).get_message_text() def get_thread_id(self): group_ids = self.result_set.getString("participant_ids") if group_ids is not None: return self.result_set.getString("conversation_id") return super(SkypeMessagesParser, self).get_thread_id() def _format_user_name(): """ This CASE SQL statement is used in many queries to format the names of users. For a user, there is a first_name column and a last_name column. Some of these columns can be null and our goal is to produce the cleanest data possible. In the event that both the first and last name columns are null, we return the skype_id which is stored in the database as 'entry_id'. Commas are removed from the name so that we can concatenate names into a comma seperate list for group chats. """ return """ CASE WHEN Ifnull(first_name, "") == "" AND Ifnull(last_name, "") == "" THEN entry_id WHEN first_name is NULL THEN replace(last_name, ",", "") WHEN last_name is NULL THEN replace(first_name, ",", "") ELSE replace(first_name, ",", "") || " " || replace(last_name, ",", "") END """