diff --git a/Dockerfile b/Dockerfile index 61e4ba34..f319d00c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,9 +84,7 @@ RUN mkdir -p /tmp/signed /tmp/release && \ cd /tmp/signed && zip -qr /app/signed-stripped.apk . && \ cd /tmp/release && zip -qr /release_apk/official-stripped.apk . -# Download apkdiff.py from Signal's reproducible builds branch -RUN wget -q https://raw.githubusercontent.com/signalapp/Signal-Android/refs/heads/main/reproducible-builds/apkdiff/apkdiff.py \ - -O /usr/local/bin/apkdiff.py && chmod +x /usr/local/bin/apkdiff.py +RUN chmod +x apkdiff.py && cp apkdiff.py /usr/local/bin/apkdiff.py # Compare the stripped APKs CMD sh -c "python3 /usr/local/bin/apkdiff.py /release_apk/official-stripped.apk /app/signed-stripped.apk" diff --git a/apkdiff.py b/apkdiff.py new file mode 100644 index 00000000..ddf1a52a --- /dev/null +++ b/apkdiff.py @@ -0,0 +1,84 @@ +#! /usr/bin/env python3 + +import sys +from zipfile import ZipFile + + +class ApkDiff: + IGNORE_FILES = [ + # Related to app signing. Not expected to be present in unsigned builds. Doesn't affect app code. + "META-INF/MANIFEST.MF", + "META-INF/CERTIFIC.SF", + "META-INF/CERTIFIC.RSA", + "META-INF/TEXTSECU.SF", + "META-INF/TEXTSECU.RSA", + "META-INF/BNDLTOOL.SF", + "META-INF/BNDLTOOL.RSA", + "META-INF/code_transparency_signed.jwt", + "stamp-cert-sha256" + ] + + def compare(self, firstApk, secondApk): + firstZip = ZipFile(firstApk, 'r') + secondZip = ZipFile(secondApk, 'r') + + return self.compareEntryNames(firstZip, secondZip) and self.compareEntryContents(firstZip, secondZip) == True + + def compareEntryNames(self, firstZip, secondZip): + firstNameListSorted = sorted(firstZip.namelist()) + secondNameListSorted = sorted(secondZip.namelist()) + + for ignoreFile in self.IGNORE_FILES: + while ignoreFile in firstNameListSorted: + firstNameListSorted.remove(ignoreFile) + while ignoreFile in secondNameListSorted: + secondNameListSorted.remove(ignoreFile) + + if len(firstNameListSorted) != len(secondNameListSorted): + print("Manifest lengths differ!") + + for (firstEntryName, secondEntryName) in zip(firstNameListSorted, secondNameListSorted): + if firstEntryName != secondEntryName: + print("Sorted manifests don't match, %s vs %s" % (firstEntryName, secondEntryName)) + return False + + return True + + def compareEntryContents(self, firstZip, secondZip): + firstInfoList = list(filter(lambda info: info.filename not in self.IGNORE_FILES, firstZip.infolist())) + secondInfoList = list(filter(lambda info: info.filename not in self.IGNORE_FILES, secondZip.infolist())) + + if len(firstInfoList) != len(secondInfoList): + print("APK info lists of different length!") + return False + + success = True + for firstEntryInfo in firstInfoList: + for secondEntryInfo in list(secondInfoList): + if firstEntryInfo.filename == secondEntryInfo.filename: + firstEntryBytes = firstZip.read(firstEntryInfo.filename) + secondEntryBytes = secondZip.read(secondEntryInfo.filename) + + if firstEntryBytes != secondEntryBytes: + firstZip.extract(firstEntryInfo, "mismatches/first") + secondZip.extract(secondEntryInfo, "mismatches/second") + print("APKs differ on file %s! Files extracted to the mismatches/ directory." % (firstEntryInfo.filename)) + success = False + + secondInfoList.remove(secondEntryInfo) + break + + return success + + +if __name__ == '__main__': + if len(sys.argv) != 3: + print("Usage: apkdiff ") + sys.exit(1) + + if ApkDiff().compare(sys.argv[1], sys.argv[2]): + print("APKs match!") + sys.exit(0) + else: + print("APKs don't match!") + sys.exit(1)