chiark / gitweb /
update: reject APKs with invalid file sig, probably Janus exploits
[fdroidserver.git] / fdroidserver / update.py
index 83018a753dfde47b0c98b7ca10f2da6f61fbf5e7..f90e49932258315d985a2286bcf76a7fe674cdb5 100644 (file)
@@ -29,7 +29,7 @@ import zipfile
 import hashlib
 import pickle
 import time
-from datetime import datetime, timedelta
+from datetime import datetime
 from argparse import ArgumentParser
 
 import collections
@@ -132,13 +132,13 @@ def update_wiki(apps, sortedids, apks):
         if app.Disabled:
             wikidata += '{{Disabled|' + app.Disabled + '}}\n'
         if app.AntiFeatures:
-            for af in app.AntiFeatures:
+            for af in sorted(app.AntiFeatures):
                 wikidata += '{{AntiFeature|' + af + '}}\n'
         if app.RequiresRoot:
             requiresroot = 'Yes'
         else:
             requiresroot = 'No'
-        wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % (
+        wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|liberapay=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % (
             appid,
             app.Name,
             app.added.strftime('%Y-%m-%d') if app.added else '',
@@ -149,6 +149,7 @@ def update_wiki(apps, sortedids, apks):
             app.Changelog,
             app.Donate,
             app.FlattrID,
+            app.LiberapayID,
             app.Bitcoin,
             app.Litecoin,
             app.License,
@@ -495,14 +496,25 @@ def has_known_vulnerability(filename):
 
     Checks whether there are more than one classes.dex or AndroidManifest.xml
     files, which is invalid and an essential part of the "Master Key" attack.
-
     http://www.saurik.com/id/17
+
+    Janus is similar to Master Key but is perhaps easier to scan for.
+    https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures
     """
 
+    found_vuln = False
+
     # statically load this pattern
     if not hasattr(has_known_vulnerability, "pattern"):
         has_known_vulnerability.pattern = re.compile(b'.*OpenSSL ([01][0-9a-z.-]+)')
 
+    with open(filename.encode(), 'rb') as fp:
+        first4 = fp.read(4)
+    if first4 != b'\x50\x4b\x03\x04':
+        raise FDroidException(_('{path} has bad file signature "{pattern}", possible Janus exploit!')
+                              .format(path=filename, pattern=first4.decode().replace('\n', ' ')) + '\n'
+                              + 'https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures')
+
     files_in_apk = set()
     with zipfile.ZipFile(filename) as zf:
         for name in zf.namelist():
@@ -523,14 +535,15 @@ def has_known_vulnerability(filename):
                         else:
                             logging.warning(_('"{path}" contains outdated {name} ({version})')
                                             .format(path=filename, name=name, version=version))
-                            return True
+                            found_vuln = True
                         break
             elif name == 'AndroidManifest.xml' or name == 'classes.dex' or name.endswith('.so'):
                 if name in files_in_apk:
-                    return True
+                    logging.warning(_('{apkfilename} has multiple {name} files, looks like Master Key exploit!')
+                                    .format(apkfilename=filename, name=name))
+                    found_vuln = True
                 files_in_apk.add(name)
-
-    return False
+    return found_vuln
 
 
 def insert_obbs(repodir, apps, apks):
@@ -772,7 +785,8 @@ def insert_localized_app_metadata(apps):
 
     """
 
-    sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*'))
+    sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'src', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*'))
+    sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*'))
     sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'metadata', '[a-z][a-z]*'))
     sourcedirs += glob.glob(os.path.join('metadata', '[A-Za-z]*', '[a-z][a-z]*'))
 
@@ -787,6 +801,17 @@ def insert_localized_app_metadata(apps):
                 continue
             locale = segments[-1]
             destdir = os.path.join('repo', packageName, locale)
+
+            # flavours specified in build receipt
+            build_flavours = ""
+            if apps[packageName] and 'builds' in apps[packageName] and len(apps[packageName].builds) > 0\
+                    and 'gradle' in apps[packageName].builds[-1]:
+                build_flavours = apps[packageName].builds[-1].gradle
+
+            if len(segments) >= 5 and segments[4] == "fastlane" and segments[3] not in build_flavours:
+                logging.debug("ignoring due to wrong flavour")
+                continue
+
             for f in files:
                 if f in ('description.txt', 'full_description.txt'):
                     _set_localized_text_entry(apps[packageName], locale, 'description',
@@ -846,24 +871,24 @@ def insert_localized_app_metadata(apps):
             base, extension = common.get_extension(filename)
 
             if packageName not in apps:
-                logging.warning('Found "%s" graphic without metadata for app "%s"!'
-                                % (filename, packageName))
+                logging.warning(_('Found "{path}" graphic without metadata for app "{name}"!')
+                                .format(path=filename, name=packageName))
                 continue
             graphics = _get_localized_dict(apps[packageName], locale)
 
             if extension not in ALLOWED_EXTENSIONS:
-                logging.warning('Only PNG and JPEG are supported for graphics, found: ' + f)
+                logging.warning(_('Only PNG and JPEG are supported for graphics, found: {path}').format(path=f))
             elif base in GRAPHIC_NAMES:
                 # there can only be zero or one of these per locale
                 graphics[base] = filename
             elif screenshotdir in SCREENSHOT_DIRS:
                 # there can any number of these per locale
-                logging.debug('adding to ' + screenshotdir + ': ' + f)
+                logging.debug(_('adding to {name}: {path}').format(name=screenshotdir, path=f))
                 if screenshotdir not in graphics:
                     graphics[screenshotdir] = []
                 graphics[screenshotdir].append(filename)
             else:
-                logging.warning('Unsupported graphics file found: ' + f)
+                logging.warning(_('Unsupported graphics file found: {path}').format(path=f))
 
 
 def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False):
@@ -1297,22 +1322,11 @@ def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=Fal
 
         apkzip = zipfile.ZipFile(apkfile, 'r')
 
-        # if an APK has files newer than the system time, suggest updating
-        # the system clock.  This is useful for offline systems, used for
-        # signing, which do not have another source of clock sync info. It
-        # has to be more than 24 hours newer because ZIP/APK files do not
-        # store timezone info
         manifest = apkzip.getinfo('AndroidManifest.xml')
         if manifest.date_time[1] == 0:  # month can't be zero
             logging.debug(_('AndroidManifest.xml has no date'))
         else:
-            dt_obj = datetime(*manifest.date_time)
-            checkdt = dt_obj - timedelta(1)
-            if datetime.today() < checkdt:
-                logging.warning('System clock is older than manifest in: '
-                                + apkfilename
-                                + '\nSet clock to that time using:\n'
-                                + 'sudo date -s "' + str(dt_obj) + '"')
+            common.check_system_clock(datetime(*manifest.date_time), apkfilename)
 
         # extract icons from APK zip file
         iconfilename = "%s.%s.png" % (apk['packageName'], apk['versionCode'])
@@ -1625,7 +1639,7 @@ def move_apk_between_sections(from_dir, to_dir, apk):
     for density in all_screen_densities:
         from_icon_dir = get_icon_dir(from_dir, density)
         to_icon_dir = get_icon_dir(to_dir, density)
-        if density not in apk['icons']:
+        if density not in apk.get('icons', []):
             continue
         _move_file(from_icon_dir, to_icon_dir, apk['icons'][density], True)
     if 'srcname' in apk:
@@ -1669,7 +1683,7 @@ def create_metadata_from_template(apk):
         with open('template.yml') as f:
             metatxt = f.read()
         if 'name' in apk and apk['name'] != '':
-            metatxt = re.sub(r'^(((Auto)?Name|Summary):).*$',
+            metatxt = re.sub(r'''^(((Auto)?Name|Summary):)[ '"\.]*$''',
                              r'\1 ' + apk['name'],
                              metatxt,
                              flags=re.IGNORECASE | re.MULTILINE)
@@ -1715,9 +1729,9 @@ def main():
     parser = ArgumentParser()
     common.setup_global_opts(parser)
     parser.add_argument("--create-key", action="store_true", default=False,
-                        help=_("Create a repo signing key in a keystore"))
+                        help=_("Add a repo signing key to an unsigned repo"))
     parser.add_argument("-c", "--create-metadata", action="store_true", default=False,
-                        help=_("Create skeleton metadata files that are missing"))
+                        help=_("Add skeleton metadata files for APKs that are missing them"))
     parser.add_argument("--delete-unknown", action="store_true", default=False,
                         help=_("Delete APKs and/or OBBs without metadata from the repo"))
     parser.add_argument("-b", "--buildreport", action="store_true", default=False,
@@ -1732,7 +1746,7 @@ def main():
     parser.add_argument("-w", "--wiki", default=False, action="store_true",
                         help=_("Update the wiki"))
     parser.add_argument("--pretty", action="store_true", default=False,
-                        help=_("Produce human-readable index.xml"))
+                        help=_("Produce human-readable XML/JSON for index files"))
     parser.add_argument("--clean", action="store_true", default=False,
                         help=_("Clean update - don't uses caches, reprocess all APKs"))
     parser.add_argument("--nosign", action="store_true", default=False,