Beispiel #1
0
    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)
Beispiel #2
0
    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)
Beispiel #5
0
    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
        )
Beispiel #6
0
    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
        )
Beispiel #7
0
    def test_looks_like_cert(self, filename, expected):
        result = CertParser.looks_like_cert(filename)

        self.assertEqual(expected, result)
Beispiel #8
0
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)
Beispiel #9
0
 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)
Beispiel #10
0
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)