From 0f175de5996abfacf64b8d8cb08d267b75175822 Mon Sep 17 00:00:00 2001 From: TacoTheDank Date: Thu, 3 Mar 2022 13:21:50 -0500 Subject: [PATCH] Kotlin-ize ReleaseVersionUtil, merge with NewVersionManager --- .../schabi/newpipe/CheckForNewAppVersion.java | 12 +- .../org/schabi/newpipe/NewVersionManager.kt | 28 ----- .../newpipe/util/ReleaseVersionUtil.java | 96 -------------- .../schabi/newpipe/util/ReleaseVersionUtil.kt | 118 ++++++++++++++++++ .../schabi/newpipe/NewVersionManagerTest.kt | 20 ++- 5 files changed, 130 insertions(+), 144 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/NewVersionManager.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index ca5862333..21673942b 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -73,7 +73,6 @@ public final class CheckForNewAppVersion extends IntentService { final App app = App.getApp(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); - final NewVersionManager manager = new NewVersionManager(); // Check if the current apk is a github one or not. if (!ReleaseVersionUtil.isReleaseApk()) { @@ -83,24 +82,23 @@ public final class CheckForNewAppVersion extends IntentService { // Check if the last request has happened a certain time ago // to reduce the number of API requests. final long expiry = prefs.getLong(app.getString(R.string.update_expiry_key), 0); - if (!manager.isExpired(expiry)) { + if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) { return; } // Make a network request to get latest NewPipe data. final Response response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL); - handleResponse(response, manager); + handleResponse(response); } - private void handleResponse(@NonNull final Response response, - @NonNull final NewVersionManager manager) { + private void handleResponse(@NonNull final Response response) { final App app = App.getApp(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app); try { // Store a timestamp which needs to be exceeded, // before a new request to the API is made. - final long newExpiry = manager - .coerceExpiry(response.getHeader("expires")); + final long newExpiry = ReleaseVersionUtil + .coerceUpdateCheckExpiry(response.getHeader("expires")); prefs.edit() .putLong(app.getString(R.string.update_expiry_key), newExpiry) .apply(); diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt b/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt deleted file mode 100644 index 36de1ecfc..000000000 --- a/app/src/main/java/org/schabi/newpipe/NewVersionManager.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.schabi.newpipe - -import java.time.Instant -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -class NewVersionManager { - - fun isExpired(expiry: Long): Boolean { - return Instant.ofEpochSecond(expiry).isBefore(Instant.now()) - } - - /** - * Coerce expiry date time in between 6 hours and 72 hours from now - * - * @return Epoch second of expiry date time - */ - fun coerceExpiry(expiryString: String?): Long { - val now = ZonedDateTime.now() - return expiryString?.let { - - var expiry = ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString)) - expiry = maxOf(expiry, now.plusHours(6)) - expiry = minOf(expiry, now.plusHours(72)) - expiry.toEpochSecond() - } ?: now.plusHours(6).toEpochSecond() - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java deleted file mode 100644 index 5b3cfea92..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.pm.PackageManager; -import android.content.pm.Signature; - -import androidx.annotation.NonNull; -import androidx.core.content.pm.PackageInfoCompat; - -import org.schabi.newpipe.App; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.ErrorUtil; -import org.schabi.newpipe.error.UserAction; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.List; - -public class ReleaseVersionUtil { - // Public key of the certificate that is used in NewPipe release versions - private static final String RELEASE_CERT_PUBLIC_KEY_SHA1 - = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; - - public static boolean isReleaseApk() { - return getCertificateSHA1Fingerprint().equals(RELEASE_CERT_PUBLIC_KEY_SHA1); - } - - /** - * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. - * - * @return String with the APK's SHA1 fingerprint in hexadecimal - */ - @NonNull - private static String getCertificateSHA1Fingerprint() { - final App app = App.getApp(); - final List signatures; - try { - signatures = PackageInfoCompat.getSignatures(app.getPackageManager(), - app.getPackageName()); - } catch (final PackageManager.NameNotFoundException e) { - ErrorUtil.createNotification(app, new ErrorInfo(e, - UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")); - return ""; - } - if (signatures.isEmpty()) { - return ""; - } - - final X509Certificate c; - try { - final byte[] cert = signatures.get(0).toByteArray(); - final InputStream input = new ByteArrayInputStream(cert); - final CertificateFactory cf = CertificateFactory.getInstance("X509"); - c = (X509Certificate) cf.generateCertificate(input); - } catch (final CertificateException e) { - ErrorUtil.createNotification(app, new ErrorInfo(e, - UserAction.CHECK_FOR_NEW_APP_VERSION, "Certificate error")); - return ""; - } - - try { - final MessageDigest md = MessageDigest.getInstance("SHA1"); - final byte[] publicKey = md.digest(c.getEncoded()); - return byte2HexFormatted(publicKey); - } catch (NoSuchAlgorithmException | CertificateEncodingException e) { - ErrorUtil.createNotification(app, new ErrorInfo(e, - UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not retrieve SHA1 key")); - return ""; - } - } - - private static String byte2HexFormatted(final byte[] arr) { - final StringBuilder str = new StringBuilder(arr.length * 2); - - for (int i = 0; i < arr.length; i++) { - String h = Integer.toHexString(arr[i]); - final int l = h.length(); - if (l == 1) { - h = "0" + h; - } - if (l > 2) { - h = h.substring(l - 2, l); - } - str.append(h.toUpperCase()); - if (i < (arr.length - 1)) { - str.append(':'); - } - } - return str.toString(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt new file mode 100644 index 000000000..4ec2a9614 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -0,0 +1,118 @@ +package org.schabi.newpipe.util + +import android.content.pm.PackageManager +import android.content.pm.Signature +import androidx.core.content.pm.PackageInfoCompat +import org.schabi.newpipe.App +import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification +import org.schabi.newpipe.error.UserAction +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.time.Instant +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +object ReleaseVersionUtil { + // Public key of the certificate that is used in NewPipe release versions + private const val RELEASE_CERT_PUBLIC_KEY_SHA1 = + "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15" + + @JvmStatic + fun isReleaseApk(): Boolean { + return certificateSHA1Fingerprint == RELEASE_CERT_PUBLIC_KEY_SHA1 + } + + /** + * Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133. + * + * @return String with the APK's SHA1 fingerprint in hexadecimal + */ + private val certificateSHA1Fingerprint: String + get() { + val app = App.getApp() + val signatures: List = try { + PackageInfoCompat.getSignatures(app.packageManager, app.packageName) + } catch (e: PackageManager.NameNotFoundException) { + showRequestError(app, e, "Could not find package info") + return "" + } + if (signatures.isEmpty()) { + return "" + } + val x509cert = try { + val cert = signatures[0].toByteArray() + val input: InputStream = ByteArrayInputStream(cert) + val cf = CertificateFactory.getInstance("X509") + cf.generateCertificate(input) as X509Certificate + } catch (e: CertificateException) { + showRequestError(app, e, "Certificate error") + return "" + } + + return try { + val md = MessageDigest.getInstance("SHA1") + val publicKey = md.digest(x509cert.encoded) + byte2HexFormatted(publicKey) + } catch (e: NoSuchAlgorithmException) { + showRequestError(app, e, "Could not retrieve SHA1 key") + "" + } catch (e: CertificateEncodingException) { + showRequestError(app, e, "Could not retrieve SHA1 key") + "" + } + } + + private fun byte2HexFormatted(arr: ByteArray): String { + val str = StringBuilder(arr.size * 2) + for (i in arr.indices) { + var h = Integer.toHexString(arr[i].toInt()) + val l = h.length + if (l == 1) { + h = "0$h" + } + if (l > 2) { + h = h.substring(l - 2, l) + } + str.append(h.uppercase()) + if (i < arr.size - 1) { + str.append(':') + } + } + return str.toString() + } + + private fun showRequestError(app: App, e: Exception, request: String) { + createNotification( + app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, request) + ) + } + + @JvmStatic + fun isLastUpdateCheckExpired(expiry: Long): Boolean { + return Instant.ofEpochSecond(expiry).isBefore(Instant.now()) + } + + /** + * Coerce expiry date time in between 6 hours and 72 hours from now + * + * @return Epoch second of expiry date time + */ + @JvmStatic + fun coerceUpdateCheckExpiry(expiryString: String?): Long { + val now = ZonedDateTime.now() + return expiryString?.let { + var expiry = + ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString)) + expiry = maxOf(expiry, now.plusHours(6)) + expiry = minOf(expiry, now.plusHours(72)) + expiry.toEpochSecond() + } ?: now.plusHours(6).toEpochSecond() + } +} diff --git a/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt b/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt index d2dacc783..7a2d965f7 100644 --- a/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt +++ b/app/src/test/java/org/schabi/newpipe/NewVersionManagerTest.kt @@ -2,8 +2,9 @@ package org.schabi.newpipe import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test +import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry +import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -11,18 +12,11 @@ import kotlin.math.abs class NewVersionManagerTest { - private lateinit var manager: NewVersionManager - - @Before - fun setup() { - manager = NewVersionManager() - } - @Test fun `Expiry is reached`() { val oneHourEarlier = Instant.now().atZone(ZoneId.of("GMT")).minusHours(1) - val expired = manager.isExpired(oneHourEarlier.toEpochSecond()) + val expired = isLastUpdateCheckExpired(oneHourEarlier.toEpochSecond()) assertTrue(expired) } @@ -31,7 +25,7 @@ class NewVersionManagerTest { fun `Expiry is not reached`() { val oneHourLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(1) - val expired = manager.isExpired(oneHourLater.toEpochSecond()) + val expired = isLastUpdateCheckExpired(oneHourLater.toEpochSecond()) assertFalse(expired) } @@ -47,7 +41,7 @@ class NewVersionManagerTest { fun `Expiry must be returned as is because it is inside the acceptable range of 6-72 hours`() { val sixHoursLater = Instant.now().atZone(ZoneId.of("GMT")).plusHours(6) - val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(sixHoursLater)) + val coerced = coerceUpdateCheckExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(sixHoursLater)) assertNearlyEqual(sixHoursLater.toEpochSecond(), coerced) } @@ -56,7 +50,7 @@ class NewVersionManagerTest { fun `Expiry must be increased to 6 hours if below`() { val tooLow = Instant.now().atZone(ZoneId.of("GMT")).plusHours(5) - val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooLow)) + val coerced = coerceUpdateCheckExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooLow)) assertNearlyEqual(tooLow.plusHours(1).toEpochSecond(), coerced) } @@ -65,7 +59,7 @@ class NewVersionManagerTest { fun `Expiry must be decreased to 72 hours if above`() { val tooHigh = Instant.now().atZone(ZoneId.of("GMT")).plusHours(73) - val coerced = manager.coerceExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooHigh)) + val coerced = coerceUpdateCheckExpiry(DateTimeFormatter.RFC_1123_DATE_TIME.format(tooHigh)) assertNearlyEqual(tooHigh.minusHours(1).toEpochSecond(), coerced) }