From 220fc35de449e03a11d5d836af0de0909d691e16 Mon Sep 17 00:00:00 2001 From: Arnaud Fontaine Date: Sat, 22 Oct 2016 22:42:22 +0200 Subject: [PATCH 2/2] Support of SmartPGP secure messaging feature --- OpenKeychain/src/main/AndroidManifest.xml | 5 + .../org/sufficientlysecure/keychain/Constants.java | 2 + .../securitytoken/SCP11bSecureMessaging.java | 714 +++++++++++++++++++++ .../keychain/securitytoken/SecureMessaging.java | 17 + .../securitytoken/SecureMessagingException.java | 8 + .../securitytoken/SecurityTokenHelper.java | 67 +- .../ui/SecurityTokenOperationActivity.java | 1 + .../keychain/ui/SettingsActivity.java | 54 ++ .../ui/SettingsSmartPGPAuthoritiesActivity.java | 124 ++++ .../ui/SettingsSmartPGPAuthorityFragment.java | 334 ++++++++++ .../ui/base/BaseSecurityTokenActivity.java | 6 +- .../AddEditSmartPGPAuthorityDialogFragment.java | 313 +++++++++ .../keychain/util/Preferences.java | 5 +- .../res/layout/add_smartpgp_authority_dialog.xml | 45 ++ .../settings_smartpgp_authority_fragment.xml | 7 + .../layout/settings_smartpgp_authority_item.xml | 27 + .../res/layout/smartpgp_authorities_preference.xml | 17 + .../main/res/menu/smartpgp_authority_pref_menu.xml | 10 + OpenKeychain/src/main/res/values/strings.xml | 17 + .../src/main/res/xml/experimental_preferences.xml | 10 + 20 files changed, 1773 insertions(+), 10 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessaging.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessagingException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthoritiesActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthorityFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditSmartPGPAuthorityDialogFragment.java create mode 100644 OpenKeychain/src/main/res/layout/add_smartpgp_authority_dialog.xml create mode 100644 OpenKeychain/src/main/res/layout/settings_smartpgp_authority_fragment.xml create mode 100644 OpenKeychain/src/main/res/layout/settings_smartpgp_authority_item.xml create mode 100644 OpenKeychain/src/main/res/layout/smartpgp_authorities_preference.xml create mode 100644 OpenKeychain/src/main/res/menu/smartpgp_authority_pref_menu.xml diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 0ab1e6a..f22c59f 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -484,6 +484,11 @@ android:label="@string/title_key_server_preference" android:windowSoftInputMode="stateHidden" /> + 0) { + final Cipher cipher = Cipher.getInstance(SCP11_CIPHER_ALGO); + + byte[] iv = new byte[AES_BLOCK_SIZE]; + Arrays.fill(iv, (byte)0); + cipher.init(Cipher.ENCRYPT_MODE, mSEnc, new IvParameterSpec(iv)); + + iv[AES_BLOCK_SIZE - 2] = (byte)((mEncryptionCounter >> 8) & 0xff); + iv[AES_BLOCK_SIZE - 1] = (byte)(mEncryptionCounter & 0xff); + + iv = cipher.doFinal(iv); + + cipher.init(Cipher.ENCRYPT_MODE, mSEnc, new IvParameterSpec(iv)); + + final byte[] pdata = new byte[data.length + AES_BLOCK_SIZE - (data.length % AES_BLOCK_SIZE)]; + System.arraycopy(data, 0, pdata, 0, data.length); + pdata[data.length] = (byte)0x80; + + Arrays.fill(data, (byte)0); + + data = cipher.doFinal(pdata); + + Arrays.fill(pdata, (byte)0); + Arrays.fill(iv, (byte)0); + } + + + final int lcc = data.length + SCP11_MAC_LENGTH; + + final byte[] odata = new byte[4 + 3 + lcc + 3]; + int ooff = 0; + + odata[ooff++] = (byte) (((byte) apdu.getCLA()) | OPENPGP_SECURE_MESSAGING_CLA_MASK); + odata[ooff++] = (byte) apdu.getINS(); + odata[ooff++] = (byte) apdu.getP1(); + odata[ooff++] = (byte) apdu.getP2(); + + if (lcc > 0xff) { + odata[ooff++] = (byte) 0; + odata[ooff++] = (byte) ((lcc >> 8) & 0xff); + } + odata[ooff++] = (byte) (lcc & 0xff); + + System.arraycopy(data, 0, odata, ooff, data.length); + ooff += data.length; + + Arrays.fill(data, (byte)0); + + final Mac mac = Mac.getInstance(SCP11_MAC_ALGO, PROVIDER); + mac.init(mSMac); + mac.update(mMacChaining); + mac.update(odata, 0, ooff); + mMacChaining = mac.doFinal(); + + System.arraycopy(mMacChaining, 0, odata, ooff, SCP11_MAC_LENGTH); + ooff += SCP11_MAC_LENGTH; + + if (lcc > 0xff) { + odata[ooff++] = (byte) 0; + } + odata[ooff++] = (byte) 0; + + apdu = new CommandAPDU(odata, 0, ooff); + + Arrays.fill(odata, (byte)0); + + return apdu; + + } catch (NoSuchAlgorithmException e) { + throw new SecureMessagingException("unavailable algorithm : " + e.getMessage()); + } catch (NoSuchProviderException e) { + throw new SecureMessagingException("unavailable provider : " + e.getMessage()); + } catch (NoSuchPaddingException e) { + throw new SecureMessagingException("unavailable padding algorithm : " + e.getMessage()); + } catch (InvalidKeyException e) { + throw new SecureMessagingException("invalid key : " + e.getMessage()); + } catch (InvalidAlgorithmParameterException e) { + throw new SecureMessagingException("invalid IV : " + e.getMessage()); + } catch (BadPaddingException e) { + throw new SecureMessagingException("invalid IV : " + e.getMessage()); + } catch (IllegalBlockSizeException e) { + throw new SecureMessagingException("invalid block size : " + e.getMessage()); + } + } + + + @Override + public ResponseAPDU verifyAndDecrypt(ResponseAPDU apdu) + throws SecureMessagingException { + + if (!isEstablished()) { + throw new SecureMessagingException("not established"); + } + + byte[] data = apdu.getData(); + + if ((data.length == 0) && + (apdu.getSW() != 0x9000) && + (apdu.getSW1() != 0x62) && + (apdu.getSW1() != 0x63)) { + return apdu; + } + + if (data.length < SCP11_MAC_LENGTH) { + throw new SecureMessagingException("missing or incomplete MAC in response"); + } + + try { + + final Mac mac = Mac.getInstance(SCP11_MAC_ALGO, PROVIDER); + mac.init(mSRMac); + + mac.update(mMacChaining); + if ((data.length - SCP11_MAC_LENGTH) > 0) { + mac.update(data, 0, data.length - SCP11_MAC_LENGTH); + } + mac.update((byte) apdu.getSW1()); + mac.update((byte) apdu.getSW2()); + + final byte[] sig = mac.doFinal(); + + for (int i = 0; i < SCP11_MAC_LENGTH; ++i) { + if ((i >= sig.length) + || (sig[i] != data[data.length - SCP11_MAC_LENGTH + i])) { + throw new SecureMessagingException("corrupted integrity"); + } + } + + if (((data.length - SCP11_MAC_LENGTH) % AES_BLOCK_SIZE) != 0) { + throw new SecureMessagingException("invalid encrypted data size"); + } + + if (data.length > SCP11_MAC_LENGTH) { + final Cipher cipher = Cipher.getInstance(SCP11_CIPHER_ALGO); + + byte[] iv = new byte[AES_BLOCK_SIZE]; + Arrays.fill(iv,(byte)0); + cipher.init(Cipher.ENCRYPT_MODE, mSEnc, new IvParameterSpec(iv)); + + iv[0] = (byte) 0x80; + iv[AES_BLOCK_SIZE - 2] = (byte) ((mEncryptionCounter >> 8) & 0xff); + iv[AES_BLOCK_SIZE - 1] = (byte) (mEncryptionCounter & 0xff); + + iv = cipher.doFinal(iv); + + cipher.init(Cipher.DECRYPT_MODE, mSEnc, new IvParameterSpec(iv)); + data = cipher.doFinal(data, 0, data.length - SCP11_MAC_LENGTH); + + int i = data.length - 1; + while ((0 < i) && (data[i] == (byte) 0)) --i; + + if ((i <= 0) || (data[i] != (byte) 0x80)) { + throw new SecureMessagingException("invalid data padding after decryption"); + } + + final byte[] datasw = new byte[i + 2]; + System.arraycopy(data, 0, datasw, 0, i); + datasw[datasw.length - 2] = (byte) apdu.getSW1(); + datasw[datasw.length - 1] = (byte) apdu.getSW2(); + + Arrays.fill(data, (byte) 0); + + data = datasw; + } else { + data = new byte[2]; + data[0] = (byte) apdu.getSW1(); + data[1] = (byte) apdu.getSW2(); + } + + apdu = new ResponseAPDU(data); + + return apdu; + + } catch (NoSuchAlgorithmException e) { + throw new SecureMessagingException("unavailable algorithm : " + e.getMessage()); + } catch (NoSuchProviderException e) { + throw new SecureMessagingException("unknown provider : " + e.getMessage()); + } catch (NoSuchPaddingException e) { + throw new SecureMessagingException("unavailable padding algorithm : " + e.getMessage()); + } catch (InvalidKeyException e) { + throw new SecureMessagingException("invalid key : " + e.getMessage()); + } catch (BadPaddingException e) { + throw new SecureMessagingException("invalid IV : " + e.getMessage()); + } catch (InvalidAlgorithmParameterException e) { + throw new SecureMessagingException("invalid IV : " + e.getMessage()); + } catch (IllegalBlockSizeException e) { + throw new SecureMessagingException("invalid block size : " + e.getMessage()); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessaging.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessaging.java new file mode 100644 index 0000000..90436a3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessaging.java @@ -0,0 +1,17 @@ +package org.sufficientlysecure.keychain.securitytoken; + +import java.io.IOException; + +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; + +public interface SecureMessaging { + + void clearSession(); + + boolean isEstablished(); + + CommandAPDU encryptAndSign(CommandAPDU apdu) throws SecureMessagingException; + + ResponseAPDU verifyAndDecrypt(ResponseAPDU apdu) throws SecureMessagingException; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessagingException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessagingException.java new file mode 100644 index 0000000..5b8a8e3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecureMessagingException.java @@ -0,0 +1,8 @@ +package org.sufficientlysecure.keychain.securitytoken; + +public final class SecureMessagingException extends Exception { + + public SecureMessagingException(String msg) { + super(msg); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java index c3619db..0fdd183 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.securitytoken; +import android.content.Context; import android.support.annotation.NonNull; import org.bouncycastle.asn1.ASN1Encodable; @@ -75,9 +76,9 @@ public class SecurityTokenHelper { private static final int MAX_APDU_NC_EXT = 65535; private static final int MAX_APDU_NE = 256; - private static final int MAX_APDU_NE_EXT = 65536; + static final int MAX_APDU_NE_EXT = 65536; - private static final int APDU_SW_SUCCESS = 0x9000; + static final int APDU_SW_SUCCESS = 0x9000; private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61; private static final int MASK_CLA_CHAINING = 1 << 4; @@ -92,6 +93,7 @@ public class SecurityTokenHelper { private Transport mTransport; private CardCapabilities mCardCapabilities; private OpenPgpCapabilities mOpenPgpCapabilities; + private SecureMessaging mSecureMessaging; private Passphrase mPin; private Passphrase mAdminPin; @@ -181,7 +183,7 @@ public class SecurityTokenHelper { * * @throws IOException */ - public void connectToDevice() throws IOException { + public void connectToDevice(final Context ctx) throws IOException { // Connect on transport layer mCardCapabilities = new CardCapabilities(); @@ -202,6 +204,16 @@ public class SecurityTokenHelper { mPw1ValidatedForSignature = false; mPw1ValidatedForDecrypt = false; mPw3Validated = false; + + if (mOpenPgpCapabilities.isHasSM()) { + try { + SCP11bSecureMessaging.establish(this, ctx); + } catch(SecureMessagingException e) { + mSecureMessaging = null; + Log.e(Constants.TAG, "failed to establish secure messaging", e); + } + } + } /** @@ -699,7 +711,16 @@ public class SecurityTokenHelper { * @return response from the card * @throws IOException */ - private ResponseAPDU communicate(CommandAPDU apdu) throws IOException { + ResponseAPDU communicate(CommandAPDU apdu) throws IOException { + if ((mSecureMessaging != null) && mSecureMessaging.isEstablished()) { + try { + apdu = mSecureMessaging.encryptAndSign(apdu); + } catch (SecureMessagingException e) { + clearSecureMessaging(); + throw new IOException("secure messaging encrypt/sign failure : " + e. getMessage()); + } + } + ByteArrayOutputStream result = new ByteArrayOutputStream(); ResponseAPDU lastResponse = null; @@ -746,7 +767,18 @@ public class SecurityTokenHelper { result.write(lastResponse.getSW1()); result.write(lastResponse.getSW2()); - return new ResponseAPDU(result.toByteArray()); + lastResponse = new ResponseAPDU(result.toByteArray()); + + if ((mSecureMessaging != null) && mSecureMessaging.isEstablished()) { + try { + lastResponse = mSecureMessaging.verifyAndDecrypt(lastResponse); + } catch (SecureMessagingException e) { + clearSecureMessaging(); + throw new IOException("secure messaging verify/decrypt failure : " + e. getMessage()); + } + } + + return lastResponse; } public Transport getTransport() { @@ -754,6 +786,7 @@ public class SecurityTokenHelper { } public void setTransport(Transport mTransport) { + clearSecureMessaging(); this.mTransport = mTransport; } @@ -833,6 +866,9 @@ public class SecurityTokenHelper { } } + // secure messaging must be disabled before reactivation + clearSecureMessaging(); + // reactivate token! // NOTE: keep the order here! First execute _both_ reactivate commands. Before checking _both_ responses // If a token is in a bad state and reactivate1 fails, it could still be reactivated with reactivate2 @@ -871,13 +907,32 @@ public class SecurityTokenHelper { } public boolean isPersistentConnectionAllowed() { - return mTransport != null && mTransport.isPersistentConnectionAllowed(); + return mTransport != null && + mTransport.isPersistentConnectionAllowed() && + (mSecureMessaging == null || + !mSecureMessaging.isEstablished()); } public boolean isConnected() { return mTransport != null && mTransport.isConnected(); } + public void clearSecureMessaging() { + if(mSecureMessaging != null) { + mSecureMessaging.clearSession(); + } + mSecureMessaging = null; + } + + void setSecureMessaging(final SecureMessaging sm) { + clearSecureMessaging(); + mSecureMessaging = sm; + } + + OpenPgpCapabilities getOpenPgpCapabilities() { + return mOpenPgpCapabilities; + } + private static class LazyHolder { private static final SecurityTokenHelper SECURITY_TOKEN_HELPER = new SecurityTokenHelper(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index 651780f..c663a2f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -298,6 +298,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { // Just close finish(); } else { + mSecurityTokenHelper.clearSecureMessaging(); new AsyncTask() { @Override protected Void doInBackground(Void... params) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java index 894ce78..ddd6d55 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -57,11 +57,18 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.List; +import java.util.Set; public class SettingsActivity extends AppCompatPreferenceActivity { public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005; + public static final int REQUEST_CODE_SMARTPGP_AUTHORITIES_PREF = 0x00007006; private static final int REQUEST_PERMISSION_READ_CONTACTS = 13; private static Preferences sPreferences; @@ -552,6 +559,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity { */ public static class ExperimentalPrefsFragment extends PresetPreferenceFragment { + private PreferenceScreen mSmartPGPAuthoritiesPreference = null; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -561,6 +570,51 @@ public class SettingsActivity extends AppCompatPreferenceActivity { initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); + mSmartPGPAuthoritiesPreference = (PreferenceScreen) findPreference(Constants.Pref.EXPERIMENTAL_SMARTPGP_AUTHORITIES); + + final KeyStore ks = SettingsSmartPGPAuthoritiesActivity.readKeystore(getActivity()); + int size = 0; + try { + if (ks != null) { + size = ks.size(); + } + } catch (KeyStoreException e) {} + + mSmartPGPAuthoritiesPreference.setSummary(getActivity().getResources().getQuantityString(R.plurals.n_authorities, size, size)); + + mSmartPGPAuthoritiesPreference + .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(getActivity(), + SettingsSmartPGPAuthoritiesActivity.class); + startActivityForResult(intent, REQUEST_CODE_SMARTPGP_AUTHORITIES_PREF); + return false; + } + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_SMARTPGP_AUTHORITIES_PREF: { + // update preference, in case it changed + final KeyStore ks = SettingsSmartPGPAuthoritiesActivity.readKeystore(getActivity()); + int size = 0; + try { + if (ks != null) { + size = ks.size(); + } + } catch (KeyStoreException e) {} + + mSmartPGPAuthoritiesPreference.setSummary(getActivity().getResources().getQuantityString(R.plurals.n_authorities, size, size)); + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + break; + } + } } private static void initializeTheme(final ListPreference themePref) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthoritiesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthoritiesActivity.java new file mode 100644 index 0000000..fe11fb6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthoritiesActivity.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2010-2014 Thialfihar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +public class SettingsSmartPGPAuthoritiesActivity extends BaseActivity { + + public static final String EXTRA_SMARTPGP_AUTHORITIES = "smartpgp_authorities"; + + private static final String KEYSTORE_FILE = "smartpgp_authorities.keystore"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + String authorities[] = intent.getStringArrayExtra(EXTRA_SMARTPGP_AUTHORITIES); + loadFragment(savedInstanceState, authorities); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void initLayout() { + setContentView(R.layout.smartpgp_authorities_preference); + } + + private void loadFragment(Bundle savedInstanceState, String[] authorities) { + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + SettingsSmartPGPAuthorityFragment fragment = SettingsSmartPGPAuthorityFragment.newInstance(authorities); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.smartpgp_authorities_settings_fragment_container, fragment) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + + public static final KeyStore readKeystore(final Context ctx) { + try { + final File kf = new File(ctx.getFilesDir(), KEYSTORE_FILE); + final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + + ks.load(null, null); + + if (kf.exists()) { + final FileInputStream fis = new FileInputStream(kf); + ks.load(fis, null); + fis.close(); + } + + return ks; + } catch (Exception e) { + return null; + } + } + + public static final void writeKeystore(final Context ctx, final KeyStore ks) { + try { + final File kf = new File(ctx.getFilesDir(), KEYSTORE_FILE); + + if (kf.exists()) { + kf.delete(); + } + + final FileOutputStream fos = new FileOutputStream(kf); + ks.store(fos, null); + fos.flush(); + fos.close(); + } catch (Exception e) { + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthorityFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthorityFragment.java new file mode 100644 index 0000000..f3dcca7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsSmartPGPAuthorityFragment.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2012-2015 Dominik Schürmann + * Copyright (C) 2015 Adithya Abraham Philip + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.dialog.AddEditSmartPGPAuthorityDialogFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperAdapter; +import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback; +import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + + +public class SettingsSmartPGPAuthorityFragment extends Fragment implements RecyclerItemClickListener.OnItemClickListener { + + private ItemTouchHelper mItemTouchHelper; + + private ArrayList mAuthorities; + private AuthorityListAdapter mAdapter; + + public static SettingsSmartPGPAuthorityFragment newInstance(String[] authorities) { + return new SettingsSmartPGPAuthorityFragment(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + + return inflater.inflate(R.layout.settings_smartpgp_authority_fragment, null); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + List authorities = new LinkedList(); + + try { + final KeyStore ks = SettingsSmartPGPAuthoritiesActivity.readKeystore(getActivity()); + final Enumeration it = ks.aliases(); + + while (it.hasMoreElements()) { + authorities.add(it.nextElement()); + } + + } catch (Exception e) { + } + + mAuthorities = new ArrayList<>(authorities); + mAdapter = new AuthorityListAdapter(mAuthorities); + + RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.smartpgp_authority_recycler_view); + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + + ItemTouchHelper.Callback callback = new ItemTouchHelperDragCallback(mAdapter); + mItemTouchHelper = new ItemTouchHelper(callback); + mItemTouchHelper.attachToRecyclerView(recyclerView); + + // for clicks + recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(getActivity(), this)); + + // can't use item decoration because it doesn't move with drag and drop + // recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); + + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + inflater.inflate(R.menu.smartpgp_authority_pref_menu, menu); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case R.id.menu_add_smartpgp_authority: + startAddAuthorityDialog(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + private void startAddAuthorityDialog() { + startEditAuthorityDialog(AddEditSmartPGPAuthorityDialogFragment.Action.ADD, null, null, -1); + } + + private void startEditAuthorityDialog(AddEditSmartPGPAuthorityDialogFragment.Action action, + final String old_alias, final Uri uri, final int position) { + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + Bundle data = message.getData(); + final String new_alias = data.getString(AddEditSmartPGPAuthorityDialogFragment.OUT_ALIAS); + final int position = data.getInt(AddEditSmartPGPAuthorityDialogFragment.OUT_POSITION); + final String uri = data.getString(AddEditSmartPGPAuthorityDialogFragment.OUT_URI); + + final AddEditSmartPGPAuthorityDialogFragment.Action action = + (AddEditSmartPGPAuthorityDialogFragment.Action) + data.getSerializable(AddEditSmartPGPAuthorityDialogFragment.OUT_ACTION); + + switch(action) { + case ADD: + if (editAuthority(old_alias, new_alias, position, uri)) { + Notify.create(getActivity(), "Authority " + new_alias + " added", + Notify.LENGTH_SHORT, Notify.Style.OK).show(); + } + break; + + case EDIT: + if (editAuthority(old_alias, new_alias, position, uri)){ + Notify.create(getActivity(), "Authority " + old_alias + " modified", + Notify.LENGTH_SHORT, Notify.Style.OK).show(); + } + break; + + case DELETE: + if (deleteAuthority(position)) { + Notify.create(getActivity(), "Authority " + old_alias + " deleted", + Notify.LENGTH_SHORT, Notify.Style.OK).show(); + } + break; + + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + AddEditSmartPGPAuthorityDialogFragment dialogFragment = AddEditSmartPGPAuthorityDialogFragment + .newInstance(messenger, action, old_alias, uri, position); + dialogFragment.show(getFragmentManager(), "addSmartPGPAuthorityDialog"); + } + + + private boolean editAuthority(final String old_alias, final String new_alias, final int position, final String uri) { + try { + final KeyStore ks = SettingsSmartPGPAuthoritiesActivity.readKeystore(getContext()); + + if (ks == null) { + throw new KeyStoreException("no keystore found"); + } + + Certificate old_cert = null; + if (old_alias != null) { + old_cert = ks.getCertificate(old_alias); + ks.deleteEntry(old_alias); + mAuthorities.remove(old_alias); + mAdapter.notifyItemRemoved(position); + } + + Certificate new_cert = null; + if (uri == null) { + new_cert = old_cert; + } else { + final InputStream fis = getContext().getContentResolver().openInputStream(Uri.parse(uri)); + + final CertificateFactory cf = CertificateFactory.getInstance("X.509"); + new_cert = cf.generateCertificate(fis); + if (!(new_cert instanceof X509Certificate)) { + Notify.create(getActivity(), "Invalid certificate", Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + return false; + } + + fis.close(); + } + + if (new_alias == null || new_cert == null) { + Notify.create(getActivity(), "Missing alias or certificate", Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + return false; + } + + final X509Certificate x509cert = (X509Certificate)new_cert; + + x509cert.checkValidity(); + + ks.setCertificateEntry(new_alias, x509cert); + + SettingsSmartPGPAuthoritiesActivity.writeKeystore(getContext(), ks); + + mAuthorities.add(new_alias); + mAdapter.notifyItemInserted(mAuthorities.size() - 1); + + return true; + + } catch (IOException e) { + Notify.create(getActivity(), "failed to open certificate (" + e.getMessage() + ")", Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + } catch (CertificateException e) { + Notify.create(getActivity(), "invalid certificate (" + e.getMessage() + ")", Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + } catch (KeyStoreException e) { + Notify.create(getActivity(), "invalid keystore (" + e.getMessage() + ")", Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + } + + return false; + } + + private boolean deleteAuthority(final int position) { + try { + final KeyStore ks = SettingsSmartPGPAuthoritiesActivity.readKeystore(getContext()); + + if (ks == null) { + return false; + } + + ks.deleteEntry(mAuthorities.get(position)); + + SettingsSmartPGPAuthoritiesActivity.writeKeystore(getContext(), ks); + + mAuthorities.remove(mAuthorities.get(position)); + mAdapter.notifyItemRemoved(position); + + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void onItemClick(View view, int position) { + startEditAuthorityDialog(AddEditSmartPGPAuthorityDialogFragment.Action.EDIT, + mAuthorities.get(position), null, position); + } + + + public class AuthorityListAdapter extends RecyclerView.Adapter + implements ItemTouchHelperAdapter { + + private final List mAuthorities; + + public AuthorityListAdapter(List authorities) { + mAuthorities = authorities; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.settings_smartpgp_authority_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.authorityName.setText(mAuthorities.get(position)); + } + + @Override + public void onItemMove(RecyclerView.ViewHolder source, RecyclerView.ViewHolder target, + int fromPosition, int toPosition) { + Collections.swap(mAuthorities, fromPosition, toPosition); + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public int getItemCount() { + return mAuthorities.size(); + } + + + public class ViewHolder extends RecyclerView.ViewHolder implements + ItemTouchHelperViewHolder { + + public final ViewGroup outerLayout; + public final TextView authorityName; + + public ViewHolder(View itemView) { + super(itemView); + outerLayout = (ViewGroup) itemView.findViewById(R.id.outer_layout); + authorityName = (TextView) itemView.findViewById(R.id.smartpgp_authority_tv); + itemView.setClickable(true); + } + + @Override + public void onItemSelected() { + } + + @Override + public void onItemClear() { + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java index fca1666..5cf8eb0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java @@ -168,7 +168,7 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity @Override protected IOException doInBackground(Void... params) { try { - handleSecurityToken(transport); + handleSecurityToken(transport, BaseSecurityTokenActivity.this); } catch (IOException e) { return e; } @@ -428,13 +428,13 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity } } - protected void handleSecurityToken(Transport transport) throws IOException { + protected void handleSecurityToken(Transport transport, Context ctx) throws IOException { // Don't reconnect if device was already connected if (!(mSecurityTokenHelper.isPersistentConnectionAllowed() && mSecurityTokenHelper.isConnected() && mSecurityTokenHelper.getTransport().equals(transport))) { mSecurityTokenHelper.setTransport(transport); - mSecurityTokenHelper.connectToDevice(); + mSecurityTokenHelper.connectToDevice(ctx); } doSecurityTokenInBackground(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditSmartPGPAuthorityDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditSmartPGPAuthorityDialogFragment.java new file mode 100644 index 0000000..b35279c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditSmartPGPAuthorityDialogFragment.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.EncryptFilesFragment; +import org.sufficientlysecure.keychain.util.FileHelper; +import org.sufficientlysecure.keychain.util.Log; + + +public class AddEditSmartPGPAuthorityDialogFragment extends DialogFragment implements OnEditorActionListener { + private static final String IN_MESSENGER = "in_messenger"; + private static final String IN_ACTION = "in_dialog_action"; + private static final String IN_POSITION = "in_position"; + private static final String IN_ALIAS = "in_authority"; + private static final String IN_URI = "in_uri"; + + public static final String OUT_ACTION = "out_action"; + public static final String OUT_ALIAS = "out_alias"; + public static final String OUT_POSITION = "out_position"; + public static final String OUT_URI = "out_uri"; + + private Messenger mMessenger; + private Action mAction; + private int mPosition; + private Uri mURI; + + private EditText mAuthorityAliasText; + private TextInputLayout mAuthorityAliasTextLayout; + private Button mAuthorityAdd; + + public enum Action { + ADD, + EDIT, + DELETE + } + + public static AddEditSmartPGPAuthorityDialogFragment newInstance(Messenger messenger, + Action action, + String alias, + Uri uri, + int position) { + AddEditSmartPGPAuthorityDialogFragment frag = new AddEditSmartPGPAuthorityDialogFragment(); + Bundle args = new Bundle(); + args.putParcelable(IN_MESSENGER, messenger); + args.putSerializable(IN_ACTION, action); + args.putString(IN_ALIAS, alias); + args.putInt(IN_POSITION, position); + if (uri != null) { + args.putString(IN_URI, uri.toString()); + } + + frag.setArguments(args); + + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + mMessenger = getArguments().getParcelable(IN_MESSENGER); + mAction = (Action) getArguments().getSerializable(IN_ACTION); + mPosition = getArguments().getInt(IN_POSITION); + if (getArguments().getString(IN_URI) == null) + mURI = null; + else + mURI = Uri.parse(getArguments().getString(IN_URI)); + + CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.add_smartpgp_authority_dialog, null); + alert.setView(view); + + mAuthorityAliasText = (EditText) view.findViewById(R.id.smartpgp_authority_alias_edit_text); + mAuthorityAliasTextLayout = (TextInputLayout) view.findViewById(R.id.smartpgp_authority_alias_edit_text_layout); + mAuthorityAdd = (Button) view.findViewById(R.id.smartpgp_authority_filename); + + mAuthorityAdd.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + FileHelper.openDocument(AddEditSmartPGPAuthorityDialogFragment.this, null, "*/*", false, + EncryptFilesFragment.REQUEST_CODE_INPUT); + } + }); + + mAuthorityAliasText.setText(getArguments().getString(IN_ALIAS)); + + switch (mAction) { + case ADD: + alert.setTitle(R.string.add_smartpgp_authority_dialog_title); + break; + case EDIT: + case DELETE: + alert.setTitle(R.string.show_smartpgp_authority_dialog_title); + break; + } + + // we don't want dialog to be dismissed on click for keyserver addition or edit, + // thereby requiring the hack seen below and in onStart + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + // we need to have an empty listener to prevent errors on some devices as mentioned + // at http://stackoverflow.com/q/13746412/3000919 + // actual listener set in onStart for adding keyservers or editing them + dismiss(); + } + }); + + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + + switch (mAction) { + case EDIT: + case DELETE: + alert.setNeutralButton(R.string.label_smartpgp_authority_dialog_delete, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + deleteAuthority(); + } + }); + break; + } + + // Hack to open keyboard. + // This is the only method that I found to work across all Android versions + // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ + // Notes: * onCreateView can't be used because we want to add buttons to the dialog + // * opening in onActivityCreated does not work on Android 4.4 + mAuthorityAliasText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + mAuthorityAliasText.post(new Runnable() { + @Override + public void run() { + InputMethodManager imm = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mAuthorityAliasText, InputMethodManager.SHOW_IMPLICIT); + } + }); + } + }); + mAuthorityAliasText.requestFocus(); + + mAuthorityAliasText.setImeActionLabel(getString(android.R.string.ok), + EditorInfo.IME_ACTION_DONE); + mAuthorityAliasText.setOnEditorActionListener(this); + + return alert.show(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case EncryptFilesFragment.REQUEST_CODE_INPUT: + if (data != null) { + mURI = data.getData(); + } else { + mURI = null; + } + break; + } + } + + @Override + public void onStart() { + super.onStart(); + AlertDialog addKeyserverDialog = (AlertDialog) getDialog(); + if (addKeyserverDialog != null) { + Button positiveButton = addKeyserverDialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mAuthorityAliasTextLayout.setErrorEnabled(false); + + dismiss(); + // return unverified keyserver back to activity + authorityEdited(); + } + }); + } + } + + public void authorityEdited() { + dismiss(); + Bundle data = new Bundle(); + data.putSerializable(OUT_ACTION, mAction); + data.putString(OUT_ALIAS, mAuthorityAliasText.getText().toString()); + data.putInt(OUT_POSITION, mPosition); + if (mURI != null) { + data.putString(OUT_URI, mURI.toString()); + } + + sendMessageToHandler(data); + } + + public void deleteAuthority() { + dismiss(); + Bundle data = new Bundle(); + data.putSerializable(OUT_ACTION, Action.DELETE); + data.putString(OUT_ALIAS, mAuthorityAliasText.getText().toString()); + data.putInt(OUT_POSITION, mPosition); + + sendMessageToHandler(data); + } + + @Override + public void onDismiss(DialogInterface dialog) { + // hide keyboard on dismiss + hideKeyboard(); + + super.onDismiss(dialog); + } + + private void hideKeyboard() { + if (getActivity() == null) { + return; + } + InputMethodManager inputManager = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + + //check if no view has focus: + View v = getActivity().getCurrentFocus(); + if (v == null) + return; + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + /** + * Associate the "done" button on the soft keyboard with the okay button in the view + */ + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (EditorInfo.IME_ACTION_DONE == actionId) { + AlertDialog dialog = ((AlertDialog) getDialog()); + Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + bt.performClick(); + return true; + } + return false; + } + + /** + * Send message back to handler which is initialized in a activity + * + */ + private void sendMessageToHandler(Bundle data) { + Message msg = Message.obtain(); + if (data != null) { + msg.setData(data); + } + + try { + mMessenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index 6a6d8d1..cc6ecfe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -34,7 +34,6 @@ import org.sufficientlysecure.keychain.Constants.Pref; import org.sufficientlysecure.keychain.KeychainApplication; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; -import org.sufficientlysecure.keychain.util.orbot.OrbotStatusReceiver; import java.io.Serializable; import java.net.Proxy; @@ -466,6 +465,10 @@ public class Preferences { return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, false); } + public boolean getExperimentalSmartPGPAuthoritiesEnable() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_SMARTPGP_VERIFY_AUTHORITY, false); + } + public void upgradePreferences(Context context) { if (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0) != Constants.Defaults.PREF_VERSION) { diff --git a/OpenKeychain/src/main/res/layout/add_smartpgp_authority_dialog.xml b/OpenKeychain/src/main/res/layout/add_smartpgp_authority_dialog.xml new file mode 100644 index 0000000..ce5e472 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/add_smartpgp_authority_dialog.xml @@ -0,0 +1,45 @@ + + + + + + + + + +