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:
Peter Serwylo 2014-12-03 10:37:12 +00:00
commit f0dba2f6e7
8 changed files with 179 additions and 83 deletions

View File

@ -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();

View File

@ -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();
/*

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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;
}