Merge branch 'fix-111/lollipop-sadness' into 'master'
Fix issue #111 (Doesn't work on lollipop devices) See Issue #111 (monster comment https://gitlab.com/fdroid/fdroidclient/issues/111#note_379713) for details of fix. See merge request !41
This commit is contained in:
commit
f0dba2f6e7
@ -55,10 +55,8 @@ import org.fdroid.fdroid.localrepo.LocalRepoService;
|
||||
import org.fdroid.fdroid.net.IconDownloader;
|
||||
import org.fdroid.fdroid.net.WifiStateChangeService;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import java.io.File;
|
||||
import java.security.Security;
|
||||
import java.util.Set;
|
||||
|
||||
public class FDroidApp extends Application {
|
||||
@ -71,6 +69,8 @@ public class FDroidApp extends Application {
|
||||
public static Repo repo = new Repo();
|
||||
public static Set<String> selectedApps = null; // init in SelectLocalAppsFragment
|
||||
|
||||
// Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle.
|
||||
private static org.spongycastle.jce.provider.BouncyCastleProvider spongyCastleProvider;
|
||||
private static Messenger localRepoServiceMessenger = null;
|
||||
private static boolean localRepoServiceIsBound = false;
|
||||
|
||||
@ -78,6 +78,11 @@ public class FDroidApp extends Application {
|
||||
|
||||
BluetoothAdapter bluetoothAdapter = null;
|
||||
|
||||
static {
|
||||
spongyCastleProvider = new org.spongycastle.jce.provider.BouncyCastleProvider();
|
||||
enableSpongyCastle();
|
||||
}
|
||||
|
||||
public static enum Theme {
|
||||
dark, light, lightWithDarkActionBar
|
||||
}
|
||||
@ -108,6 +113,22 @@ public class FDroidApp extends Application {
|
||||
return curTheme;
|
||||
}
|
||||
|
||||
public static void enableSpongyCastle() {
|
||||
Security.addProvider(spongyCastleProvider);
|
||||
}
|
||||
|
||||
public static void enableSpongyCastleOnLollipop() {
|
||||
if (Build.VERSION.SDK_INT == 21) {
|
||||
Security.addProvider(spongyCastleProvider);
|
||||
}
|
||||
}
|
||||
|
||||
public static void disableSpongyCastleOnLollipop() {
|
||||
if (Build.VERSION.SDK_INT == 21) {
|
||||
Security.removeProvider(spongyCastleProvider.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
@ -11,6 +11,7 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.AppFilter;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.io.File;
|
||||
@ -263,25 +264,32 @@ public class App extends ValueObject implements Comparable<App> {
|
||||
throw new CertificateEncodingException("null signed entry!");
|
||||
}
|
||||
|
||||
InputStream tmpIn = apkJar.getInputStream(aSignedEntry);
|
||||
byte[] buff = new byte[2048];
|
||||
while (tmpIn.read(buff, 0, buff.length) != -1) {
|
||||
/*
|
||||
* NOP - apparently have to READ from the JarEntry before you can
|
||||
* call getCerficates() and have it return != null. Yay Java.
|
||||
*/
|
||||
// Due to a bug in android 5.0 lollipop, the inclusion of BouncyCastle causes
|
||||
// breakage when verifying the signature of most .jars. For more
|
||||
// details, check out https://gitlab.com/fdroid/fdroidclient/issues/111.
|
||||
try {
|
||||
FDroidApp.disableSpongyCastleOnLollipop();
|
||||
InputStream tmpIn = apkJar.getInputStream(aSignedEntry);
|
||||
byte[] buff = new byte[2048];
|
||||
while (tmpIn.read(buff, 0, buff.length) != -1) {
|
||||
/*
|
||||
* NOP - apparently have to READ from the JarEntry before you can
|
||||
* call getCerficates() and have it return != null. Yay Java.
|
||||
*/
|
||||
}
|
||||
tmpIn.close();
|
||||
|
||||
if (aSignedEntry.getCertificates() == null
|
||||
|| aSignedEntry.getCertificates().length == 0) {
|
||||
apkJar.close();
|
||||
throw new CertificateEncodingException("No Certificates found!");
|
||||
}
|
||||
|
||||
Certificate signer = aSignedEntry.getCertificates()[0];
|
||||
rawCertBytes = signer.getEncoded();
|
||||
} finally {
|
||||
FDroidApp.enableSpongyCastleOnLollipop();
|
||||
}
|
||||
tmpIn.close();
|
||||
|
||||
if (aSignedEntry.getCertificates() == null
|
||||
|| aSignedEntry.getCertificates().length == 0) {
|
||||
apkJar.close();
|
||||
throw new CertificateEncodingException("No Certificates found!");
|
||||
}
|
||||
|
||||
Certificate signer = aSignedEntry.getCertificates()[0];
|
||||
rawCertBytes = signer.getEncoded();
|
||||
|
||||
apkJar.close();
|
||||
|
||||
/*
|
||||
|
@ -34,12 +34,8 @@ import kellinwood.security.zipsigner.ZipSigner;
|
||||
// TODO Address exception handling in a uniform way throughout
|
||||
|
||||
public class LocalRepoKeyStore {
|
||||
private static final String TAG = "KerplappKeyStore";
|
||||
|
||||
static {
|
||||
Security.insertProviderAt(
|
||||
new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
|
||||
}
|
||||
private static final String TAG = "org.fdroid.fdroid.localrepo.LocalRepoKeyStore";
|
||||
|
||||
public static final String INDEX_CERT_ALIAS = "fdroid";
|
||||
public static final String HTTP_CERT_ALIAS = "https";
|
||||
@ -55,26 +51,49 @@ public class LocalRepoKeyStore {
|
||||
private KeyManager[] keyManagers;
|
||||
private File keyStoreFile;
|
||||
|
||||
public static LocalRepoKeyStore get(Context context) {
|
||||
public static LocalRepoKeyStore get(Context context) throws InitException {
|
||||
if (localRepoKeyStore == null)
|
||||
localRepoKeyStore = new LocalRepoKeyStore(context);
|
||||
return localRepoKeyStore;
|
||||
}
|
||||
|
||||
private LocalRepoKeyStore(Context context) {
|
||||
public static class InitException extends Exception {
|
||||
public InitException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private LocalRepoKeyStore(Context context) throws InitException {
|
||||
try {
|
||||
Log.d(TAG, "generating LocalRepoKeyStore instance");
|
||||
File appKeyStoreDir = context.getDir("keystore", Context.MODE_PRIVATE);
|
||||
|
||||
Log.d(TAG, "Generating LocalRepoKeyStore instance: " + appKeyStoreDir.getAbsolutePath());
|
||||
this.keyStoreFile = new File(appKeyStoreDir, "kerplapp.bks");
|
||||
|
||||
Log.d(TAG, "Using default KeyStore type: " + KeyStore.getDefaultType());
|
||||
this.keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
|
||||
// If there isn't a persisted BKS keystore on disk we need to
|
||||
// create a new empty keystore
|
||||
if (keyStoreFile.exists()) {
|
||||
try {
|
||||
Log.d(TAG, "Keystore already exists, loading...");
|
||||
keyStore.load(new FileInputStream(keyStoreFile), "".toCharArray());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error while loading existing keystore. Will delete and create a new one.");
|
||||
|
||||
// NOTE: Could opt to delete and then re-create the keystore here, but that may
|
||||
// be undesirable. For example - if you were to re-connect to an existing device
|
||||
// that you have swapped apps with in the past, then you would really want the
|
||||
// signature to be the same as last time.
|
||||
throw new InitException("Could not initialize local repo keystore: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyStoreFile.exists()) {
|
||||
// If there isn't a persisted BKS keystore on disk we need to
|
||||
// create a new empty keystore
|
||||
// Init a new keystore with a blank passphrase
|
||||
Log.d(TAG, "Keystore doesn't exist, creating...");
|
||||
keyStore.load(null, "".toCharArray());
|
||||
} else {
|
||||
keyStore.load(new FileInputStream(keyStoreFile), "".toCharArray());
|
||||
}
|
||||
|
||||
/*
|
||||
@ -119,17 +138,23 @@ public class LocalRepoKeyStore {
|
||||
wrappedKeyManager
|
||||
};
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error loading keystore: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error loading keystore: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error loading keystore: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error loading keystore: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (OperatorCreationException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error loading keystore: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error loading keystore: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +175,8 @@ public class LocalRepoKeyStore {
|
||||
FDroidApp.ipAddressString);
|
||||
addToStore(HTTP_CERT_ALIAS, kerplappKeypair, indexCert);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Failed to setup HTTPS certificate: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,21 +206,29 @@ public class LocalRepoKeyStore {
|
||||
zipSigner.signZip(input.getAbsolutePath(), output.getAbsolutePath());
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (InstantiationException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to sign local repo index: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,11 +257,14 @@ public class LocalRepoKeyStore {
|
||||
if (key instanceof PrivateKey)
|
||||
return keyStore.getCertificate(INDEX_CERT_ALIAS);
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to get certificate for local repo: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to get certificate for local repo: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Unable to get certificate for local repo: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -185,7 +185,8 @@ public class LocalRepoManager {
|
||||
symlinkIndexPageElsewhere("../../", repoCAPS);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error writing local repo index: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,13 +250,16 @@ public class LocalRepoManager {
|
||||
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA);
|
||||
app.icon = getIconFile(packageName, packageInfo.versionCode).getName();
|
||||
} catch (NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error adding app to local repo: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
return;
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error adding app to local repo: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error adding app to local repo: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "apps.put: " + packageName);
|
||||
@ -317,7 +321,7 @@ public class LocalRepoManager {
|
||||
|
||||
// TODO this needs to be ported to < android-8
|
||||
@TargetApi(8)
|
||||
private void writeIndexXML() throws TransformerException, ParserConfigurationException {
|
||||
private void writeIndexXML() throws TransformerException, ParserConfigurationException, LocalRepoKeyStore.InitException {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
|
||||
@ -480,12 +484,11 @@ public class LocalRepoManager {
|
||||
writeIndexXML();
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedOutputStream bo = new BufferedOutputStream(
|
||||
new FileOutputStream(xmlIndexJarUnsigned));
|
||||
BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
|
||||
JarOutputStream jo = new JarOutputStream(bo);
|
||||
|
||||
BufferedInputStream bi = new BufferedInputStream(new FileInputStream(xmlIndex));
|
||||
@ -504,8 +507,13 @@ public class LocalRepoManager {
|
||||
jo.close();
|
||||
bo.close();
|
||||
|
||||
LocalRepoKeyStore.get(context).signZip(xmlIndexJarUnsigned, xmlIndexJar);
|
||||
try {
|
||||
LocalRepoKeyStore.get(context).signZip(xmlIndexJarUnsigned, xmlIndexJar);
|
||||
} catch (LocalRepoKeyStore.InitException e) {
|
||||
throw new IOException("Could not sign index - keystore failed to initialize");
|
||||
} finally {
|
||||
xmlIndexJarUnsigned.delete();
|
||||
}
|
||||
|
||||
xmlIndexJarUnsigned.delete();
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.Preferences.ChangeListener;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.net.LocalHTTPD;
|
||||
import org.fdroid.fdroid.net.WifiStateChangeService;
|
||||
import org.fdroid.fdroid.views.swap.SwapActivity;
|
||||
@ -226,7 +227,8 @@ public class LocalRepoService extends Service {
|
||||
Log.w(TAG, "port " + prev + " occupied, trying on " + FDroidApp.port + "!");
|
||||
startService(new Intent(LocalRepoService.this, WifiStateChangeService.class));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Could not start local repo HTTP server: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
Looper.loop(); // start the message receiving loop
|
||||
}
|
||||
@ -279,7 +281,8 @@ public class LocalRepoService extends Service {
|
||||
jmdns = JmDNS.create();
|
||||
jmdns.registerService(pairService);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error while registering jmdns service: " + e);
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
@ -300,11 +303,7 @@ public class LocalRepoService extends Service {
|
||||
pairService = null;
|
||||
}
|
||||
jmdns.unregisterAllServices();
|
||||
try {
|
||||
jmdns.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Utils.closeQuietly(jmdns);
|
||||
jmdns = null;
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +92,11 @@ public class LocalHTTPD extends NanoHTTPD {
|
||||
localRepoKeyStore.getKeyManagers());
|
||||
makeSecure(factory);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Could not enable HTTPS: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
} catch (LocalRepoKeyStore.InitException e) {
|
||||
Log.e(TAG, "Could not enable HTTPS: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,9 +82,6 @@ public class WifiStateChangeService extends Service {
|
||||
return null;
|
||||
|
||||
Context context = WifiStateChangeService.this.getApplicationContext();
|
||||
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
|
||||
Certificate localCert = localRepoKeyStore.getCertificate();
|
||||
FDroidApp.repo.fingerprint = Utils.calcFingerprint(localCert);
|
||||
LocalRepoManager lrm = LocalRepoManager.get(context);
|
||||
lrm.setUriString(FDroidApp.repo.address);
|
||||
lrm.writeIndexPage(Utils.getSharingUri(context, FDroidApp.repo).toString());
|
||||
@ -92,17 +89,28 @@ public class WifiStateChangeService extends Service {
|
||||
if (isCancelled())
|
||||
return null;
|
||||
|
||||
/*
|
||||
* Once the IP address is known we need to generate a self
|
||||
* signed certificate to use for HTTPS that has a CN field set
|
||||
* to the ipAddressString. This must be run in the background
|
||||
* because if this is the first time the singleton is run, it
|
||||
* can take a while to instantiate.
|
||||
*/
|
||||
if (Preferences.get().isLocalRepoHttpsEnabled())
|
||||
localRepoKeyStore.setupHTTPSCertificate();
|
||||
try {
|
||||
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
|
||||
Certificate localCert = localRepoKeyStore.getCertificate();
|
||||
FDroidApp.repo.fingerprint = Utils.calcFingerprint(localCert);
|
||||
|
||||
/*
|
||||
* Once the IP address is known we need to generate a self
|
||||
* signed certificate to use for HTTPS that has a CN field set
|
||||
* to the ipAddressString. This must be run in the background
|
||||
* because if this is the first time the singleton is run, it
|
||||
* can take a while to instantiate.
|
||||
*/
|
||||
if (Preferences.get().isLocalRepoHttpsEnabled())
|
||||
localRepoKeyStore.setupHTTPSCertificate();
|
||||
|
||||
} catch (LocalRepoKeyStore.InitException e) {
|
||||
Log.e(TAG, "Unable to configure a fingerprint or HTTPS for the local repo: " + e.getMessage());
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
@ -123,7 +124,17 @@ public class SignedRepoUpdater extends RepoUpdater {
|
||||
// Don't worry about checking the status code for 200. If it was a
|
||||
// successful download, then we will have a file ready to use:
|
||||
if (indexJar != null && indexJar.exists()) {
|
||||
indexXml = extractIndexFromJar(indexJar);
|
||||
|
||||
// Due to a bug in android 5.0 lollipop, the inclusion of BouncyCastle causes
|
||||
// breakage when verifying the signature of the downloaded .jar. For more
|
||||
// details, check out https://gitlab.com/fdroid/fdroidclient/issues/111.
|
||||
try {
|
||||
FDroidApp.disableSpongyCastleOnLollipop();
|
||||
indexXml = extractIndexFromJar(indexJar);
|
||||
} finally {
|
||||
FDroidApp.enableSpongyCastleOnLollipop();
|
||||
}
|
||||
|
||||
}
|
||||
return indexXml;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user