From 2975d4c09fce2fc048db1a15b77e459552b74bec Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner <hans@eds.org>
Date: Tue, 5 Jan 2021 15:26:38 +0100
Subject: [PATCH] always use fingerprint hashes in lowercase

* Utils.getBinaryHash() converts it to lowercase()
* Utils.getPackageSig() outputs lowercase
* fdroidserver outputs lowercase for all hash entries
---
 .../main/java/org/fdroid/fdroid/Utils.java    | 30 ++++++++++----
 .../main/java/org/fdroid/fdroid/data/App.java | 19 +--------
 .../java/org/fdroid/fdroid/UtilsTest.java     | 41 ++++++++++++++++++-
 3 files changed, 63 insertions(+), 27 deletions(-)

diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java
index b9b101d48..34623bd1c 100644
--- a/app/src/main/java/org/fdroid/fdroid/Utils.java
+++ b/app/src/main/java/org/fdroid/fdroid/Utils.java
@@ -33,10 +33,6 @@ import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.StatFs;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 import android.text.Editable;
 import android.text.Html;
 import android.text.SpannableStringBuilder;
@@ -52,6 +48,10 @@ import android.view.View;
 import android.view.ViewTreeObserver;
 import android.widget.ImageView;
 import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 import com.nostra13.universalimageloader.core.DisplayImageOptions;
 import com.nostra13.universalimageloader.core.ImageLoader;
 import com.nostra13.universalimageloader.core.assist.ImageScaleType;
@@ -406,10 +406,26 @@ public final class Utils {
         return ret;
     }
 
+
     /**
      * Get the fingerprint used to represent an APK signing key in F-Droid.
      * This is a custom fingerprint algorithm that was kind of accidentally
      * created, but is still in use.
+     *
+     * @see #getPackageSig(PackageInfo)
+     * @see org.fdroid.fdroid.data.Apk#sig
+     */
+    public static String getsig(byte[] rawCertBytes) {
+        return Utils.hashBytes(toHexString(rawCertBytes).getBytes(), "md5");
+    }
+
+    /**
+     * Get the fingerprint used to represent an APK signing key in F-Droid.
+     * This is a custom fingerprint algorithm that was kind of accidentally
+     * created, but is still in use.
+     *
+     * @see #getsig(byte[])
+     * @see org.fdroid.fdroid.data.Apk#sig
      */
     public static String getPackageSig(PackageInfo info) {
         if (info == null || info.signatures == null || info.signatures.length < 1) {
@@ -556,7 +572,7 @@ public final class Utils {
             }
 
             byte[] mdbytes = md.digest();
-            return toHexString(mdbytes).toLowerCase(Locale.ENGLISH);
+            return toHexString(mdbytes);
         } catch (IOException e) {
             String message = e.getMessage();
             if (message.contains("read failed: EIO (I/O error)")) {
@@ -576,7 +592,7 @@ public final class Utils {
      * Computes the base 16 representation of the byte array argument.
      *
      * @param bytes an array of bytes.
-     * @return the bytes represented as a string of hexadecimal digits.
+     * @return the bytes represented as a string of lowercase hexadecimal digits.
      * @see <a href="https://stackoverflow.com/a/9855338">source</a>
      */
     public static String toHexString(byte[] bytes) {
@@ -589,7 +605,7 @@ public final class Utils {
         return new String(hexChars);
     }
 
-    private static final char[] HEX_LOOKUP_ARRAY = "0123456789ABCDEF".toCharArray();
+    private static final char[] HEX_LOOKUP_ARRAY = "0123456789abcdef".toCharArray();
 
     public static int parseInt(String str, int fallback) {
         if (str == null || str.length() == 0) {
diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index 9e69bd899..3ba04b38f 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -949,24 +949,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
         }
         apkJar.close();
 
-        /*
-         * I don't fully understand the loop used here. I've copied it verbatim
-         * from getsig.java bundled with FDroidServer. I *believe* it is taking
-         * the raw byte encoding of the certificate & converting it to a byte
-         * array of the hex representation of the original certificate byte
-         * array. This is then MD5 sum'd. It's a really bad way to be doing this
-         * if I'm right... If I'm not right, I really don't know! see lines
-         * 67->75 in getsig.java bundled with Fdroidserver
-         */
-        final byte[] fdroidSig = new byte[rawCertBytes.length * 2];
-        for (int j = 0; j < rawCertBytes.length; j++) {
-            byte v = rawCertBytes[j];
-            int d = (v >> 4) & 0xF;
-            fdroidSig[j * 2] = (byte) (d >= 10 ? ('a' + d - 10) : ('0' + d));
-            d = v & 0xF;
-            fdroidSig[j * 2 + 1] = (byte) (d >= 10 ? ('a' + d - 10) : ('0' + d));
-        }
-        apk.sig = Utils.hashBytes(fdroidSig, "md5");
+        apk.sig = Utils.getsig(rawCertBytes);
     }
 
     /**
diff --git a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java b/app/src/test/java/org/fdroid/fdroid/UtilsTest.java
index 1c6453d63..518d63dba 100644
--- a/app/src/test/java/org/fdroid/fdroid/UtilsTest.java
+++ b/app/src/test/java/org/fdroid/fdroid/UtilsTest.java
@@ -2,9 +2,9 @@
 package org.fdroid.fdroid;
 
 import android.content.Context;
-
+import android.content.pm.PackageInfo;
+import android.content.pm.Signature;
 import androidx.test.core.app.ApplicationProvider;
-
 import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -12,6 +12,7 @@ import org.robolectric.RobolectricTestRunner;
 
 import java.io.File;
 import java.util.Date;
+import java.util.Random;
 import java.util.TimeZone;
 
 import static org.junit.Assert.assertEquals;
@@ -215,4 +216,40 @@ public class UtilsTest {
             }
         }
     }
+
+    /**
+     * Test the replacement for the ancient fingerprint algorithm.
+     *
+     * @see org.fdroid.fdroid.data.Apk#sig
+     */
+    @Test
+    public void testGetsig() {
+        /*
+         * I don't fully understand the loop used here. I've copied it verbatim
+         * from getsig.java bundled with FDroidServer. I *believe* it is taking
+         * the raw byte encoding of the certificate & converting it to a byte
+         * array of the hex representation of the original certificate byte
+         * array. This is then MD5 sum'd. It's a really bad way to be doing this
+         * if I'm right... If I'm not right, I really don't know! see lines
+         * 67->75 in getsig.java bundled with Fdroidserver
+         */
+        for (int length : new int[]{256, 345, 1233, 4032, 12092}) {
+            byte[] rawCertBytes = new byte[length];
+            new Random().nextBytes(rawCertBytes);
+            final byte[] fdroidSig = new byte[rawCertBytes.length * 2];
+            for (int j = 0; j < rawCertBytes.length; j++) {
+                byte v = rawCertBytes[j];
+                int d = (v >> 4) & 0xF;
+                fdroidSig[j * 2] = (byte) (d >= 10 ? ('a' + d - 10) : ('0' + d));
+                d = v & 0xF;
+                fdroidSig[j * 2 + 1] = (byte) (d >= 10 ? ('a' + d - 10) : ('0' + d));
+            }
+            String sig = Utils.hashBytes(fdroidSig, "md5");
+            assertEquals(sig, Utils.getsig(rawCertBytes));
+
+            PackageInfo packageInfo = new PackageInfo();
+            packageInfo.signatures = new Signature[]{new Signature(rawCertBytes)};
+            assertEquals(sig, Utils.getPackageSig(packageInfo));
+        }
+    }
 }