def test_parse_cert(self, mock_popen): mock_popen.return_value = any_popen(b"any-cert") cert = CertParser.parse_cert("any-file-path") assert_popen_called_once_with(mock_popen, "keytool -printcert -file any-file-path") self.assertEqual("any-cert", cert)
def parse(self, filepath: str, extended_processing: bool = True): """ :param filepath: path of the APK file :param extended_processing: (optional) whether should parse all information or only a summary. True by default. :return: the parsed APK file :raise: ApkParsingError if cannot parse the file as an APK """ self.logger.debug("Parsing APK file: filepath=\"%s\"", filepath) if not self.looks_like_apk(filepath): raise ApkParsingError file = self.file_parser.parse(filepath) cert = None manifest = None dex_files = [] other_files = [] with ZipFile(filepath) as apk: tmpdir = self.__create_temporary_directory(ApkParser.__TEMPORARY_DIR) for filename in apk.namelist(): entry_filepath = apk.extract(filename, tmpdir) self.logger.debug("Extracting APK resource %s to %s", filename, entry_filepath) try: if AndroidManifestParser.looks_like_manifest(filename): self.logger.debug("%s looks like an AndroidManifest.xml file", filename) manifest = self.manifest_parser.parse(entry_filepath, True, filepath, True) elif CertParser.looks_like_cert(filename): self.logger.debug("%s looks like a CERT file", filename) cert = self.__parse_cert(entry_filepath, filename, extended_processing) elif DexParser.looks_like_dex(filename): self.logger.debug("%s looks like a dex file", filename) dex = self.__parse_dex(entry_filepath, filename, extended_processing) dex_files.append(dex) else: self.logger.debug("%s looks like a generic file", filename) entry = self.__parse_file(entry_filepath, filename, extended_processing) if entry is not None: other_files.append(entry) except (AndroidManifestParsingError, CertParsingError, FileParsingError) as error: self.__remove_directory(tmpdir) raise ApkParsingError from error self.__remove_directory(tmpdir) if manifest is None or cert is None or not dex_files: raise ApkParsingError return APK( filename=file.get_file_name(), size=file.get_size(), md5hash=file.get_md5(), sha1hash=file.get_sha1(), sha256hash=file.get_sha256(), sha512hash=file.get_sha512(), app_name=Aapt.get_app_name(filepath), cert=cert, manifest=manifest, dex_files=dex_files, other_files=other_files )
def test_parse_validity_when_localize_fails(self, mock_get_localzone): mock_get_localzone.return_value.localize.side_effect = ValueError() validity = CertParser.parse_validity("Valid from: Sat Jun 27 12:06:13 CEST 2015 until: Tue Feb 26 11:06:13 CET 2515") self.assertEqual( CertValidity(valid_from="Sat Jun 27 12:06:13 CEST 2015", valid_to="Tue Feb 26 11:06:13 CET 2515"), validity )
def test_parse_validity(self, mock_get_localzone, mock_datetime): mock_astimezone = Mock() mock_get_localzone.return_value.localize.return_value.astimezone.return_value = mock_astimezone mock_astimezone.strftime.side_effect = ["2015-06-27 10:06:13Z", "2515-02-26 10:06:13Z"] mock_datetime.strptime.return_value = Mock() validity = CertParser.parse_validity("Valid from: Sat Jun 27 12:06:13 CEST 2015 until: Tue Feb 26 11:06:13 CET 2515") mock_datetime.strptime.assert_has_calls([ call("Sat Jun 27 12:06:13 CEST 2015", "%a %b %d %H:%M:%S %Z %Y"), call("Tue Feb 26 11:06:13 CET 2515", "%a %b %d %H:%M:%S %Z %Y") ]) mock_astimezone.strftime.assert_has_calls([ call("%Y-%m-%d %H:%M:%SZ"), call("%Y-%m-%d %H:%M:%SZ") ]) self.assertEqual(CertValidity(valid_from="2015-06-27 10:06:13Z", valid_to="2515-02-26 10:06:13Z"), validity)
def test_parse_participant(self): owner = CertParser.parse_participant( "ParticipantLabel: CN=any-name, OU=any-unit, O=any-organization, L=any-city, ST=any-state, C=any-country", pattern="^ParticipantLabel: (.*)$" ) self.assertEqual( CertParticipant( name="any-name", email="", unit="any-unit", organization="any-organization", city="any-city", state="any-state", country="any-country", domain="" ), owner )
def test_parse_fingerprint(self): fingerprint = CertParser.parse_fingerprint( "Certificate fingerprints:\n" "\t MD5: any-md5\n" "\t SHA1: any-sha1\n" "\t SHA256: any-sha256\n" "Signature algorithm name: any-signature\n" "Version: any-version" ) self.assertEqual( CertFingerprint( md5="any-md5", sha1="any-sha1", sha256="any-sha256", signature="any-signature", version="any-version" ), fingerprint )
def test_looks_like_cert(self, filename, expected): result = CertParser.looks_like_cert(filename) self.assertEqual(expected, result)
class TestCertParser(unittest.TestCase): """ Test Cert parser. """ sut = CertParser() @patch('ninjadroid.parsers.cert.Popen') @patch('ninjadroid.parsers.cert.get_localzone') @patch('ninjadroid.parsers.cert.FileParser') def test_init(self, mock_file_parser, mock_get_localzone, mock_popen): file = any_file() mock_parser_instance = any_file_parser(file=file) mock_file_parser.return_value = mock_parser_instance mock_popen.return_value = any_popen( response=b"Owner: CN=OwnerName, OU=OwnerUnit, O=OwnerOrganization, L=OwnerCity, ST=OwnerState, C=OwnerCountry\n" \ b"Issuer: CN=IssuerName, OU=IssuerUnit, O=IssuerOrganization, L=IssuerCity, ST=IssuerState, C=IssuerCountry\n" \ b"Serial number: 558e7595\n" \ b"Valid from: Sat Jun 27 12:06:13 CEST 2015 until: Tue Feb 26 11:06:13 CET 2515\n" \ b"Certificate fingerprints:\n" \ b"\t MD5: 90:22:EF:0C:DB:C3:78:87:7B:C3:A3:6C:5A:68:E6:45\n" \ b"\t SHA1: 5A:C0:6C:32:63:7F:5D:BE:CA:F9:38:38:4C:FA:FF:ED:20:52:43:B6\n" \ b"\t SHA256: E5:15:CC:BC:5E:BF:B2:9D:A6:13:03:63:CF:19:33:FA:CE:AF:DC:ED:5D:2F:F5:98:7C:CE:37:13:64:4A:CF:77\n" \ b"Signature algorithm name: SHA1withRSA\n" \ b"Subject Public Key Algorithm: 1024-bit RSA key\n" \ b"Version: 3" ) mock_get_localzone.return_value.localize.side_effect = ValueError() cert = self.sut.parse("any-file-path", "any-file-name") assert_file_parser_called_once_with(mock_parser_instance, filepath="any-file-path", filename="any-file-name") assert_popen_called_once_with(mock_popen, "keytool -printcert -file any-file-path") assert_file_equal(self, expected=file, actual=cert) self.assertEqual("558e7595", cert.get_serial_number()) self.assertEqual( CertValidity(valid_from="Sat Jun 27 12:06:13 CEST 2015", valid_to="Tue Feb 26 11:06:13 CET 2515"), cert.get_validity() ) self.assertEqual( CertFingerprint( md5="90:22:EF:0C:DB:C3:78:87:7B:C3:A3:6C:5A:68:E6:45", sha1="5A:C0:6C:32:63:7F:5D:BE:CA:F9:38:38:4C:FA:FF:ED:20:52:43:B6", sha256="E5:15:CC:BC:5E:BF:B2:9D:A6:13:03:63:CF:19:33:FA:CE:AF:DC:ED:5D:2F:F5:98:7C:CE:37:13:64:4A:CF:77", signature="SHA1withRSA", version="3" ), cert.get_fingerprint() ) self.assertEqual( CertParticipant( name="OwnerName", email="", unit="OwnerUnit", organization="OwnerOrganization", city="OwnerCity", state="OwnerState", country="OwnerCountry", domain="" ), cert.get_owner() ) self.assertEqual( CertParticipant( name="IssuerName", email="", unit="IssuerUnit", organization="IssuerOrganization", city="IssuerCity", state="IssuerState", country="IssuerCountry", domain="" ), cert.get_issuer() ) @patch('ninjadroid.parsers.cert.Popen') @patch('ninjadroid.parsers.cert.FileParser') def test_parse_fails_when_file_parser_fails(self, mock_file_parser, mock_popen): mock_parser_instance = any_file_parser_failure() mock_file_parser.return_value = mock_parser_instance with self.assertRaises(FileParsingError): self.sut.parse("any-file-path", "any-file-name") assert_file_parser_called_once_with(mock_parser_instance, filepath="any-file-path", filename="any-file-name") mock_popen.assert_not_called() @patch('ninjadroid.parsers.cert.Popen') @patch('ninjadroid.parsers.cert.FileParser') def test_parse_fails_when_keytool_fails(self, mock_file_parser, mock_popen): mock_file_parser.return_value = any_file_parser(file=any_file()) mock_popen.return_value = any_popen(b"keytool error") with self.assertRaises(CertParsingError): self.sut.parse("any-file-path", "any-file-name") assert_popen_called_once_with(mock_popen, "keytool -printcert -file any-file-path") @patch('ninjadroid.parsers.cert.Popen') def test_parse_cert(self, mock_popen): mock_popen.return_value = any_popen(b"any-cert") cert = CertParser.parse_cert("any-file-path") assert_popen_called_once_with(mock_popen, "keytool -printcert -file any-file-path") self.assertEqual("any-cert", cert) @patch('ninjadroid.parsers.cert.datetime') @patch('ninjadroid.parsers.cert.get_localzone') def test_parse_validity(self, mock_get_localzone, mock_datetime): mock_astimezone = Mock() mock_get_localzone.return_value.localize.return_value.astimezone.return_value = mock_astimezone mock_astimezone.strftime.side_effect = ["2015-06-27 10:06:13Z", "2515-02-26 10:06:13Z"] mock_datetime.strptime.return_value = Mock() validity = CertParser.parse_validity( "Valid from: Sat Jun 27 12:06:13 CEST 2015 until: Tue Feb 26 11:06:13 CET 2515" ) mock_datetime.strptime.assert_has_calls([ call("Sat Jun 27 12:06:13 CEST 2015", "%a %b %d %H:%M:%S %Z %Y"), call("Tue Feb 26 11:06:13 CET 2515", "%a %b %d %H:%M:%S %Z %Y") ]) mock_astimezone.strftime.assert_has_calls([ call("%Y-%m-%d %H:%M:%SZ"), call("%Y-%m-%d %H:%M:%SZ") ]) self.assertEqual(CertValidity(valid_from="2015-06-27 10:06:13Z", valid_to="2515-02-26 10:06:13Z"), validity) @patch('ninjadroid.parsers.cert.get_localzone') def test_parse_validity_when_localize_fails(self, mock_get_localzone): mock_get_localzone.return_value.localize.side_effect = ValueError() validity = CertParser.parse_validity( "Valid from: Sat Jun 27 12:06:13 CEST 2015 until: Tue Feb 26 11:06:13 CET 2515" ) self.assertEqual( CertValidity(valid_from="Sat Jun 27 12:06:13 CEST 2015", valid_to="Tue Feb 26 11:06:13 CET 2515"), validity ) def test_parse_fingerprint(self): fingerprint = CertParser.parse_fingerprint( "Certificate fingerprints:\n" "\t MD5: any-md5\n" "\t SHA1: any-sha1\n" "\t SHA256: any-sha256\n" "Signature algorithm name: any-signature\n" "Version: any-version" ) self.assertEqual( CertFingerprint( md5="any-md5", sha1="any-sha1", sha256="any-sha256", signature="any-signature", version="any-version" ), fingerprint ) def test_parse_participant(self): owner = CertParser.parse_participant( "ParticipantLabel: CN=any-name, OU=any-unit, O=any-organization, L=any-city, ST=any-state, C=any-country", pattern="^ParticipantLabel: (.*)$" ) self.assertEqual( CertParticipant( name="any-name", email="", unit="any-unit", organization="any-organization", city="any-city", state="any-state", country="any-country", domain="" ), owner ) @parameterized.expand([ ["META-INF/CERT.RSA", True], ["META-INF/CERT.DSA", True], ["META-INF/WHATEVER.RSA", True], ["META-INF/WHATEVER.DSA", False], ["META-INF/NON_CERT.apk", False], ["META-INF/NON_CERT.dex", False], ["META-INF/NON_CERT.so", False], ["AndroidManifest.xml", False], ["classes.dex", False], ["Example.apk", False] ]) def test_looks_like_cert(self, filename, expected): result = CertParser.looks_like_cert(filename) self.assertEqual(expected, result)
def __init__(self, logger: Logger = default_logger): self.logger = logger self.file_parser = FileParser(logger) self.manifest_parser = AndroidManifestParser(logger) self.cert_parser = CertParser(logger) self.dex_parser = DexParser(logger)
class ApkParser: """ Parser implementation for Android APK packages. """ __TEMPORARY_DIR = ".ninjadroid" def __init__(self, logger: Logger = default_logger): self.logger = logger self.file_parser = FileParser(logger) self.manifest_parser = AndroidManifestParser(logger) self.cert_parser = CertParser(logger) self.dex_parser = DexParser(logger) def parse(self, filepath: str, extended_processing: bool = True): """ :param filepath: path of the APK file :param extended_processing: (optional) whether should parse all information or only a summary. True by default. :return: the parsed APK file :raise: ApkParsingError if cannot parse the file as an APK """ self.logger.debug("Parsing APK file: filepath=\"%s\"", filepath) if not self.looks_like_apk(filepath): raise ApkParsingError file = self.file_parser.parse(filepath) cert = None manifest = None dex_files = [] other_files = [] with ZipFile(filepath) as apk: tmpdir = self.__create_temporary_directory(ApkParser.__TEMPORARY_DIR) for filename in apk.namelist(): entry_filepath = apk.extract(filename, tmpdir) self.logger.debug("Extracting APK resource %s to %s", filename, entry_filepath) try: if AndroidManifestParser.looks_like_manifest(filename): self.logger.debug("%s looks like an AndroidManifest.xml file", filename) manifest = self.manifest_parser.parse(entry_filepath, True, filepath, True) elif CertParser.looks_like_cert(filename): self.logger.debug("%s looks like a CERT file", filename) cert = self.__parse_cert(entry_filepath, filename, extended_processing) elif DexParser.looks_like_dex(filename): self.logger.debug("%s looks like a dex file", filename) dex = self.__parse_dex(entry_filepath, filename, extended_processing) dex_files.append(dex) else: self.logger.debug("%s looks like a generic file", filename) entry = self.__parse_file(entry_filepath, filename, extended_processing) if entry is not None: other_files.append(entry) except (AndroidManifestParsingError, CertParsingError, FileParsingError) as error: self.__remove_directory(tmpdir) raise ApkParsingError from error self.__remove_directory(tmpdir) if manifest is None or cert is None or not dex_files: raise ApkParsingError return APK( filename=file.get_file_name(), size=file.get_size(), md5hash=file.get_md5(), sha1hash=file.get_sha1(), sha256hash=file.get_sha256(), sha512hash=file.get_sha512(), app_name=Aapt.get_app_name(filepath), cert=cert, manifest=manifest, dex_files=dex_files, other_files=other_files ) def __parse_cert(self, filepath: str, filename: str, extended_processing: bool) -> Union[Cert,File]: if extended_processing: return self.cert_parser.parse(filepath, filename) return self.file_parser.parse(filepath, filename) def __parse_dex(self, filepath: str, filename: str, extended_processing: bool) -> Union[Dex,File]: if extended_processing: return self.dex_parser.parse(filepath, filename) return self.file_parser.parse(filepath, filename) def __parse_file(self, filepath: str, filename: str, extended_processing: bool) -> Optional[File]: if extended_processing and not FileParser.is_directory(filepath): try: return self.file_parser.parse(filepath, filename) except FileParsingError: self.logger.error("Could not parse file '%s'!", filename) return None @staticmethod def __create_temporary_directory(path: str) -> str: return mkdtemp(path) @staticmethod def __remove_directory(path: str): try: rmtree(path) except OSError: pass @staticmethod def looks_like_apk(filename: str) -> bool: return FileParser.is_zip_file(filename)