apklisparser 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
apklisparser/apk.py ADDED
@@ -0,0 +1,1228 @@
1
+ import io
2
+ import logging
3
+ import re
4
+ import zipfile
5
+ from io import BytesIO
6
+ from struct import unpack
7
+ from zlib import crc32
8
+
9
+ from cryptography import x509
10
+ from cryptography.hazmat.backends import default_backend
11
+
12
+ # Used for reading Certificates
13
+ from pyasn1.codec.der.decoder import decode
14
+ from pyasn1.codec.der.encoder import encode
15
+
16
+ from . import util
17
+ from .axml import ARSCParser, ARSCResTableConfig, AXMLPrinter
18
+ from lxml import objectify, etree
19
+ from .build_icon import build_icon
20
+
21
+ NS_ANDROID_URI = "http://schemas.android.com/apk/res/android"
22
+ NS_ANDROID = "{http://schemas.android.com/apk/res/android}"
23
+
24
+ log = logging.getLogger("ApklisParser")
25
+
26
+
27
+ class Error(Exception):
28
+ """Base class for exceptions in this module."""
29
+
30
+ pass
31
+
32
+
33
+ class FileNotPresent(Error):
34
+ pass
35
+
36
+
37
+ class BrokenAPKError(Error):
38
+ pass
39
+
40
+
41
+ _xml_clean = etree.XSLT(
42
+ etree.fromstring(
43
+ """
44
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
45
+ <xsl:output indent="yes"/>
46
+ <xsl:strip-space elements="*"/>
47
+
48
+ <xsl:template match="node()">
49
+ <xsl:copy>
50
+ <xsl:apply-templates select="@*|node()"/>
51
+ </xsl:copy>
52
+ </xsl:template>
53
+
54
+ <xsl:template match="*" priority="1">
55
+ <xsl:element name="{local-name()}" namespace="">
56
+ <xsl:apply-templates select="@*|node()"/>
57
+ </xsl:element>
58
+ </xsl:template>
59
+
60
+ <xsl:template match="@*">
61
+ <xsl:attribute name="{local-name()}" namespace="">
62
+ <xsl:value-of select="."/>
63
+ </xsl:attribute>
64
+ </xsl:template>
65
+ </xsl:stylesheet>
66
+ """
67
+ )
68
+ )
69
+
70
+
71
+ class APK(object):
72
+ # Constants in ZipFile
73
+ PK_END_OF_CENTRAL_DIR = b"\x50\x4b\x05\x06"
74
+ PK_CENTRAL_DIR = b"\x50\x4b\x01\x02"
75
+
76
+ # Constants in the APK Signature Block
77
+ APK_SIG_MAGIC = b"APK Sig Block 42"
78
+ APK_SIG_KEY_SIGNATURE = 0x7109871a
79
+
80
+ def __init__(
81
+ self, filename, raw=False, magic_file=None, skip_analysis=False, testzip=False
82
+ ):
83
+ """
84
+ This class can access to all elements in an APK file
85
+
86
+ :param filename: specify the path of the file, or raw data
87
+ :param raw: specify if the filename is a path or raw data (optional)
88
+ :param magic_file: specify the magic file (optional)
89
+ :param skip_analysis: Skip the analysis, e.g. no manifest files are read. (default: False)
90
+ :param testzip: Test the APK for integrity, e.g. if the ZIP file is broken. Throw an exception on failure (default False)
91
+
92
+ :type filename: string
93
+ :type raw: boolean
94
+ :type magic_file: string
95
+ :type skip_analysis: boolean
96
+ :type testzip: boolean
97
+
98
+ :Example:
99
+ APK("myfile.apk")
100
+ APK(read("myfile.apk"), raw=True)
101
+ """
102
+ self.filename = filename
103
+
104
+ self.xml = {}
105
+ self.axml = {}
106
+ self.arsc = {}
107
+
108
+ self.package = ""
109
+ self.androidversion = {}
110
+ self.permissions = []
111
+ self.uses_permissions = []
112
+ self.declared_permissions = {}
113
+ self.app_icon = ""
114
+ self.app_name = ""
115
+ self.valid_apk = False
116
+ self._is_signed_v2 = None
117
+ self._v2_blocks = {}
118
+
119
+ self._files = {}
120
+ self.files_crc32 = {}
121
+
122
+ self.magic_file = magic_file
123
+
124
+ if raw is True:
125
+ self.__raw = bytearray(filename)
126
+ else:
127
+ self.__raw = bytearray(util.read(filename))
128
+
129
+ self.size = len(self.__raw)
130
+
131
+ self.zip = zipfile.ZipFile(io.BytesIO(self.__raw), mode="r")
132
+
133
+ if testzip:
134
+ # Test the zipfile for integrity before continuing.
135
+ # This process might be slow, as the whole file is read.
136
+ # Therefore it is possible to enable it as a separate feature.
137
+ #
138
+ # A short benchmark showed, that testing the zip takes about 10 times longer!
139
+ # e.g. normal zip loading (skip_analysis=True) takes about 0.01s, where
140
+ # testzip takes 0.1s!
141
+ ret = self.zip.testzip()
142
+ if ret is not None:
143
+ # we could print the filename here, but there are zip which are so broken
144
+ # That the filename is either very very long or does not make any sense.
145
+ # Thus we do not do it, the user might find out by using other tools.
146
+ raise BrokenAPKError(
147
+ "The APK is probably broken: testzip returned an error."
148
+ )
149
+
150
+ if not skip_analysis:
151
+ self._apk_analysis()
152
+
153
+ def _apk_analysis(self):
154
+ """
155
+ Run analysis on the APK file.
156
+
157
+ This method is usually called by __init__ except if skip_analysis is False.
158
+ It will then parse the AndroidManifest.xml and set all fields in the APK class which can be
159
+ extracted from the Manifest.
160
+ """
161
+ for i in self.zip.namelist():
162
+ if i == "AndroidManifest.xml":
163
+ self.axml[i] = AXMLPrinter(self.zip.read(i))
164
+ self.xml[i] = None
165
+ raw_xml = self.axml[i].get_buff()
166
+ if len(raw_xml) == 0:
167
+ log.warning("AXML parsing failed, file is empty")
168
+ else:
169
+ try:
170
+ if self.axml[i].is_packed():
171
+ log.warning(
172
+ "XML Seems to be packed, parsing is very likely to fail."
173
+ )
174
+
175
+ self.xml[i] = _xml_clean(self.axml[i].get_xml_obj()).getroot()
176
+
177
+ except Exception as e:
178
+ log.warning("reading AXML as XML failed: " + str(e))
179
+
180
+ if self.xml[i] is not None:
181
+ self.package = self.xml[i].get("package")
182
+ self.androidversion["Code"] = self.xml[i].get("versionCode")
183
+ self.androidversion["Name"] = self.xml[i].get("versionName")
184
+
185
+ for item in self.xml[i].findall("uses-permission"):
186
+ name = item.get("name")
187
+ self.permissions.append(name)
188
+ maxSdkVersion = None
189
+ try:
190
+ maxSdkVersion = int(item.get("maxSdkVersion"))
191
+ except ValueError:
192
+ log.warning(
193
+ item.get("maxSdkVersion")
194
+ + "is not a valid value for <uses-permission> maxSdkVersion"
195
+ )
196
+ except TypeError:
197
+ pass
198
+ self.uses_permissions.append([name, maxSdkVersion])
199
+
200
+ # getting details of the declared permissions
201
+ for d_perm_item in self.xml[i].findall("permission"):
202
+ d_perm_name = self._get_res_string_value(
203
+ str(d_perm_item.get("name"))
204
+ )
205
+ d_perm_label = self._get_res_string_value(
206
+ str(d_perm_item.get("label"))
207
+ )
208
+ d_perm_description = self._get_res_string_value(
209
+ str(d_perm_item.get("description"))
210
+ )
211
+ d_perm_permissionGroup = self._get_res_string_value(
212
+ str(d_perm_item.get("permissionGroup"))
213
+ )
214
+ d_perm_protectionLevel = self._get_res_string_value(
215
+ str(d_perm_item.get("protectionLevel"))
216
+ )
217
+
218
+ d_perm_details = {
219
+ "label": d_perm_label,
220
+ "description": d_perm_description,
221
+ "permissionGroup": d_perm_permissionGroup,
222
+ "protectionLevel": d_perm_protectionLevel,
223
+ }
224
+ self.declared_permissions[d_perm_name] = d_perm_details
225
+
226
+ self.valid_apk = True
227
+
228
+ def __getstate__(self):
229
+ """
230
+ Function for pickling APK Objects.
231
+
232
+ We remove the zip from the Object, as it is not pickable
233
+ And it does not make any sense to pickle it anyways.
234
+
235
+ :return: the picklable APK Object without zip.
236
+ """
237
+ # Upon pickling, we need to remove the ZipFile
238
+ x = self.__dict__
239
+ x["axml"] = str(x["axml"])
240
+ x["xml"] = str(x["xml"])
241
+ del x["zip"]
242
+
243
+ return x
244
+
245
+ def get_resource_as_xml(self, id_, which=lambda x: x[0]):
246
+ res_parser = self.get_android_resources()
247
+ if isinstance(id_, str):
248
+ id_ = int(id_.replace('@', ''), 16)
249
+ candidates = res_parser.get_resolved_res_configs(id_)
250
+ conf, name = which(candidates)
251
+ if name not in self.xml:
252
+ self.xml[name] = AXMLPrinter(self.get_file(name)).get_xml_obj()
253
+ return self.xml[name]
254
+
255
+ def __setstate__(self, state):
256
+ """
257
+ Load a pickled APK Object and restore the state
258
+
259
+ We load the zip file back by reading __raw from the Object.
260
+
261
+ :param state: pickled state
262
+ """
263
+ self.__dict__ = state
264
+
265
+ self.zip = zipfile.ZipFile(io.BytesIO(self.__raw), mode="r")
266
+
267
+ def _get_res_string_value(self, string):
268
+ if not string.startswith("@string/"):
269
+ return string
270
+ string_key = string[9:]
271
+
272
+ res_parser = self.get_android_resources()
273
+ string_value = ""
274
+ for package_name in res_parser.get_packages_names():
275
+ extracted_values = res_parser.get_string(package_name, string_key)
276
+ if extracted_values:
277
+ string_value = extracted_values[1]
278
+ break
279
+ return string_value
280
+
281
+ def is_valid_APK(self):
282
+ """
283
+ Return true if the APK is valid, false otherwise
284
+
285
+ :rtype: boolean
286
+ """
287
+ return self.valid_apk
288
+
289
+ def get_size(self):
290
+ return self.size
291
+
292
+ def get_filename(self):
293
+ """
294
+ Return the filename of the APK
295
+
296
+ :rtype: string
297
+ """
298
+ return self.filename
299
+
300
+ def get_name(self):
301
+ """
302
+ Return the appname of the APK
303
+
304
+ :rtype: string
305
+ """
306
+ if not self.app_name:
307
+ self.app_name = self.get_element("application", "label")
308
+
309
+ if not self.app_name:
310
+ self.app_name = self.get_element(
311
+ "activity", "label", name=self.get_main_activity()
312
+ )
313
+
314
+ if not self.app_name:
315
+ raise Exception("Error extracting application name.")
316
+
317
+ if self.app_name.startswith("@"):
318
+ res_id = int(self.app_name[1:], 16)
319
+ res_parser = self.get_android_resources()
320
+
321
+ try:
322
+ self.app_name = res_parser.get_resolved_res_configs(
323
+ res_id, ARSCResTableConfig.default_config()
324
+ )[0][1]
325
+ except Exception as e:
326
+ raise Exception('Error extracting application name "%s".' % e)
327
+
328
+ return self.app_name
329
+
330
+ def get_icon(self, max_dpi=65536):
331
+ """
332
+ Return the first non-greater density than max_dpi icon file name background and foreground,
333
+ unless exact icon resolution is set in the manifest, in which case
334
+ return the exact file
335
+
336
+ From https://developer.android.com/guide/practices/screens_support.html
337
+ ldpi (low) ~120dpi
338
+ mdpi (medium) ~160dpi
339
+ hdpi (high) ~240dpi
340
+ xhdpi (extra-high) ~320dpi
341
+ xxhdpi (extra-extra-high) ~480dpi
342
+ xxxhdpi (extra-extra-extra-high) ~640dpi
343
+
344
+ :rtype: string
345
+ """
346
+
347
+ if not self.app_icon:
348
+ self.app_icon = self.get_element("application", "icon") or self.get_element(
349
+ "activity", "icon", name=self.get_main_activity()
350
+ )
351
+
352
+ if self.app_icon.startswith("@"):
353
+ self.app_icon = self._resolve_icon_resource(self.app_icon[1:], max_dpi)
354
+
355
+ if not self.app_icon:
356
+ raise Exception("Impossible to extract application icon.")
357
+
358
+ return self.app_icon
359
+
360
+ def _resolve_icon_resource(self, res, max_dpi):
361
+ res_id = int(res, 16)
362
+ res_parser = self.get_android_resources()
363
+ candidates = res_parser.get_resolved_res_configs(res_id)
364
+
365
+ res = None
366
+ current_dpi = -1
367
+
368
+ try:
369
+ for config, file_name in candidates:
370
+ dpi = config.get_density()
371
+ if current_dpi < dpi <= max_dpi:
372
+ res = file_name
373
+ current_dpi = dpi
374
+ except Exception as e:
375
+ log.warning("Exception selecting application res: %s" % e)
376
+ return res
377
+
378
+ def get_part(self, tree, key):
379
+ element = tree.find(key)
380
+ if len(element.values()) == 0:
381
+ element = element.find('inset')
382
+
383
+ return element
384
+
385
+ def extract_icon(self, filename, max_dpi=65536):
386
+ """
387
+ Extract application icon in `filename` location
388
+ :param filename:
389
+ :return:
390
+ """
391
+ icon = self.get_icon()
392
+ if icon.endswith(".xml"):
393
+ icon_element = AXMLPrinter(self.get_file(icon)).get_xml_obj()
394
+ if icon_element.tag in ('adaptative-icon', 'adaptive-icon'):
395
+ parts = [
396
+ (self.get_part(icon_element, 'background').values())[0].replace(
397
+ "android:", ""
398
+ ),
399
+ (self.get_part(icon_element, 'foreground').values())[0].replace(
400
+ "android:", ""
401
+ ),
402
+ ]
403
+ else:
404
+ # should be a bitmap
405
+ parts = [icon_element.attrib.values()[0]]
406
+ parts = [
407
+ self._resolve_icon_resource(p[1:], max_dpi) if p.startswith("@") else p
408
+ for p in parts
409
+ ]
410
+ else:
411
+ parts = [icon]
412
+ parts = filter(None, parts) # some app have invalide parts :'(
413
+ parts = [(p, None if p.startswith("#") else self.get_file(p)) for p in parts]
414
+ build_icon(self, parts, filename)
415
+
416
+ def get_package_name(self):
417
+ """
418
+ Return the name of the package
419
+ :rtype: string
420
+ """
421
+ return self.package
422
+
423
+ def get_version_code(self):
424
+ """
425
+ Return the application version code
426
+
427
+ :rtype: string
428
+ """
429
+ return self.androidversion["Code"]
430
+
431
+ def get_version_name(self):
432
+ """
433
+ Return the application version name
434
+
435
+ :rtype: string
436
+ """
437
+ return self.androidversion["Name"]
438
+
439
+ def get_files(self):
440
+ """
441
+ Return the files inside the APK
442
+
443
+ :rtype: a list of strings
444
+ """
445
+ return self.zip.namelist()
446
+
447
+ def _get_file_magic_name(self, buffer):
448
+ """
449
+ Return the filetype guessed for a buffer
450
+ :param buffer: bytes
451
+ :return: str of filetype
452
+ """
453
+ # TODO this functions should be better in another package
454
+ default = "Unknown"
455
+ ftype = None
456
+
457
+ # There are several implementations of magic,
458
+ # unfortunately all called magic
459
+ try:
460
+ import magic
461
+ except ImportError:
462
+ # no lib magic at all, return unknown
463
+ return default
464
+
465
+ try:
466
+ # We test for the python-magic package here
467
+ getattr(magic, "MagicException")
468
+ except AttributeError:
469
+ try:
470
+ # Check for filemagic package
471
+ getattr(magic.Magic, "id_buffer")
472
+ except AttributeError:
473
+ # Here, we load the file-magic package
474
+ ms = magic.open(magic.MAGIC_NONE)
475
+ ms.load()
476
+ ftype = ms.buffer(buffer)
477
+ else:
478
+ # This is now the filemagic package
479
+ if self.magic_file is not None:
480
+ m = magic.Magic(paths=[self.magic_file])
481
+ else:
482
+ m = magic.Magic()
483
+ ftype = m.id_buffer(buffer)
484
+ else:
485
+ # This is the code for python-magic
486
+ if self.magic_file is not None:
487
+ m = magic.Magic(magic_file=self.magic_file)
488
+ else:
489
+ m = magic.Magic()
490
+ ftype = m.from_buffer(buffer)
491
+
492
+ if ftype is None:
493
+ return default
494
+ else:
495
+ return self._patch_magic(buffer, ftype)
496
+
497
+ @property
498
+ def files(self):
499
+ """
500
+ Wrapper for the files object
501
+
502
+ :return: dictionary of files and their mime type
503
+ """
504
+ return self.get_files_types()
505
+
506
+ def get_files_types(self):
507
+ """
508
+ Return the files inside the APK with their associated types (by using python-magic)
509
+
510
+ :rtype: a dictionnary
511
+ """
512
+ if self._files == {}:
513
+ # Generate File Types / CRC List
514
+ for i in self.get_files():
515
+ buffer = self.zip.read(i)
516
+ self.files_crc32[i] = crc32(buffer)
517
+ # FIXME why not use the crc from the zipfile?
518
+ # crc = self.zip.getinfo(i).CRC
519
+ self._files[i] = self._get_file_magic_name(buffer)
520
+
521
+ return self._files
522
+
523
+ def _patch_magic(self, buffer, orig):
524
+ if ("Zip" in orig) or ("DBase" in orig):
525
+ val = util.is_android_raw(buffer)
526
+ if val == "APK":
527
+ return "Android application package file"
528
+ elif val == "AXML":
529
+ return "Android's binary XML"
530
+
531
+ return orig
532
+
533
+ def get_files_crc32(self):
534
+ """
535
+ Calculates and returns a dictionary of filenames and CRC32
536
+
537
+ :return: dict of filename: CRC32
538
+ """
539
+ if self.files_crc32 == {}:
540
+ for i in self.get_files():
541
+ buffer = self.zip.read(i)
542
+ self.files_crc32[i] = crc32(buffer)
543
+
544
+ return self.files_crc32
545
+
546
+ def get_files_information(self):
547
+ """
548
+ Return the files inside the APK with their associated types and crc32
549
+
550
+ :rtype: string, string, int
551
+ """
552
+ for k in self.get_files():
553
+ yield k, self.get_files_types()[k], self.get_files_crc32()[k]
554
+
555
+ def get_raw(self):
556
+ """
557
+ Return raw bytes of the APK
558
+
559
+ :rtype: string
560
+ """
561
+ return self.__raw
562
+
563
+ def get_file(self, filename):
564
+ """
565
+ Return the raw data of the specified filename
566
+ inside the APK
567
+
568
+ :rtype: string
569
+ """
570
+ try:
571
+ return self.zip.read(filename)
572
+ except KeyError:
573
+ raise FileNotPresent(filename)
574
+
575
+ def get_dex(self):
576
+ """
577
+ Return the raw data of the classes dex file
578
+
579
+ :rtype: a string
580
+ """
581
+ try:
582
+ return self.get_file("classes.dex")
583
+ except FileNotPresent:
584
+ return ""
585
+
586
+ def get_dex_names(self):
587
+ """
588
+ Return the name of all classes dex files
589
+
590
+ :rtype: a list of string
591
+ """
592
+ dexre = re.compile(r"classes(\d*).dex")
593
+ return filter(lambda x: dexre.match(x), self.get_files())
594
+
595
+ def get_all_dex(self):
596
+ """
597
+ Return the raw data of all classes dex files
598
+
599
+ :rtype: a generator
600
+ """
601
+ for dex_name in self.get_dex_names():
602
+ yield self.get_file(dex_name)
603
+
604
+ def is_multidex(self):
605
+ """
606
+ Test if the APK has multiple DEX files
607
+
608
+ :return: True if multiple dex found, otherwise False
609
+ """
610
+ dexre = re.compile(r"^classes(\d+)?.dex$")
611
+ return (
612
+ len([instance for instance in self.get_files() if dexre.search(instance)])
613
+ > 1
614
+ )
615
+
616
+ def get_elements(self, tag_name, attribute, with_namespace=True):
617
+ """
618
+ Return elements in xml files which match with the tag name and the specific attribute
619
+
620
+ :param tag_name: a string which specify the tag name
621
+ :param attribute: a string which specify the attribute
622
+ """
623
+ for i in self.xml:
624
+ for item in self.xml[i].findall(".//" + tag_name):
625
+ if with_namespace:
626
+ value = item.get(attribute)
627
+ else:
628
+ value = item.get(attribute)
629
+ # There might be an attribute without the namespace
630
+ if value:
631
+ yield self.format_value(value)
632
+
633
+ def format_value(self, value):
634
+ """
635
+ Format a value with packagename, if not already set
636
+
637
+ :param value:
638
+ :return:
639
+ """
640
+ if len(value) > 0:
641
+ if value[0] == ".":
642
+ value = self.package + value
643
+ else:
644
+ v_dot = value.find(".")
645
+ if v_dot == 0:
646
+ value = self.package + "." + value
647
+ elif v_dot == -1:
648
+ value = self.package + "." + value
649
+ return value
650
+
651
+ def get_element(self, tag_name, attribute, **attribute_filter):
652
+ """
653
+ Return element in xml files which match with the tag name and the specific attribute
654
+
655
+ :param tag_name: specify the tag name
656
+ :type tag_name: string
657
+ :param attribute: specify the attribute
658
+ :type attribute: string
659
+
660
+ :rtype: string
661
+ """
662
+ for i in self.xml:
663
+ if self.xml[i] is None:
664
+ continue
665
+ tag = self.xml[i].findall(".//" + tag_name)
666
+ if len(tag) == 0:
667
+ return None
668
+ for item in tag:
669
+ skip_this_item = False
670
+ for attr, val in list(attribute_filter.items()):
671
+ attr_val = item.get(attr)
672
+ if attr_val != val:
673
+ skip_this_item = True
674
+ break
675
+
676
+ if skip_this_item:
677
+ continue
678
+
679
+ value = item.get(attribute)
680
+
681
+ if value is not None:
682
+ return value
683
+ return None
684
+
685
+ def get_main_activity(self):
686
+ """
687
+ Return the name of the main activity
688
+
689
+ :rtype: string
690
+ """
691
+ x = set()
692
+ y = set()
693
+
694
+ for i in self.xml:
695
+ activities_and_aliases = self.xml[i].findall(".//activity") + self.xml[
696
+ i
697
+ ].findall(".//activity-alias")
698
+
699
+ for item in activities_and_aliases:
700
+ # Some applications have more than one MAIN activity.
701
+ # For example: paid and free content
702
+ activityEnabled = item.get("enabled")
703
+ if (
704
+ activityEnabled is not None
705
+ and activityEnabled != ""
706
+ and activityEnabled == "false"
707
+ ):
708
+ continue
709
+
710
+ for sitem in item.findall(".//action"):
711
+ val = sitem.get("name")
712
+ if val == "android.intent.action.MAIN":
713
+ x.add(item.get("name"))
714
+
715
+ for sitem in item.findall(".//category"):
716
+ val = sitem.get("name")
717
+ if val == "android.intent.category.LAUNCHER":
718
+ y.add(item.get("name"))
719
+
720
+ z = x.intersection(y)
721
+ if len(z) > 0:
722
+ return self.format_value(z.pop())
723
+ return None
724
+
725
+ def get_activities(self):
726
+ """
727
+ Return the android:name attribute of all activities
728
+
729
+ :rtype: a list of string
730
+ """
731
+ return list(self.get_elements("activity", "name"))
732
+
733
+ def get_services(self):
734
+ """
735
+ Return the android:name attribute of all services
736
+
737
+ :rtype: a list of string
738
+ """
739
+ return list(self.get_elements("service", "name"))
740
+
741
+ def get_receivers(self):
742
+ """
743
+ Return the android:name attribute of all receivers
744
+
745
+ :rtype: a list of string
746
+ """
747
+ return list(self.get_elements("receiver", "name"))
748
+
749
+ def get_providers(self):
750
+ """
751
+ Return the android:name attribute of all providers
752
+
753
+ :rtype: a list of string
754
+ """
755
+ return list(self.get_elements("provider", "name"))
756
+
757
+ def get_intent_filters(self, category, name):
758
+ d = {"action": [], "category": []}
759
+
760
+ for i in self.xml:
761
+ for item in self.xml[i].findall(".//" + category):
762
+ if self.format_value(item.get("name")) == name:
763
+ for sitem in item.findall(".//intent-filter"):
764
+ for ssitem in sitem.findall("action"):
765
+ if ssitem.get("name") not in d["action"]:
766
+ d["action"].append(ssitem.get("name"))
767
+ for ssitem in sitem.findall("category"):
768
+ if ssitem.get("name") not in d["category"]:
769
+ d["category"].append(ssitem.get("name"))
770
+
771
+ if not d["action"]:
772
+ del d["action"]
773
+
774
+ if not d["category"]:
775
+ del d["category"]
776
+
777
+ return d
778
+
779
+ def get_permissions(self):
780
+ """
781
+ Return permissions
782
+
783
+ :rtype: list of string
784
+ """
785
+ return self.permissions
786
+
787
+ def get_uses_implied_permission_list(self):
788
+ """
789
+ Return all permissions implied by the target SDK or other permissions.
790
+
791
+ :rtype: list of string
792
+ """
793
+ target_sdk_version = self.get_effective_target_sdk_version()
794
+
795
+ READ_CALL_LOG = "android.permission.READ_CALL_LOG"
796
+ READ_CONTACTS = "android.permission.READ_CONTACTS"
797
+ READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"
798
+ READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"
799
+ WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG"
800
+ WRITE_CONTACTS = "android.permission.WRITE_CONTACTS"
801
+ WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE"
802
+
803
+ implied = []
804
+
805
+ implied_WRITE_EXTERNAL_STORAGE = False
806
+ if target_sdk_version < 4:
807
+ if WRITE_EXTERNAL_STORAGE not in self.permissions:
808
+ implied.append([WRITE_EXTERNAL_STORAGE, None])
809
+ implied_WRITE_EXTERNAL_STORAGE = True
810
+ if READ_PHONE_STATE not in self.permissions:
811
+ implied.append([READ_PHONE_STATE, None])
812
+
813
+ if (
814
+ WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE
815
+ ) and READ_EXTERNAL_STORAGE not in self.permissions:
816
+ maxSdkVersion = None
817
+ for name, version in self.uses_permissions:
818
+ if name == WRITE_EXTERNAL_STORAGE:
819
+ maxSdkVersion = version
820
+ break
821
+ implied.append([READ_EXTERNAL_STORAGE, maxSdkVersion])
822
+
823
+ if target_sdk_version < 16:
824
+ if (
825
+ READ_CONTACTS in self.permissions
826
+ and READ_CALL_LOG not in self.permissions
827
+ ):
828
+ implied.append([READ_CALL_LOG, None])
829
+ if (
830
+ WRITE_CONTACTS in self.permissions
831
+ and WRITE_CALL_LOG not in self.permissions
832
+ ):
833
+ implied.append([WRITE_CALL_LOG, None])
834
+
835
+ return implied
836
+
837
+ @DeprecationWarning
838
+ def get_requested_permissions(self):
839
+ """
840
+ Returns all requested permissions.
841
+
842
+ :rtype: list of strings
843
+ """
844
+ return self.get_permissions()
845
+
846
+ def get_declared_permissions(self):
847
+ """
848
+ Returns list of the declared permissions.
849
+
850
+ :rtype: list of strings
851
+ """
852
+ return list(self.declared_permissions.keys())
853
+
854
+ def get_declared_permissions_details(self):
855
+ """
856
+ Returns declared permissions with the details.
857
+
858
+ :rtype: dict
859
+ """
860
+ return self.declared_permissions
861
+
862
+ def get_max_sdk_version(self):
863
+ """
864
+ Return the android:maxSdkVersion attribute
865
+
866
+ :rtype: string
867
+ """
868
+ return self.get_element("uses-sdk", "maxSdkVersion")
869
+
870
+ def get_min_sdk_version(self):
871
+ """
872
+ Return the android:minSdkVersion attribute
873
+
874
+ :rtype: string
875
+ """
876
+ return self.get_element("uses-sdk", "minSdkVersion")
877
+
878
+ def get_target_sdk_version(self):
879
+ """
880
+ Return the android:targetSdkVersion attribute
881
+
882
+ :rtype: string
883
+ """
884
+ return self.get_element("uses-sdk", "targetSdkVersion")
885
+
886
+ def get_effective_target_sdk_version(self):
887
+ """
888
+ Return the effective targetSdkVersion, always returns int > 0.
889
+
890
+ If the targetSdkVersion is not set, it defaults to 1. This is
891
+ set based on defaults as defined in:
892
+ https://developer.android.com/guide/topics/manifest/uses-sdk-element.html
893
+
894
+ :rtype: int
895
+ """
896
+ target_sdk_version = self.get_target_sdk_version()
897
+ if not target_sdk_version:
898
+ target_sdk_version = self.get_min_sdk_version()
899
+ try:
900
+ return int(target_sdk_version)
901
+ except (ValueError, TypeError):
902
+ return 1
903
+
904
+ def get_libraries(self):
905
+ """
906
+ Return the android:name attributes for libraries
907
+
908
+ :rtype: list
909
+ """
910
+ return self.get_elements("uses-library", "name")
911
+
912
+ def get_certificate_der(self, filename):
913
+ """
914
+ Return the DER coded X.509 certificate from the signature file.
915
+
916
+ :param filename: Signature filename in APK
917
+ :return: DER coded X.509 certificate as binary
918
+ """
919
+ pkcs7message = self.get_file(filename)
920
+
921
+ # TODO for correct parsing, we would need to write our own ASN1Spec for the SignatureBlock format
922
+ message, _ = decode(pkcs7message)
923
+ cert = encode(message[1][3])
924
+ # Remove the first identifier
925
+ # byte 0 == identifier, skip
926
+ # byte 1 == length. If byte1 & 0x80 > 1, we have long format
927
+ # The length of to read bytes is then coded
928
+ # in byte1 & 0x7F
929
+ # Check if the first byte is 0xA0 (Sequence Tag)
930
+ tag = cert[0]
931
+ l = cert[1]
932
+ # Python2 compliance
933
+ if not isinstance(l, int):
934
+ l = ord(l)
935
+ tag = ord(tag)
936
+ if tag == 0xA0:
937
+ cert = cert[2 + (l & 0x7F) if l & 0x80 > 1 else 2:]
938
+
939
+ return cert
940
+
941
+ def get_certificate(self, filename):
942
+ """
943
+ Return a X.509 certificate object by giving the name in the apk file
944
+
945
+ :param filename: filename of the signature file in the APK
946
+ :return: a `x509` certificate
947
+ """
948
+ cert = self.get_certificate_der(filename)
949
+ certificate = x509.load_der_x509_certificate(cert, default_backend())
950
+
951
+ return certificate
952
+
953
+ def new_zip(self, filename, deleted_files=None, new_files={}):
954
+ """
955
+ Create a new zip file
956
+
957
+ :param filename: the output filename of the zip
958
+ :param deleted_files: a regex pattern to remove specific file
959
+ :param new_files: a dictionnary of new files
960
+
961
+ :type filename: string
962
+ :type deleted_files: None or a string
963
+ :type new_files: a dictionnary (key:filename, value:content of the file)
964
+ """
965
+ zout = zipfile.ZipFile(filename, "w")
966
+
967
+ for item in self.zip.infolist():
968
+ # Block one: deleted_files, or deleted_files and new_files
969
+ if deleted_files is not None:
970
+ if re.match(deleted_files, item.filename) is None:
971
+ # if the regex of deleted_files doesn't match the filename
972
+ if new_files is not False:
973
+ if item.filename in new_files:
974
+ # and if the filename is in new_files
975
+ zout.writestr(item, new_files[item.filename])
976
+ continue
977
+ # Otherwise, write the original file.
978
+ buffer = self.zip.read(item.filename)
979
+ zout.writestr(item, buffer)
980
+ # Block two: deleted_files is None, new_files is not empty
981
+ elif new_files is not False:
982
+ if item.filename in new_files:
983
+ zout.writestr(item, new_files[item.filename])
984
+ else:
985
+ buffer = self.zip.read(item.filename)
986
+ zout.writestr(item, buffer)
987
+ # Block three: deleted_files is None, new_files is empty.
988
+ # Just write out the default zip
989
+ else:
990
+ buffer = self.zip.read(item.filename)
991
+ zout.writestr(item, buffer)
992
+ zout.close()
993
+
994
+ def get_android_manifest_axml(self):
995
+ """
996
+ Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file
997
+
998
+ :rtype: :class:`AXMLPrinter`
999
+ """
1000
+ try:
1001
+ return self.axml["AndroidManifest.xml"]
1002
+ except KeyError:
1003
+ return None
1004
+
1005
+ def get_android_manifest_xml(self):
1006
+ """
1007
+ Return the xml object which corresponds to the AndroidManifest.xml file
1008
+
1009
+ :rtype: object
1010
+ """
1011
+ try:
1012
+ return self.xml["AndroidManifest.xml"]
1013
+ except KeyError:
1014
+ return None
1015
+
1016
+ def get_android_resources(self):
1017
+ """
1018
+ Return the :class:`ARSCParser` object which corresponds to the resources.arsc file
1019
+
1020
+ :rtype: :class:`ARSCParser`
1021
+ """
1022
+ try:
1023
+ return self.arsc["resources.arsc"]
1024
+ except KeyError:
1025
+ if "resources.arsc" not in self.zip.namelist():
1026
+ # There is a rare case, that no resource file is supplied.
1027
+ # Maybe it was added manually, thus we check here
1028
+ return None
1029
+ self.arsc["resources.arsc"] = ARSCParser(self.zip.read("resources.arsc"))
1030
+ return self.arsc["resources.arsc"]
1031
+
1032
+ def is_signed(self):
1033
+ """
1034
+ Returns true if either a v1 or v2 (or both) signature was found.
1035
+ """
1036
+ return self.is_signed_v1() or self.is_signed_v2()
1037
+
1038
+ def is_signed_v1(self):
1039
+ """
1040
+ Returns true if a v1 / JAR signature was found.
1041
+ Returning `True` does not mean that the file is properly signed!
1042
+ It just says that there is a signature file which needs to be validated.
1043
+ """
1044
+ return self.get_signature_name() is not None
1045
+
1046
+ def is_signed_v2(self):
1047
+ """
1048
+ Returns true of a v2 / APK signature was found.
1049
+ Returning `True` does not mean that the file is properly signed!
1050
+ It just says that there is a signature file which needs to be validated.
1051
+ """
1052
+ if not self._is_signed_v2:
1053
+ # Need to find an v2 Block in the APK.
1054
+ # The Google Docs gives you the following rule:
1055
+ # * go to the end of the ZIP File
1056
+ # * search for the End of Central directory
1057
+ # * then jump to the beginning of the central directory
1058
+ # * Read now the magic of the signing block
1059
+ # * before the magic there is the size_of_block, so we can jump to
1060
+ # the beginning.
1061
+ # * There should be again the size_of_block
1062
+ # * Now we can read the Key-Values
1063
+ # * IDs with an unknown value should be ignored.
1064
+ f = io.BytesIO(self.__raw)
1065
+
1066
+ size_central = None
1067
+ offset_central = None
1068
+
1069
+ # Go to the end
1070
+ f.seek(-1, io.SEEK_END)
1071
+ # we know the minimal length for the central dir is 16+4+2
1072
+ f.seek(-20, io.SEEK_CUR)
1073
+ while f.tell() > 0:
1074
+ f.seek(-1, io.SEEK_CUR)
1075
+ r, = unpack("<4s", f.read(4))
1076
+ if r == self.PK_END_OF_CENTRAL_DIR:
1077
+ # Read central dir
1078
+ this_disk, disk_central, this_entries, total_entries, size_central, offset_central = unpack(
1079
+ "<HHHHII", f.read(16)
1080
+ )
1081
+ # TODO according to the standard we need to check if the
1082
+ # end of central directory is the last item in the zip file
1083
+ # TODO We also need to check if the central dir is exactly
1084
+ # before the end of central dir...
1085
+
1086
+ # These things should not happen for APKs
1087
+ assert this_disk == 0, "Not sure what to do with multi disk ZIP!"
1088
+ assert disk_central == 0, "Not sure what to do with multi disk ZIP!"
1089
+ break
1090
+ f.seek(-4, io.SEEK_CUR)
1091
+ if offset_central:
1092
+ f.seek(offset_central)
1093
+ r, = unpack("<4s", f.read(4))
1094
+ f.seek(-4, io.SEEK_CUR)
1095
+ assert r == self.PK_CENTRAL_DIR, "No Central Dir at specified offset"
1096
+
1097
+ # Go back and check if we have a magic
1098
+ end_offset = f.tell()
1099
+ f.seek(-24, io.SEEK_CUR)
1100
+ size_of_block, magic = unpack("<Q16s", f.read(24))
1101
+ self._is_signed_v2 = False
1102
+ if magic == self.APK_SIG_MAGIC:
1103
+ # go back size_of_blocks + 8 and read size_of_block again
1104
+ f.seek(-(size_of_block + 8), io.SEEK_CUR)
1105
+ size_of_block_start, = unpack("<Q", f.read(8))
1106
+ assert (
1107
+ size_of_block_start == size_of_block
1108
+ ), "Sizes at beginning and and does not match!"
1109
+
1110
+ # Store all blocks
1111
+ while f.tell() < end_offset - 24:
1112
+ size, key = unpack("<QI", f.read(12))
1113
+ value = f.read(size - 4)
1114
+ self._v2_blocks[key] = value
1115
+
1116
+ # Test if a signature is found
1117
+ if self.APK_SIG_KEY_SIGNATURE in self._v2_blocks:
1118
+ self._is_signed_v2 = True
1119
+
1120
+ return self._is_signed_v2
1121
+
1122
+ def get_certificates_der_v2(self):
1123
+ """
1124
+ Return a list of DER coded X.509 certificates from the v2 signature
1125
+ """
1126
+ # calling is_signed_v2 should also load the signature, if any
1127
+ if not self.is_signed_v2():
1128
+ return []
1129
+
1130
+ certificates = []
1131
+ block_bytes = self._v2_blocks[self.APK_SIG_KEY_SIGNATURE]
1132
+ block = io.BytesIO(block_bytes)
1133
+
1134
+ size_sequence, = unpack("<I", block.read(4))
1135
+ assert size_sequence + 4 == len(
1136
+ block_bytes
1137
+ ), "size of sequence and blocksize does not match"
1138
+ while block.tell() < len(block_bytes):
1139
+ size_signer, = unpack("<I", block.read(4))
1140
+
1141
+ len_signed_data, = unpack("<I", block.read(4))
1142
+ len_digests, = unpack("<I", block.read(4))
1143
+ # Skip it for now
1144
+ block.seek(len_digests, io.SEEK_CUR)
1145
+
1146
+ len_certs, = unpack("<I", block.read(4))
1147
+ start_certs = block.tell()
1148
+ while block.tell() < start_certs + len_certs:
1149
+ len_cert, = unpack("<I", block.read(4))
1150
+ certificates.append(block.read(len_cert))
1151
+
1152
+ # Now we have the signatures and the public key...
1153
+ # we need to read it (or at least skip it)
1154
+ len_attr, = unpack("<I", block.read(4))
1155
+ block.seek(len_attr, io.SEEK_CUR)
1156
+ len_sigs, = unpack("<I", block.read(4))
1157
+ block.seek(len_sigs, io.SEEK_CUR)
1158
+ len_publickey, = unpack("<I", block.read(4))
1159
+ block.seek(len_publickey, io.SEEK_CUR)
1160
+
1161
+ return certificates
1162
+
1163
+ def get_certificates_v2(self):
1164
+ """
1165
+ Return a list of :class:`cryptography.x509.Certificate` which are found
1166
+ in the v2 signing block.
1167
+ Note that we simply extract all certificates regardless of the signer.
1168
+ Therefore this is just a list of all certificates found in all signers.
1169
+ """
1170
+ certs = []
1171
+ for cert in self.get_certificates_der_v2():
1172
+ certs.append(x509.load_der_x509_certificate(cert, default_backend()))
1173
+
1174
+ return certs
1175
+
1176
+ def get_signature_name(self):
1177
+ """
1178
+ Return the name of the first signature file found.
1179
+ """
1180
+ if self.get_signature_names():
1181
+ return self.get_signature_names()[0]
1182
+ else:
1183
+ # Unsigned APK
1184
+ return None
1185
+
1186
+ def get_signature_names(self):
1187
+ """
1188
+ Return a list of the signature file names (v1 Signature / JAR
1189
+ Signature)
1190
+
1191
+ :rtype: List of filenames matching a Signature
1192
+ """
1193
+ signature_expr = re.compile(r"^(META-INF/)(.*)(\.RSA|\.EC|\.DSA)$")
1194
+ signatures = []
1195
+
1196
+ for i in self.get_files():
1197
+ if signature_expr.search(i):
1198
+ signatures.append(i)
1199
+
1200
+ return signatures
1201
+
1202
+ def get_signature(self):
1203
+ """
1204
+ Return the data of the first signature file found (v1 Signature / JAR
1205
+ Signature)
1206
+
1207
+ :rtype: First signature name or None if not signed
1208
+ """
1209
+ if self.get_signatures():
1210
+ return self.get_signatures()[0]
1211
+ else:
1212
+ return None
1213
+
1214
+ def get_signatures(self):
1215
+ """
1216
+ Return a list of the data of the signature files.
1217
+ Only v1 / JAR Signing.
1218
+
1219
+ :rtype: list of bytes
1220
+ """
1221
+ signature_expr = re.compile(r"^(META-INF/)(.*)(\.RSA|\.EC|\.DSA)$")
1222
+ signature_datas = []
1223
+
1224
+ for i in self.get_files():
1225
+ if signature_expr.search(i):
1226
+ signature_datas.append(self.get_file(i))
1227
+
1228
+ return signature_datas