package org.fdroid.fdroid.localrepo; import android.content.Context; import android.util.Log; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; import org.spongycastle.asn1.ASN1Sequence; import org.spongycastle.asn1.x500.X500Name; import org.spongycastle.asn1.x509.GeneralName; import org.spongycastle.asn1.x509.GeneralNames; import org.spongycastle.asn1.x509.SubjectPublicKeyInfo; import org.spongycastle.asn1.x509.Time; import org.spongycastle.asn1.x509.X509Extension; import org.spongycastle.cert.X509CertificateHolder; import org.spongycastle.cert.X509v3CertificateBuilder; import org.spongycastle.cert.jcajce.JcaX509CertificateConverter; import org.spongycastle.operator.ContentSigner; import org.spongycastle.operator.OperatorCreationException; import org.spongycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.Socket; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.X509KeyManager; import kellinwood.security.zipsigner.ZipSigner; // TODO Address exception handling in a uniform way throughout public final class LocalRepoKeyStore { private static final String TAG = "LocalRepoKeyStore"; private static final String INDEX_CERT_ALIAS = "fdroid"; private static final String HTTP_CERT_ALIAS = "https"; private static final String DEFAULT_SIG_ALG = "SHA1withRSA"; private static final String DEFAULT_KEY_ALGO = "RSA"; private static final int DEFAULT_KEY_BITS = 2048; private static final String DEFAULT_INDEX_CERT_INFO = "O=Kerplapp,OU=GuardianProject"; private static LocalRepoKeyStore localRepoKeyStore; private KeyStore keyStore; private KeyManager[] keyManagers; private File keyStoreFile; public static LocalRepoKeyStore get(Context context) throws InitException { if (localRepoKeyStore == null) { localRepoKeyStore = new LocalRepoKeyStore(context); } return localRepoKeyStore; } @SuppressWarnings("serial") public static class InitException extends Exception { public InitException(String detailMessage) { super(detailMessage); } } private LocalRepoKeyStore(Context context) throws InitException { try { File appKeyStoreDir = context.getDir("keystore", Context.MODE_PRIVATE); Utils.debugLog(TAG, "Generating LocalRepoKeyStore instance: " + appKeyStoreDir.getAbsolutePath()); this.keyStoreFile = new File(appKeyStoreDir, "kerplapp.bks"); Utils.debugLog(TAG, "Using default KeyStore type: " + KeyStore.getDefaultType()); this.keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); if (keyStoreFile.exists()) { InputStream in = null; try { Utils.debugLog(TAG, "Keystore already exists, loading..."); in = new FileInputStream(keyStoreFile); keyStore.load(in, "".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); } finally { Utils.closeQuietly(in); } } 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 Utils.debugLog(TAG, "Keystore doesn't exist, creating..."); keyStore.load(null, "".toCharArray()); } /* * If the keystore we loaded doesn't have an INDEX_CERT_ALIAS entry * we need to generate a new random keypair and a self signed * certificate for this slot. */ if (keyStore.getKey(INDEX_CERT_ALIAS, "".toCharArray()) == null) { /* * Generate a random key pair to associate with the * INDEX_CERT_ALIAS certificate in the keystore. This keypair * will be used for the HTTPS cert as well. */ KeyPair rndKeys = generateRandomKeypair(); /* * Generate a self signed certificate for signing the index.jar * We can't generate the HTTPS certificate until we know what * the IP address will be to use for the CN field. */ X500Name subject = new X500Name(DEFAULT_INDEX_CERT_INFO); Certificate indexCert = generateSelfSignedCertChain(rndKeys, subject); addToStore(INDEX_CERT_ALIAS, rndKeys, indexCert); } /* * Kerplapp uses its own KeyManager to to ensure the correct * keystore alias is used for the correct purpose. With the default * key manager it is not possible to specify that HTTP_CERT_ALIAS * should be used for TLS and INDEX_CERT_ALIAS for signing the * index.jar. */ KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, "".toCharArray()); KeyManager defaultKeyManager = keyManagerFactory.getKeyManagers()[0]; KeyManager wrappedKeyManager = new KerplappKeyManager( (X509KeyManager) defaultKeyManager); keyManagers = new KeyManager[] { wrappedKeyManager, }; } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException | CertificateException | OperatorCreationException | IOException e) { Log.e(TAG, "Error loading keystore", e); } } public void setupHTTPSCertificate() { try { // Get the existing private/public keypair to use for the HTTPS cert KeyPair kerplappKeypair = getKerplappKeypair(); /* * Once we have an IP address, that can be used as the hostname. We * can generate a self signed cert with a valid CN field to stash * into the keystore in a predictable place. If the IP address * changes we should run this method again to stomp old * HTTPS_CERT_ALIAS entries. */ X500Name subject = new X500Name("CN=" + FDroidApp.ipAddressString); Certificate indexCert = generateSelfSignedCertChain(kerplappKeypair, subject, FDroidApp.ipAddressString); addToStore(HTTP_CERT_ALIAS, kerplappKeypair, indexCert); } catch (Exception e) { Log.e(TAG, "Failed to setup HTTPS certificate", e); } } public File getKeyStoreFile() { return keyStoreFile; } public KeyStore getKeyStore() { return keyStore; } public KeyManager[] getKeyManagers() { return keyManagers; } public void signZip(File input, File output) { try { ZipSigner zipSigner = new ZipSigner(); X509Certificate cert = (X509Certificate) keyStore.getCertificate(INDEX_CERT_ALIAS); KeyPair kp = getKerplappKeypair(); PrivateKey priv = kp.getPrivate(); zipSigner.setKeys("kerplapp", cert, priv, DEFAULT_SIG_ALG, null); zipSigner.signZip(input.getAbsolutePath(), output.getAbsolutePath()); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | GeneralSecurityException | IOException e) { Log.e(TAG, "Unable to sign local repo index", e); } } private KeyPair getKerplappKeypair() throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { /* * You can't store a keypair without an associated certificate chain so, * we'll use the INDEX_CERT_ALIAS as the de-facto keypair/certificate * chain. This cert/key is initialized when the KerplappKeyStore is * constructed for the first time and should *always* be present. */ Key key = keyStore.getKey(INDEX_CERT_ALIAS, "".toCharArray()); if (key instanceof PrivateKey) { Certificate cert = keyStore.getCertificate(INDEX_CERT_ALIAS); PublicKey publicKey = cert.getPublicKey(); return new KeyPair(publicKey, (PrivateKey) key); } return null; } public Certificate getCertificate() { try { Key key = keyStore.getKey(INDEX_CERT_ALIAS, "".toCharArray()); if (key instanceof PrivateKey) { return keyStore.getCertificate(INDEX_CERT_ALIAS); } } catch (GeneralSecurityException e) { Log.e(TAG, "Unable to get certificate for local repo", e); } return null; } private void addToStore(String alias, KeyPair kp, Certificate cert) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException { Certificate[] chain = { cert, }; keyStore.setKeyEntry(alias, kp.getPrivate(), "".toCharArray(), chain); keyStore.store(new FileOutputStream(keyStoreFile), "".toCharArray()); /* * After adding an entry to the keystore we need to create a fresh * KeyManager by reinitializing the KeyManagerFactory with the new key * store content and then rewrapping the default KeyManager with our own */ KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, "".toCharArray()); KeyManager defaultKeyManager = keyManagerFactory.getKeyManagers()[0]; KeyManager wrappedKeyManager = new KerplappKeyManager((X509KeyManager) defaultKeyManager); keyManagers = new KeyManager[] { wrappedKeyManager, }; } private KeyPair generateRandomKeypair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(DEFAULT_KEY_ALGO); keyPairGenerator.initialize(DEFAULT_KEY_BITS); return keyPairGenerator.generateKeyPair(); } private Certificate generateSelfSignedCertChain(KeyPair kp, X500Name subject) throws CertificateException, OperatorCreationException, IOException { return generateSelfSignedCertChain(kp, subject, null); } private Certificate generateSelfSignedCertChain(KeyPair kp, X500Name subject, String hostname) throws CertificateException, OperatorCreationException, IOException { SecureRandom rand = new SecureRandom(); PrivateKey privKey = kp.getPrivate(); PublicKey pubKey = kp.getPublic(); ContentSigner sigGen = new JcaContentSignerBuilder(DEFAULT_SIG_ALG).build(privKey); SubjectPublicKeyInfo subPubKeyInfo = new SubjectPublicKeyInfo( ASN1Sequence.getInstance(pubKey.getEncoded())); Date now = new Date(); // now /* force it to use a English/Gregorian dates for the cert, hardly anyone ever looks at the cert metadata anyway, and its very likely that they understand English/Gregorian dates */ Calendar c = new GregorianCalendar(Locale.ENGLISH); c.setTime(now); c.add(Calendar.YEAR, 1); Time startTime = new Time(now, Locale.ENGLISH); Time endTime = new Time(c.getTime(), Locale.ENGLISH); X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder( subject, BigInteger.valueOf(rand.nextLong()), startTime, endTime, subject, subPubKeyInfo); if (hostname != null) { GeneralNames subjectAltName = new GeneralNames( new GeneralName(GeneralName.iPAddress, hostname)); v3CertGen.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName); } X509CertificateHolder certHolder = v3CertGen.build(sigGen); return new JcaX509CertificateConverter().getCertificate(certHolder); } /* * A X509KeyManager that always returns the KerplappKeyStore.HTTP_CERT_ALIAS * for it's chosen server alias. All other operations are deferred to the * wrapped X509KeyManager. */ private static final class KerplappKeyManager implements X509KeyManager { private final X509KeyManager wrapped; private KerplappKeyManager(X509KeyManager wrapped) { this.wrapped = wrapped; } @Override public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { return wrapped.chooseClientAlias(keyType, issuers, socket); } @Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { /* * Always use the HTTP_CERT_ALIAS for the server alias. */ return LocalRepoKeyStore.HTTP_CERT_ALIAS; } @Override public X509Certificate[] getCertificateChain(String alias) { return wrapped.getCertificateChain(alias); } @Override public String[] getClientAliases(String keyType, Principal[] issuers) { return wrapped.getClientAliases(keyType, issuers); } @Override public PrivateKey getPrivateKey(String alias) { return wrapped.getPrivateKey(alias); } @Override public String[] getServerAliases(String keyType, Principal[] issuers) { return wrapped.getServerAliases(keyType, issuers); } } }