Merge branch 'master' into backup-api
Conflicts: OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java extern/openpgp-api-lib
This commit is contained in:
@@ -5,11 +5,6 @@
|
||||
android:installLocation="auto">
|
||||
|
||||
<!--
|
||||
General remarks
|
||||
===============
|
||||
- Last APG 1 version was 10900 (1.0.9 beta 00)
|
||||
- Keychain starting with versionCode 20000!
|
||||
|
||||
Association of file types to Keychain
|
||||
=====================================
|
||||
General remarks about file ending conventions:
|
||||
@@ -48,6 +43,11 @@
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
|
||||
<!-- For OTG tokens -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<!-- TemporaryStorageProvider should be writable by OpenKeychain only, thus signature-level permission -->
|
||||
<permission
|
||||
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
|
||||
@@ -81,6 +81,7 @@
|
||||
|
||||
<!-- other group (for free) -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
@@ -94,6 +95,15 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Keychain.Light">
|
||||
<!-- broadcast receiver for Wi-Fi Connection -->
|
||||
<receiver
|
||||
android:name=".receiver.NetworkReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- singleTop for NFC dispatch, see SecurityTokenOperationActivity -->
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
@@ -503,21 +513,6 @@
|
||||
android:theme="@style/Theme.Keychain.Transparent"
|
||||
android:windowSoftInputMode="stateHidden">
|
||||
|
||||
<!-- VIEW with fingerprint scheme:
|
||||
Handle URIs with fingerprints when scanning directly from Barcode Scanner -->
|
||||
<intent-filter android:label="@string/intent_import_key">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<!-- Android's scheme matcher is case-sensitive, so include most likely variations -->
|
||||
<data android:scheme="openpgp4fpr" />
|
||||
<data android:scheme="OPENPGP4FPR" />
|
||||
<data android:scheme="OpenPGP4FPR" />
|
||||
<data android:scheme="OpenPGP4Fpr" />
|
||||
<data android:scheme="OpenPGP4fpr" />
|
||||
</intent-filter>
|
||||
<!-- IMPORT_KEY without mimeType to allow import with extras Bundle -->
|
||||
<intent-filter android:label="@string/intent_import_key">
|
||||
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" />
|
||||
@@ -730,10 +725,35 @@
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.RedirectImportKeysActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/title_activity_redirect_key"
|
||||
android:theme="@style/Theme.Keychain.Transparent">
|
||||
|
||||
<!-- VIEW with fingerprint scheme:
|
||||
Handle URIs with fingerprints when scanning directly from Barcode Scanner -->
|
||||
<intent-filter android:label="@string/intent_import_key">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<!-- Android's scheme matcher is case-sensitive, so include most likely variations -->
|
||||
<data android:scheme="openpgp4fpr" />
|
||||
<data android:scheme="OPENPGP4FPR" />
|
||||
<data android:scheme="OpenPGP4FPR" />
|
||||
<data android:scheme="OpenPGP4Fpr" />
|
||||
<data android:scheme="OpenPGP4fpr" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.LogDisplayActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
@@ -806,7 +826,12 @@
|
||||
android:exported="false"
|
||||
android:label="@string/keyserver_sync_settings_title" />
|
||||
|
||||
<!-- Internal classes of the remote APIs (not exported!) -->
|
||||
<provider
|
||||
android:name=".remote.KeychainExternalProvider"
|
||||
android:authorities="${applicationId}.provider.exported"
|
||||
android:exported="true" />
|
||||
|
||||
<!-- Internal classes of the remote APIs (not exported) -->
|
||||
<activity
|
||||
android:name=".remote.ui.RemoteCreateAccountActivity"
|
||||
android:exported="false"
|
||||
@@ -864,6 +889,24 @@
|
||||
android:configChanges="keyboardHidden|keyboard"
|
||||
android:label="@string/title_backup" />
|
||||
|
||||
<!-- Usb interceptor activity -->
|
||||
<activity
|
||||
android:name=".ui.UsbEventReceiverActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Keychain.Transparent"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity="com.example.taskAffinityUsbEventReceiver"
|
||||
android:process=":UsbEventReceiverActivityProcess"
|
||||
android:exported="false">
|
||||
|
||||
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/usb_device_filter" />
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- DEPRECATED service,
|
||||
using this service may lead to truncated data being returned to the caller -->
|
||||
<service
|
||||
|
||||
+8
-3
@@ -6,15 +6,16 @@
|
||||
|
||||
package org.bouncycastle.openpgp.operator.jcajce;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bouncycastle.jcajce.util.NamedJcaJceHelper;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
|
||||
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
|
||||
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
|
||||
public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactory
|
||||
{
|
||||
private final PublicKeyDataDecryptorFactory mWrappedDecryptor;
|
||||
@@ -59,6 +60,10 @@ public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactor
|
||||
return mSessionKeyCache.get(bi);
|
||||
}
|
||||
|
||||
if (mWrappedDecryptor == null) {
|
||||
throw new IllegalStateException("tried to decrypt without wrapped decryptor, this is a bug!");
|
||||
}
|
||||
|
||||
byte[] sessionData = mWrappedDecryptor.recoverSessionData(keyAlgorithm, secKeyData);
|
||||
mSessionKeyCache.put(bi, sessionData);
|
||||
return sessionData;
|
||||
|
||||
@@ -118,6 +118,7 @@ public final class Constants {
|
||||
// keyserver sync settings
|
||||
public static final String SYNC_CONTACTS = "syncContacts";
|
||||
public static final String SYNC_KEYSERVER = "syncKeyserver";
|
||||
public static final String ENABLE_WIFI_SYNC_ONLY = "enableWifiSyncOnly";
|
||||
// other settings
|
||||
public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm";
|
||||
public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities";
|
||||
|
||||
@@ -100,6 +100,11 @@ public class KeychainApplication extends Application {
|
||||
// Add OpenKeychain account to Android to link contacts with keys and keyserver sync
|
||||
createAccountIfNecessary(this);
|
||||
|
||||
if (Preferences.getKeyserverSyncEnabled(this)) {
|
||||
// will update a keyserver sync if the interval has changed
|
||||
KeyserverSyncAdapterService.enableKeyserverSync(this);
|
||||
}
|
||||
|
||||
// if first time, enable keyserver and contact sync
|
||||
if (Preferences.getPreferences(this).isFirstTime()) {
|
||||
KeyserverSyncAdapterService.enableKeyserverSync(this);
|
||||
|
||||
+16
-9
@@ -21,11 +21,12 @@ package org.sufficientlysecure.keychain.keyimport;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
@@ -33,11 +34,12 @@ import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
|
||||
import org.sufficientlysecure.keychain.util.TlsHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -104,11 +106,10 @@ public class FacebookKeyserver extends Keyserver {
|
||||
String request = String.format(FB_KEY_URL_FORMAT, fbUsername);
|
||||
Log.d(Constants.TAG, "fetching from Facebook with: " + request + " proxy: " + mProxy);
|
||||
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
client.setProxy(mProxy);
|
||||
|
||||
URL url = new URL(request);
|
||||
|
||||
OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, mProxy);
|
||||
|
||||
Response response = client.newCall(new Request.Builder().url(url).build()).execute();
|
||||
|
||||
// contains body both in case of success or failure
|
||||
@@ -126,6 +127,9 @@ public class FacebookKeyserver extends Keyserver {
|
||||
throw new QueryFailedException("Cannot connect to Facebook. "
|
||||
+ "Check your Internet connection!"
|
||||
+ (mProxy == Proxy.NO_PROXY ? "" : " Using proxy " + mProxy));
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
Log.e(Constants.TAG, "Exception in cert pinning", e);
|
||||
throw new QueryFailedException("Exception in cert pinning. ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +194,11 @@ public class FacebookKeyserver extends Keyserver {
|
||||
return uri.getPathSegments().get(0);
|
||||
}
|
||||
|
||||
public static boolean isFacebookHost(Uri uri) {
|
||||
public static boolean isFacebookHost(@Nullable Uri uri) {
|
||||
if (uri == null) {
|
||||
return false;
|
||||
}
|
||||
String host = uri.getHost();
|
||||
return host.equalsIgnoreCase(FB_HOST) || host.equalsIgnoreCase(FB_HOST_WWW);
|
||||
return FB_HOST.equalsIgnoreCase(host) || FB_HOST_WWW.equalsIgnoreCase(host);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-39
@@ -18,16 +18,18 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.keyimport;
|
||||
|
||||
import com.squareup.okhttp.MediaType;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
|
||||
import org.sufficientlysecure.keychain.util.TlsHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -42,7 +44,6 @@ import java.util.Comparator;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -199,43 +200,12 @@ public class HkpKeyserver extends Keyserver {
|
||||
return mSecure ? "https://" : "http://";
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a client with pinned certificate if necessary
|
||||
*
|
||||
* @param url url to be queried by client
|
||||
* @param proxy proxy to be used by client
|
||||
* @return client with a pinned certificate if necessary
|
||||
*/
|
||||
public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException {
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
|
||||
try {
|
||||
TlsHelper.usePinnedCertificateIfAvailable(client, url);
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
Log.w(Constants.TAG, e);
|
||||
}
|
||||
|
||||
// don't follow any redirects
|
||||
client.setFollowRedirects(false);
|
||||
client.setFollowSslRedirects(false);
|
||||
|
||||
if (proxy != null) {
|
||||
client.setProxy(proxy);
|
||||
client.setConnectTimeout(30000, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
client.setProxy(Proxy.NO_PROXY);
|
||||
client.setConnectTimeout(5000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
client.setReadTimeout(45000, TimeUnit.MILLISECONDS);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private String query(String request, @NonNull Proxy proxy) throws QueryFailedException, HttpError {
|
||||
try {
|
||||
URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request);
|
||||
Log.d(Constants.TAG, "hkp keyserver query: " + url + " Proxy: " + proxy);
|
||||
OkHttpClient client = getClient(url, proxy);
|
||||
OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, proxy);
|
||||
Response response = client.newCall(new Request.Builder().url(url).build()).execute();
|
||||
|
||||
String responseBody = response.body().string(); // contains body both in case of success or failure
|
||||
@@ -249,6 +219,9 @@ public class HkpKeyserver extends Keyserver {
|
||||
Log.e(Constants.TAG, "IOException at HkpKeyserver", e);
|
||||
throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" +
|
||||
(proxy == Proxy.NO_PROXY ? "" : " Using proxy " + proxy));
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
Log.e(Constants.TAG, "Exception in pinning certs", e);
|
||||
throw new QueryFailedException("Exception in pinning certs");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +386,7 @@ public class HkpKeyserver extends Keyserver {
|
||||
Log.d(Constants.TAG, "hkp keyserver add: " + url);
|
||||
Log.d(Constants.TAG, "params: " + params);
|
||||
|
||||
|
||||
RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), params);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
@@ -422,7 +396,7 @@ public class HkpKeyserver extends Keyserver {
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
Response response = getClient(url, mProxy).newCall(request).execute();
|
||||
Response response = OkHttpClientFactory.getClientPinnedIfAvailable(url, mProxy).newCall(request).execute();
|
||||
|
||||
Log.d(Constants.TAG, "response code: " + response.code());
|
||||
Log.d(Constants.TAG, "answer: " + response.body().string());
|
||||
@@ -434,6 +408,9 @@ public class HkpKeyserver extends Keyserver {
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "IOException", e);
|
||||
throw new AddKeyException();
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
Log.e(Constants.TAG, "Exception in pinning certs", e);
|
||||
throw new AddKeyException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-38
@@ -2,12 +2,10 @@ package org.sufficientlysecure.keychain.linked;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpRequestBase;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import okhttp3.CertificatePinner;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONException;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
|
||||
@@ -18,12 +16,9 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.thoughtcrime.ssl.pinning.util.PinningHelper;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
@@ -233,46 +228,38 @@ public abstract class LinkedTokenResource extends LinkedResource {
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // HttpRequestBase is deprecated
|
||||
public static String getResponseBody(Context context, HttpRequestBase request)
|
||||
throws IOException, HttpStatusException {
|
||||
return getResponseBody(context, request, null);
|
||||
|
||||
private static CertificatePinner getCertificatePinner(String hostname, String[] pins){
|
||||
CertificatePinner.Builder builder = new CertificatePinner.Builder();
|
||||
for(String pin : pins){
|
||||
builder.add(hostname,pin);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // HttpRequestBase is deprecated
|
||||
public static String getResponseBody(Context context, HttpRequestBase request, String[] pins)
|
||||
throws IOException, HttpStatusException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
request.setHeader("User-Agent", "Open Keychain");
|
||||
public static String getResponseBody(Request request, String... pins)
|
||||
throws IOException, HttpStatusException {
|
||||
|
||||
|
||||
HttpClient httpClient;
|
||||
if (pins == null) {
|
||||
httpClient = new DefaultHttpClient(new BasicHttpParams());
|
||||
Log.d("Connection to: " + request.url().url().getHost(), "");
|
||||
OkHttpClient client;
|
||||
if (pins != null) {
|
||||
client = OkHttpClientFactory.getSimpleClientPinned(getCertificatePinner(request.url().url().getHost(), pins));
|
||||
} else {
|
||||
httpClient = PinningHelper.getPinnedHttpClient(context, pins);
|
||||
client = OkHttpClientFactory.getSimpleClient();
|
||||
}
|
||||
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String reason = response.getStatusLine().getReasonPhrase();
|
||||
Response response = client.newCall(request).execute();
|
||||
|
||||
|
||||
int statusCode = response.code();
|
||||
String reason = response.message();
|
||||
|
||||
if (statusCode != 200) {
|
||||
throw new HttpStatusException(statusCode, reason);
|
||||
}
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
InputStream inputStream = entity.getContent();
|
||||
|
||||
BufferedReader bReader = new BufferedReader(
|
||||
new InputStreamReader(inputStream, "UTF-8"), 8);
|
||||
String line;
|
||||
while ((line = bReader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
return response.body().string();
|
||||
}
|
||||
|
||||
public static class HttpStatusException extends Throwable {
|
||||
|
||||
+6
-4
@@ -6,7 +6,7 @@ import android.net.Uri;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.StringRes;
|
||||
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import okhttp3.Request;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
@@ -32,14 +32,16 @@ public class GenericHttpsResource extends LinkedTokenResource {
|
||||
token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // HttpGet is deprecated
|
||||
@Override
|
||||
protected String fetchResource (Context context, OperationLog log, int indent)
|
||||
throws HttpStatusException, IOException {
|
||||
|
||||
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
|
||||
HttpGet httpGet = new HttpGet(mSubUri);
|
||||
return getResponseBody(context, httpGet);
|
||||
Request request = new Request.Builder()
|
||||
.url(mSubUri.toURL())
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.build();
|
||||
return getResponseBody(request);
|
||||
|
||||
}
|
||||
|
||||
|
||||
+20
-14
@@ -6,7 +6,7 @@ import android.net.Uri;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.StringRes;
|
||||
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import okhttp3.Request;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -47,7 +47,7 @@ public class GithubResource extends LinkedTokenResource {
|
||||
return String.format(context.getResources().getString(R.string.linked_id_github_text), token);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // HttpGet is deprecated
|
||||
|
||||
@Override
|
||||
protected String fetchResource (Context context, OperationLog log, int indent)
|
||||
throws HttpStatusException, IOException, JSONException {
|
||||
@@ -55,8 +55,11 @@ public class GithubResource extends LinkedTokenResource {
|
||||
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
|
||||
indent += 1;
|
||||
|
||||
HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId);
|
||||
String response = getResponseBody(context, httpGet);
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.github.com/gists/" + mGistId)
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.build();
|
||||
String response = getResponseBody(request);
|
||||
|
||||
JSONObject obj = new JSONObject(response);
|
||||
|
||||
@@ -79,7 +82,7 @@ public class GithubResource extends LinkedTokenResource {
|
||||
|
||||
}
|
||||
|
||||
@Deprecated // not used for now, but could be used to pick up earlier posted gist if already present?
|
||||
|
||||
@SuppressWarnings({ "deprecation", "unused" })
|
||||
public static GithubResource searchInGithubStream(
|
||||
Context context, String screenName, String needle, OperationLog log) {
|
||||
@@ -94,12 +97,12 @@ public class GithubResource extends LinkedTokenResource {
|
||||
try {
|
||||
|
||||
JSONArray array; {
|
||||
HttpGet httpGet =
|
||||
new HttpGet("https://api.github.com/users/" + screenName + "/gists");
|
||||
httpGet.setHeader("Content-Type", "application/json");
|
||||
httpGet.setHeader("User-Agent", "OpenKeychain");
|
||||
|
||||
String response = getResponseBody(context, httpGet);
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.github.com/users/" + screenName + "/gists")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.build();
|
||||
String response = getResponseBody(request);
|
||||
array = new JSONArray(response);
|
||||
}
|
||||
|
||||
@@ -116,10 +119,13 @@ public class GithubResource extends LinkedTokenResource {
|
||||
continue;
|
||||
}
|
||||
String id = obj.getString("id");
|
||||
HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id);
|
||||
httpGet.setHeader("User-Agent", "OpenKeychain");
|
||||
|
||||
JSONObject gistObj = new JSONObject(getResponseBody(context, httpGet));
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.github.com/gists/" + id)
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.build();
|
||||
|
||||
JSONObject gistObj = new JSONObject(getResponseBody(request));
|
||||
JSONObject gistFiles = gistObj.getJSONObject("files");
|
||||
Iterator<String> gistIt = gistFiles.keys();
|
||||
if (!gistIt.hasNext()) {
|
||||
|
||||
+33
-25
@@ -9,9 +9,9 @@ import android.util.Log;
|
||||
|
||||
import com.textuality.keybase.lib.JWalk;
|
||||
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -84,18 +84,19 @@ public class TwitterResource extends LinkedTokenResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpGet httpGet =
|
||||
new HttpGet("https://api.twitter.com/1.1/statuses/show.json"
|
||||
+ "?id=" + mTweetId
|
||||
+ "&include_entities=false");
|
||||
|
||||
// construct a normal HTTPS request and include an Authorization
|
||||
// header with the value of Bearer <>
|
||||
httpGet.setHeader("Authorization", "Bearer " + authToken);
|
||||
httpGet.setHeader("Content-Type", "application/json");
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.twitter.com/1.1/statuses/show.json"
|
||||
+ "?id=" + mTweetId
|
||||
+ "&include_entities=false")
|
||||
.addHeader("Authorization", "Bearer " + authToken)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.build();
|
||||
|
||||
try {
|
||||
String response = getResponseBody(context, httpGet, CERT_PINS);
|
||||
String response = getResponseBody(request, CERT_PINS);
|
||||
JSONObject obj = new JSONObject(response);
|
||||
JSONObject user = obj.getJSONObject("user");
|
||||
if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
|
||||
@@ -157,21 +158,20 @@ public class TwitterResource extends LinkedTokenResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpGet httpGet =
|
||||
new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json"
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.twitter.com/1.1/statuses/user_timeline.json"
|
||||
+ "?screen_name=" + screenName
|
||||
+ "&count=15"
|
||||
+ "&include_rts=false"
|
||||
+ "&trim_user=true"
|
||||
+ "&exclude_replies=true");
|
||||
|
||||
// construct a normal HTTPS request and include an Authorization
|
||||
// header with the value of Bearer <>
|
||||
httpGet.setHeader("Authorization", "Bearer " + authToken);
|
||||
httpGet.setHeader("Content-Type", "application/json");
|
||||
+ "&exclude_replies=true")
|
||||
.addHeader("Authorization", "Bearer " + authToken)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.build();
|
||||
|
||||
try {
|
||||
String response = getResponseBody(context, httpGet, CERT_PINS);
|
||||
String response = getResponseBody(request, CERT_PINS);
|
||||
JSONArray array = new JSONArray(response);
|
||||
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
@@ -216,12 +216,20 @@ public class TwitterResource extends LinkedTokenResource {
|
||||
String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
|
||||
+ "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "==";
|
||||
|
||||
RequestBody requestBody = RequestBody.create(
|
||||
MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"),
|
||||
"grant_type=client_credentials");
|
||||
|
||||
// Step 2: Obtain a bearer token
|
||||
HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
|
||||
httpPost.setHeader("Authorization", "Basic " + base64Encoded);
|
||||
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
|
||||
httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
|
||||
JSONObject rawAuthorization = new JSONObject(getResponseBody(context, httpPost, CERT_PINS));
|
||||
Request request = new Request.Builder()
|
||||
.url("https://api.twitter.com/oauth2/token")
|
||||
.addHeader("Authorization", "Basic " + base64Encoded)
|
||||
.addHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
|
||||
.addHeader("User-Agent", "OpenKeychain")
|
||||
.post(requestBody)
|
||||
.build();
|
||||
|
||||
JSONObject rawAuthorization = new JSONObject(getResponseBody(request, CERT_PINS));
|
||||
|
||||
// Applications should verify that the value associated with the
|
||||
// token_type key of the returned object is bearer
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
@@ -144,7 +144,7 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> {
|
||||
|
||||
int certifyOk = 0, certifyError = 0, uploadOk = 0, uploadError = 0;
|
||||
|
||||
NfcSignOperationsBuilder allRequiredInput = new NfcSignOperationsBuilder(
|
||||
SecurityTokenSignOperationsBuilder allRequiredInput = new SecurityTokenSignOperationsBuilder(
|
||||
cryptoInput.getSignatureTime(), masterKeyId, masterKeyId);
|
||||
|
||||
// Work through all requested certifications
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Alex Fong Jie Wen <alexfongg@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
|
||||
|
||||
public class ChangeUnlockOperation extends BaseOperation<ChangeUnlockParcel> {
|
||||
|
||||
public ChangeUnlockOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
|
||||
super(context, providerHelper, progressable);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OperationResult execute(ChangeUnlockParcel unlockParcel, CryptoInputParcel cryptoInput) {
|
||||
OperationResult.OperationLog log = new OperationResult.OperationLog();
|
||||
log.add(OperationResult.LogType.MSG_ED, 0);
|
||||
|
||||
if (unlockParcel == null || unlockParcel.mMasterKeyId == null) {
|
||||
log.add(OperationResult.LogType.MSG_ED_ERROR_NO_PARCEL, 1);
|
||||
return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
// Perform actual modification
|
||||
PgpEditKeyResult modifyResult;
|
||||
{
|
||||
PgpKeyOperation keyOperations =
|
||||
new PgpKeyOperation(new ProgressScaler(mProgressable, 0, 70, 100));
|
||||
|
||||
try {
|
||||
log.add(OperationResult.LogType.MSG_ED_FETCHING, 1,
|
||||
KeyFormattingUtils.convertKeyIdToHex(unlockParcel.mMasterKeyId));
|
||||
|
||||
CanonicalizedSecretKeyRing secRing =
|
||||
mProviderHelper.getCanonicalizedSecretKeyRing(unlockParcel.mMasterKeyId);
|
||||
modifyResult = keyOperations.modifyKeyRingPassphrase(secRing, cryptoInput, unlockParcel);
|
||||
|
||||
if (modifyResult.isPending()) {
|
||||
// obtain original passphrase from user
|
||||
log.add(modifyResult, 1);
|
||||
return new EditKeyResult(log, modifyResult);
|
||||
}
|
||||
} catch (ProviderHelper.NotFoundException e) {
|
||||
log.add(OperationResult.LogType.MSG_ED_ERROR_KEY_NOT_FOUND, 2);
|
||||
return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
}
|
||||
|
||||
log.add(modifyResult, 1);
|
||||
|
||||
if (!modifyResult.success()) {
|
||||
// error is already logged by modification
|
||||
return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
// Cannot cancel from here on out!
|
||||
mProgressable.setPreventCancel();
|
||||
|
||||
// It's a success, so this must be non-null now
|
||||
UncachedKeyRing ring = modifyResult.getRing();
|
||||
|
||||
SaveKeyringResult saveResult = mProviderHelper
|
||||
.saveSecretKeyRing(ring, new ProgressScaler(mProgressable, 70, 95, 100));
|
||||
log.add(saveResult, 1);
|
||||
|
||||
// If the save operation didn't succeed, exit here
|
||||
if (!saveResult.success()) {
|
||||
return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
updateProgress(R.string.progress_done, 100, 100);
|
||||
log.add(OperationResult.LogType.MSG_ED_SUCCESS, 0);
|
||||
return new EditKeyResult(EditKeyResult.RESULT_OK, log, ring.getMasterKeyId());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-2
@@ -119,8 +119,9 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
|
||||
// inform the storage provider about the mime type for this uri
|
||||
if (decryptResult.getDecryptionMetadata() != null) {
|
||||
TemporaryFileProvider.setMimeType(mContext, currentInputUri,
|
||||
decryptResult.getDecryptionMetadata().getMimeType());
|
||||
OpenPgpMetadata meta = decryptResult.getDecryptionMetadata();
|
||||
TemporaryFileProvider.setName(mContext, currentInputUri, meta.getFilename());
|
||||
TemporaryFileProvider.setMimeType(mContext, currentInputUri, meta.getMimeType());
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -43,7 +43,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
@@ -80,7 +80,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> {
|
||||
int total = inputBytes != null ? 1 : inputUris.size(), count = 0;
|
||||
ArrayList<PgpSignEncryptResult> results = new ArrayList<>();
|
||||
|
||||
NfcSignOperationsBuilder pendingInputBuilder = null;
|
||||
SecurityTokenSignOperationsBuilder pendingInputBuilder = null;
|
||||
|
||||
// if signing subkey has not explicitly been set, get first usable subkey capable of signing
|
||||
if (input.getSignatureMasterKeyId() != Constants.key.none
|
||||
@@ -161,7 +161,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> {
|
||||
return new SignEncryptResult(log, requiredInput, results, cryptoInput);
|
||||
}
|
||||
if (pendingInputBuilder == null) {
|
||||
pendingInputBuilder = new NfcSignOperationsBuilder(requiredInput.mSignatureTime,
|
||||
pendingInputBuilder = new SecurityTokenSignOperationsBuilder(requiredInput.mSignatureTime,
|
||||
input.getSignatureMasterKeyId(), input.getSignatureSubKeyId());
|
||||
}
|
||||
pendingInputBuilder.addAll(requiredInput);
|
||||
|
||||
+2
-1
@@ -131,7 +131,8 @@ public class DeleteResult extends InputPendingResult {
|
||||
else if (mFail == 0) {
|
||||
str = activity.getString(R.string.delete_nothing);
|
||||
} else {
|
||||
str = activity.getResources().getQuantityString(R.plurals.delete_fail, mFail);
|
||||
str = activity.getResources().getQuantityString(
|
||||
R.plurals.delete_fail, mFail, mFail);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -539,6 +539,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
// secret key modify
|
||||
MSG_MF (LogLevel.START, R.string.msg_mr),
|
||||
MSG_MF_DIVERT (LogLevel.DEBUG, R.string.msg_mf_divert),
|
||||
MSG_MF_ERROR_ALL_KEYS_STRIPPED (LogLevel.ERROR, R.string.msg_mf_error_all_keys_stripped),
|
||||
MSG_MF_ERROR_DIVERT_NEWSUB (LogLevel.ERROR, R.string.msg_mf_error_divert_newsub),
|
||||
MSG_MF_ERROR_DIVERT_SERIAL (LogLevel.ERROR, R.string.msg_mf_error_divert_serial),
|
||||
MSG_MF_ERROR_ENCODE (LogLevel.ERROR, R.string.msg_mf_error_encode),
|
||||
@@ -552,6 +553,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_MF_ERROR_NOOP (LogLevel.ERROR, R.string.msg_mf_error_noop),
|
||||
MSG_MF_ERROR_NULL_EXPIRY (LogLevel.ERROR, R.string.msg_mf_error_null_expiry),
|
||||
MSG_MF_ERROR_PASSPHRASE_MASTER(LogLevel.ERROR, R.string.msg_mf_error_passphrase_master),
|
||||
MSG_MF_ERROR_PASSPHRASES_UNCHANGED(LogLevel.ERROR, R.string.msg_mf_error_passphrases_unchanged),
|
||||
MSG_MF_ERROR_PAST_EXPIRY(LogLevel.ERROR, R.string.msg_mf_error_past_expiry),
|
||||
MSG_MF_ERROR_PGP (LogLevel.ERROR, R.string.msg_mf_error_pgp),
|
||||
MSG_MF_ERROR_RESTRICTED(LogLevel.ERROR, R.string.msg_mf_error_restricted),
|
||||
@@ -566,8 +568,6 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_MF_ERROR_BAD_SECURITY_TOKEN_SIZE(LogLevel.ERROR, R.string.edit_key_error_bad_security_token_size),
|
||||
MSG_MF_ERROR_BAD_SECURITY_TOKEN_STRIPPED(LogLevel.ERROR, R.string.edit_key_error_bad_security_token_stripped),
|
||||
MSG_MF_MASTER (LogLevel.DEBUG, R.string.msg_mf_master),
|
||||
MSG_MF_NOTATION_PIN (LogLevel.DEBUG, R.string.msg_mf_notation_pin),
|
||||
MSG_MF_NOTATION_EMPTY (LogLevel.DEBUG, R.string.msg_mf_notation_empty),
|
||||
MSG_MF_PASSPHRASE (LogLevel.INFO, R.string.msg_mf_passphrase),
|
||||
MSG_MF_PIN (LogLevel.INFO, R.string.msg_mf_pin),
|
||||
MSG_MF_ADMIN_PIN (LogLevel.INFO, R.string.msg_mf_admin_pin),
|
||||
@@ -730,6 +730,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_PSE_ERROR_PGP (LogLevel.ERROR, R.string.msg_pse_error_pgp),
|
||||
MSG_PSE_ERROR_SIG (LogLevel.ERROR, R.string.msg_pse_error_sig),
|
||||
MSG_PSE_ERROR_UNLOCK (LogLevel.ERROR, R.string.msg_pse_error_unlock),
|
||||
MSG_PSE_ERROR_REVOKED_OR_EXPIRED (LogLevel.ERROR, R.string.msg_pse_error_revoked_or_expired),
|
||||
MSG_PSE_KEY_OK (LogLevel.OK, R.string.msg_pse_key_ok),
|
||||
MSG_PSE_KEY_UNKNOWN (LogLevel.DEBUG, R.string.msg_pse_key_unknown),
|
||||
MSG_PSE_KEY_WARN (LogLevel.WARN, R.string.msg_pse_key_warn),
|
||||
|
||||
@@ -61,6 +61,8 @@ public abstract class KeyRing {
|
||||
|
||||
private static final Pattern USER_ID_PATTERN = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$");
|
||||
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("^.*@.*\\..*$");
|
||||
|
||||
/**
|
||||
* Splits userId string into naming part, email part, and comment part
|
||||
* <p/>
|
||||
@@ -71,25 +73,38 @@ public abstract class KeyRing {
|
||||
if (!TextUtils.isEmpty(userId)) {
|
||||
final Matcher matcher = USER_ID_PATTERN.matcher(userId);
|
||||
if (matcher.matches()) {
|
||||
return new UserId(matcher.group(1), matcher.group(3), matcher.group(2));
|
||||
String name = matcher.group(1).isEmpty() ? null : matcher.group(1);
|
||||
String comment = matcher.group(2);
|
||||
String email = matcher.group(3);
|
||||
if (comment == null && email == null && name != null && EMAIL_PATTERN.matcher(name).matches()) {
|
||||
email = name;
|
||||
name = null;
|
||||
}
|
||||
return new UserId(name, email, comment);
|
||||
}
|
||||
}
|
||||
return new UserId(null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a composed user id. Returns null if name is null!
|
||||
* Returns a composed user id. Returns null if name, email and comment are empty.
|
||||
*/
|
||||
public static String createUserId(UserId userId) {
|
||||
String userIdString = userId.name; // consider name a required value
|
||||
if (userIdString != null && !TextUtils.isEmpty(userId.comment)) {
|
||||
userIdString += " (" + userId.comment + ")";
|
||||
StringBuilder userIdBuilder = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(userId.name)) {
|
||||
userIdBuilder.append(userId.name);
|
||||
}
|
||||
if (userIdString != null && !TextUtils.isEmpty(userId.email)) {
|
||||
userIdString += " <" + userId.email + ">";
|
||||
if (!TextUtils.isEmpty(userId.comment)) {
|
||||
userIdBuilder.append(" (");
|
||||
userIdBuilder.append(userId.comment);
|
||||
userIdBuilder.append(")");
|
||||
}
|
||||
|
||||
return userIdString;
|
||||
if (!TextUtils.isEmpty(userId.email)) {
|
||||
userIdBuilder.append(" <");
|
||||
userIdBuilder.append(userId.email);
|
||||
userIdBuilder.append(">");
|
||||
}
|
||||
return userIdBuilder.length() == 0 ? null : userIdBuilder.toString();
|
||||
}
|
||||
|
||||
public static class UserId implements Serializable {
|
||||
|
||||
+13
-9
@@ -26,6 +26,8 @@ public class OpenPgpDecryptionResultBuilder {
|
||||
// builder
|
||||
private boolean mInsecure = false;
|
||||
private boolean mEncrypted = false;
|
||||
private byte[] sessionKey;
|
||||
private byte[] decryptedSessionKey;
|
||||
|
||||
public void setInsecure(boolean insecure) {
|
||||
this.mInsecure = insecure;
|
||||
@@ -36,24 +38,26 @@ public class OpenPgpDecryptionResultBuilder {
|
||||
}
|
||||
|
||||
public OpenPgpDecryptionResult build() {
|
||||
OpenPgpDecryptionResult result = new OpenPgpDecryptionResult();
|
||||
|
||||
if (mInsecure) {
|
||||
Log.d(Constants.TAG, "RESULT_INSECURE");
|
||||
result.setResult(OpenPgpDecryptionResult.RESULT_INSECURE);
|
||||
return result;
|
||||
return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_INSECURE, sessionKey, decryptedSessionKey);
|
||||
}
|
||||
|
||||
if (mEncrypted) {
|
||||
Log.d(Constants.TAG, "RESULT_ENCRYPTED");
|
||||
result.setResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED);
|
||||
} else {
|
||||
Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED");
|
||||
result.setResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED);
|
||||
return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED, sessionKey, decryptedSessionKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED");
|
||||
return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED);
|
||||
}
|
||||
|
||||
|
||||
public void setSessionKey(byte[] sessionKey, byte[] decryptedSessionKey) {
|
||||
if ((sessionKey == null) != (decryptedSessionKey == null)) {
|
||||
throw new AssertionError("sessionKey must be null iff decryptedSessionKey is null!");
|
||||
}
|
||||
this.sessionKey = sessionKey;
|
||||
this.decryptedSessionKey = decryptedSessionKey;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
@@ -76,7 +76,7 @@ public class PgpCertifyOperation {
|
||||
// get the master subkey (which we certify for)
|
||||
PGPPublicKey publicKey = publicRing.getPublicKey().getPublicKey();
|
||||
|
||||
NfcSignOperationsBuilder requiredInput = new NfcSignOperationsBuilder(creationTimestamp,
|
||||
SecurityTokenSignOperationsBuilder requiredInput = new SecurityTokenSignOperationsBuilder(creationTimestamp,
|
||||
publicKey.getKeyID(), publicKey.getKeyID());
|
||||
|
||||
try {
|
||||
|
||||
+55
-25
@@ -26,9 +26,12 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -60,7 +63,6 @@ import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Constants.key;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.BaseOperation;
|
||||
import org.sufficientlysecure.keychain.util.CharsetVerifier;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
@@ -73,6 +75,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.CharsetVerifier;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -197,6 +200,10 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
PGPEncryptedData encryptedData;
|
||||
InputStream cleartextStream;
|
||||
|
||||
// the cached session key
|
||||
byte[] sessionKey;
|
||||
byte[] decryptedSessionKey;
|
||||
|
||||
int symmetricEncryptionAlgo = 0;
|
||||
|
||||
boolean skippedDisallowedKey = false;
|
||||
@@ -304,6 +311,9 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
|
||||
// if this worked out so far, the data is encrypted
|
||||
decryptionResultBuilder.setEncrypted(true);
|
||||
if (esResult.sessionKey != null && esResult.decryptedSessionKey != null) {
|
||||
decryptionResultBuilder.setSessionKey(esResult.sessionKey, esResult.decryptedSessionKey);
|
||||
}
|
||||
|
||||
if (esResult.insecureEncryptionKey) {
|
||||
log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
|
||||
@@ -545,10 +555,14 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
boolean asymmetricPacketFound = false;
|
||||
boolean symmetricPacketFound = false;
|
||||
boolean anyPacketFound = false;
|
||||
boolean decryptedSessionKeyAvailable = false;
|
||||
|
||||
PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
|
||||
PGPPBEEncryptedData encryptedDataSymmetric = null;
|
||||
CanonicalizedSecretKey decryptionKey = null;
|
||||
CachingDataDecryptorFactory cachedKeyDecryptorFactory = new CachingDataDecryptorFactory(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME, cryptoInput.getCryptoData());
|
||||
;
|
||||
|
||||
Passphrase passphrase = null;
|
||||
|
||||
@@ -569,6 +583,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
log.add(LogType.MSG_DC_ASYM, indent,
|
||||
KeyFormattingUtils.convertKeyIdToHex(subKeyId));
|
||||
|
||||
decryptedSessionKeyAvailable = cachedKeyDecryptorFactory.hasCachedSessionData(encData);
|
||||
if (decryptedSessionKeyAvailable) {
|
||||
asymmetricPacketFound = true;
|
||||
encryptedDataAsymmetric = encData;
|
||||
break;
|
||||
}
|
||||
|
||||
CachedPublicKeyRing cachedPublicKeyRing;
|
||||
try {
|
||||
// get actual keyring object based on master key id
|
||||
@@ -746,34 +767,38 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
currentProgress += 2;
|
||||
updateProgress(R.string.progress_extracting_key, currentProgress, 100);
|
||||
|
||||
try {
|
||||
log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
|
||||
if (!decryptionKey.unlock(passphrase)) {
|
||||
log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
|
||||
CachingDataDecryptorFactory decryptorFactory;
|
||||
if (decryptedSessionKeyAvailable) {
|
||||
decryptorFactory = cachedKeyDecryptorFactory;
|
||||
} else {
|
||||
try {
|
||||
log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
|
||||
if (!decryptionKey.unlock(passphrase)) {
|
||||
log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
|
||||
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
|
||||
}
|
||||
} catch (PgpGeneralException e) {
|
||||
log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
|
||||
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
|
||||
}
|
||||
} catch (PgpGeneralException e) {
|
||||
log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
|
||||
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
|
||||
}
|
||||
|
||||
currentProgress += 2;
|
||||
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
|
||||
currentProgress += 2;
|
||||
updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
|
||||
|
||||
CachingDataDecryptorFactory decryptorFactory
|
||||
= decryptionKey.getCachingDecryptorFactory(cryptoInput);
|
||||
decryptorFactory = decryptionKey.getCachingDecryptorFactory(cryptoInput);
|
||||
|
||||
// special case: if the decryptor does not have a session key cached for this encrypted
|
||||
// data, and can't actually decrypt on its own, return a pending intent
|
||||
if (!decryptorFactory.canDecrypt()
|
||||
&& !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
|
||||
|
||||
log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
|
||||
return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
|
||||
decryptionKey.getRing().getMasterKeyId(),
|
||||
decryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
|
||||
), cryptoInput));
|
||||
// special case: if the decryptor does not have a session key cached for this encrypted
|
||||
// data, and can't actually decrypt on its own, return a pending intent
|
||||
if (!decryptorFactory.canDecrypt()
|
||||
&& !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
|
||||
|
||||
log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
|
||||
return result.with(new DecryptVerifyResult(log,
|
||||
RequiredInputParcel.createSecurityTokenDecryptOperation(
|
||||
decryptionKey.getRing().getMasterKeyId(),
|
||||
decryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
|
||||
), cryptoInput));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -786,8 +811,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
|
||||
result.encryptedData = encryptedDataAsymmetric;
|
||||
|
||||
cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys());
|
||||
|
||||
Map<ByteBuffer, byte[]> cachedSessionKeys = decryptorFactory.getCachedSessionKeys();
|
||||
cryptoInput.addCryptoData(cachedSessionKeys);
|
||||
if (cachedSessionKeys.size() >= 1) {
|
||||
Entry<ByteBuffer, byte[]> entry = cachedSessionKeys.entrySet().iterator().next();
|
||||
result.sessionKey = entry.getKey().array();
|
||||
result.decryptedSessionKey = entry.getValue();
|
||||
}
|
||||
} else {
|
||||
// there wasn't even any useful data
|
||||
if (!anyPacketFound) {
|
||||
|
||||
+98
-84
@@ -18,6 +18,23 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.S2K;
|
||||
import org.bouncycastle.bcpg.sig.Features;
|
||||
@@ -55,15 +72,15 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
|
||||
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcKeyToCardOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenKeyToCardOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -71,22 +88,6 @@ import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import org.sufficientlysecure.keychain.util.Primes;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* This class is the single place where ALL operations that actually modify a PGP public or secret
|
||||
* key take place.
|
||||
@@ -496,10 +497,10 @@ public class PgpKeyOperation {
|
||||
OperationLog log,
|
||||
int indent) {
|
||||
|
||||
NfcSignOperationsBuilder nfcSignOps = new NfcSignOperationsBuilder(
|
||||
SecurityTokenSignOperationsBuilder nfcSignOps = new SecurityTokenSignOperationsBuilder(
|
||||
cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(),
|
||||
masterSecretKey.getKeyID());
|
||||
NfcKeyToCardOperationsBuilder nfcKeyToCardOps = new NfcKeyToCardOperationsBuilder(
|
||||
SecurityTokenKeyToCardOperationsBuilder nfcKeyToCardOps = new SecurityTokenKeyToCardOperationsBuilder(
|
||||
masterSecretKey.getKeyID());
|
||||
|
||||
progress(R.string.progress_modify, 0);
|
||||
@@ -1053,13 +1054,13 @@ public class PgpKeyOperation {
|
||||
}
|
||||
|
||||
// 6. If requested, change passphrase
|
||||
if (saveParcel.mNewUnlock != null) {
|
||||
if (saveParcel.getChangeUnlockParcel() != null) {
|
||||
progress(R.string.progress_modify_passphrase, 90);
|
||||
log.add(LogType.MSG_MF_PASSPHRASE, indent);
|
||||
indent += 1;
|
||||
|
||||
sKR = applyNewUnlock(sKR, masterPublicKey, masterPrivateKey,
|
||||
cryptoInput.getPassphrase(), saveParcel.mNewUnlock, log, indent);
|
||||
sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
|
||||
saveParcel.getChangeUnlockParcel().mNewPassphrase, log, indent);
|
||||
if (sKR == null) {
|
||||
// The error has been logged above, just return a bad state
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
@@ -1192,73 +1193,80 @@ public class PgpKeyOperation {
|
||||
}
|
||||
|
||||
|
||||
private static PGPSecretKeyRing applyNewUnlock(
|
||||
PGPSecretKeyRing sKR,
|
||||
PGPPublicKey masterPublicKey,
|
||||
PGPPrivateKey masterPrivateKey,
|
||||
Passphrase passphrase,
|
||||
ChangeUnlockParcel newUnlock,
|
||||
OperationLog log, int indent) throws PGPException {
|
||||
public PgpEditKeyResult modifyKeyRingPassphrase(CanonicalizedSecretKeyRing wsKR,
|
||||
CryptoInputParcel cryptoInput,
|
||||
ChangeUnlockParcel changeUnlockParcel) {
|
||||
|
||||
if (newUnlock.mNewPassphrase != null) {
|
||||
sKR = applyNewPassphrase(sKR, masterPublicKey, passphrase, newUnlock.mNewPassphrase, log, indent);
|
||||
OperationLog log = new OperationLog();
|
||||
int indent = 0;
|
||||
|
||||
// if there is any old packet with notation data
|
||||
if (hasNotationData(sKR)) {
|
||||
if (changeUnlockParcel.mMasterKeyId == null || changeUnlockParcel.mMasterKeyId != wsKR.getMasterKeyId()) {
|
||||
log.add(LogType.MSG_MF_ERROR_KEYID, indent);
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
log.add(LogType.MSG_MF_NOTATION_EMPTY, indent);
|
||||
log.add(LogType.MSG_MF, indent,
|
||||
KeyFormattingUtils.convertKeyIdToHex(wsKR.getMasterKeyId()));
|
||||
indent += 1;
|
||||
progress(R.string.progress_building_key, 0);
|
||||
|
||||
// add packet with EMPTY notation data (updates old one, but will be stripped later)
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterPrivateKey.getPublicKeyPacket().getAlgorithm(),
|
||||
PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
{ // set subpackets
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
hashedPacketsGen.setExportable(false, false);
|
||||
sGen.setHashedSubpackets(hashedPacketsGen.generate());
|
||||
// We work on bouncycastle object level here
|
||||
PGPSecretKeyRing sKR = wsKR.getRing();
|
||||
PGPSecretKey masterSecretKey = sKR.getSecretKey();
|
||||
PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
|
||||
// Make sure the fingerprint matches
|
||||
if (changeUnlockParcel.mFingerprint == null || !Arrays.equals(changeUnlockParcel.mFingerprint,
|
||||
masterSecretKey.getPublicKey().getFingerprint())) {
|
||||
log.add(LogType.MSG_MF_ERROR_FINGERPRINT, indent);
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
// Find the first unstripped secret key
|
||||
PGPSecretKey nonDummy = firstNonDummySecretKeyID(sKR);
|
||||
if(nonDummy == null) {
|
||||
log.add(OperationResult.LogType.MSG_MF_ERROR_ALL_KEYS_STRIPPED, indent);
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
|
||||
if (!cryptoInput.hasPassphrase()) {
|
||||
log.add(LogType.MSG_MF_REQUIRE_PASSPHRASE, indent);
|
||||
|
||||
return new PgpEditKeyResult(log, RequiredInputParcel.createRequiredSignPassphrase(
|
||||
masterSecretKey.getKeyID(), nonDummy.getKeyID(),
|
||||
cryptoInput.getSignatureTime()), cryptoInput);
|
||||
} else {
|
||||
progress(R.string.progress_modify_passphrase, 50);
|
||||
log.add(LogType.MSG_MF_PASSPHRASE, indent);
|
||||
indent += 1;
|
||||
|
||||
try {
|
||||
sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
|
||||
changeUnlockParcel.mNewPassphrase, log, indent);
|
||||
if (sKR == null) {
|
||||
// The error has been logged above, just return a bad state
|
||||
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
|
||||
}
|
||||
sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey);
|
||||
PGPSignature emptySig = sGen.generateCertification(masterPublicKey);
|
||||
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, emptySig);
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR,
|
||||
PGPSecretKey.replacePublicKey(sKR.getSecretKey(), masterPublicKey));
|
||||
} catch (PGPException e) {
|
||||
throw new UnsupportedOperationException("Failed to build encryptor/decryptor!");
|
||||
}
|
||||
|
||||
return sKR;
|
||||
indent -= 1;
|
||||
progress(R.string.progress_done, 100);
|
||||
log.add(LogType.MSG_MF_SUCCESS, indent);
|
||||
return new PgpEditKeyResult(OperationResult.RESULT_OK, log, new UncachedKeyRing(sKR));
|
||||
}
|
||||
}
|
||||
|
||||
if (newUnlock.mNewPin != null) {
|
||||
sKR = applyNewPassphrase(sKR, masterPublicKey, passphrase, newUnlock.mNewPin, log, indent);
|
||||
private static PGPSecretKey firstNonDummySecretKeyID(PGPSecretKeyRing secRing) {
|
||||
Iterator<PGPSecretKey> secretKeyIterator = secRing.getSecretKeys();
|
||||
|
||||
log.add(LogType.MSG_MF_NOTATION_PIN, indent);
|
||||
|
||||
// add packet with "pin" notation data
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterPrivateKey.getPublicKeyPacket().getAlgorithm(),
|
||||
PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
{ // set subpackets
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
hashedPacketsGen.setExportable(false, false);
|
||||
hashedPacketsGen.setNotationData(false, true, "unlock.pin@sufficientlysecure.org", "1");
|
||||
sGen.setHashedSubpackets(hashedPacketsGen.generate());
|
||||
while(secretKeyIterator.hasNext()) {
|
||||
PGPSecretKey secretKey = secretKeyIterator.next();
|
||||
if(!isDummy(secretKey)){
|
||||
return secretKey;
|
||||
}
|
||||
sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey);
|
||||
PGPSignature emptySig = sGen.generateCertification(masterPublicKey);
|
||||
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, emptySig);
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR,
|
||||
PGPSecretKey.replacePublicKey(sKR.getSecretKey(), masterPublicKey));
|
||||
|
||||
return sKR;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("PIN passphrases not yet implemented!");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** This method returns true iff the provided keyring has a local direct key signature
|
||||
@@ -1293,9 +1301,9 @@ public class PgpKeyOperation {
|
||||
PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, encryptorHashCalc,
|
||||
PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(newPassphrase.getCharArray());
|
||||
boolean keysModified = false;
|
||||
|
||||
// noinspection unchecked
|
||||
for (PGPSecretKey sKey : new IterableIterator<PGPSecretKey>(sKR.getSecretKeys())) {
|
||||
for (PGPSecretKey sKey : new IterableIterator<>(sKR.getSecretKeys())) {
|
||||
log.add(LogType.MSG_MF_PASSPHRASE_KEY, indent,
|
||||
KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID()));
|
||||
|
||||
@@ -1307,8 +1315,8 @@ public class PgpKeyOperation {
|
||||
ok = true;
|
||||
} catch (PGPException e) {
|
||||
|
||||
// if this is the master key, error!
|
||||
if (sKey.getKeyID() == masterPublicKey.getKeyID()) {
|
||||
// if the master key failed && it's not stripped, error!
|
||||
if (sKey.getKeyID() == masterPublicKey.getKeyID() && !isDummy(sKey)) {
|
||||
log.add(LogType.MSG_MF_ERROR_PASSPHRASE_MASTER, indent+1);
|
||||
return null;
|
||||
}
|
||||
@@ -1335,7 +1343,13 @@ public class PgpKeyOperation {
|
||||
}
|
||||
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
|
||||
keysModified = true;
|
||||
}
|
||||
|
||||
if(!keysModified) {
|
||||
// no passphrase was changed
|
||||
log.add(LogType.MSG_MF_ERROR_PASSPHRASES_UNCHANGED, indent+1);
|
||||
return null;
|
||||
}
|
||||
|
||||
return sKR;
|
||||
@@ -1348,7 +1362,7 @@ public class PgpKeyOperation {
|
||||
PGPPublicKey masterPublicKey,
|
||||
int flags, long expiry,
|
||||
CryptoInputParcel cryptoInput,
|
||||
NfcSignOperationsBuilder nfcSignOps,
|
||||
SecurityTokenSignOperationsBuilder nfcSignOps,
|
||||
int indent, OperationLog log)
|
||||
throws PGPException, IOException, SignatureException {
|
||||
|
||||
|
||||
-12
@@ -38,7 +38,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
|
||||
protected Long mSignatureSubKeyId = null;
|
||||
protected int mSignatureHashAlgorithm = PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT;
|
||||
protected long mAdditionalEncryptId = Constants.key.none;
|
||||
protected boolean mFailOnMissingEncryptionKeyIds = false;
|
||||
protected String mCharset;
|
||||
protected boolean mCleartextSignature;
|
||||
protected boolean mDetachedSignature = false;
|
||||
@@ -65,7 +64,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
|
||||
mSignatureSubKeyId = source.readInt() == 1 ? source.readLong() : null;
|
||||
mSignatureHashAlgorithm = source.readInt();
|
||||
mAdditionalEncryptId = source.readLong();
|
||||
mFailOnMissingEncryptionKeyIds = source.readInt() == 1;
|
||||
mCharset = source.readString();
|
||||
mCleartextSignature = source.readInt() == 1;
|
||||
mDetachedSignature = source.readInt() == 1;
|
||||
@@ -96,7 +94,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
|
||||
}
|
||||
dest.writeInt(mSignatureHashAlgorithm);
|
||||
dest.writeLong(mAdditionalEncryptId);
|
||||
dest.writeInt(mFailOnMissingEncryptionKeyIds ? 1 : 0);
|
||||
dest.writeString(mCharset);
|
||||
dest.writeInt(mCleartextSignature ? 1 : 0);
|
||||
dest.writeInt(mDetachedSignature ? 1 : 0);
|
||||
@@ -113,10 +110,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
|
||||
this.mCharset = mCharset;
|
||||
}
|
||||
|
||||
public boolean isFailOnMissingEncryptionKeyIds() {
|
||||
return mFailOnMissingEncryptionKeyIds;
|
||||
}
|
||||
|
||||
public long getAdditionalEncryptId() {
|
||||
return mAdditionalEncryptId;
|
||||
}
|
||||
@@ -207,11 +200,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public PgpSignEncryptInputParcel setFailOnMissingEncryptionKeyIds(boolean failOnMissingEncryptionKeyIds) {
|
||||
mFailOnMissingEncryptionKeyIds = failOnMissingEncryptionKeyIds;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PgpSignEncryptInputParcel setCleartextSignature(boolean cleartextSignature) {
|
||||
this.mCleartextSignature = cleartextSignature;
|
||||
return this;
|
||||
|
||||
+26
-12
@@ -168,10 +168,17 @@ public class PgpSignEncryptOperation extends BaseOperation {
|
||||
try {
|
||||
long signingMasterKeyId = input.getSignatureMasterKeyId();
|
||||
long signingSubKeyId = input.getSignatureSubKeyId();
|
||||
{
|
||||
CanonicalizedSecretKeyRing signingKeyRing =
|
||||
mProviderHelper.getCanonicalizedSecretKeyRing(signingMasterKeyId);
|
||||
signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId());
|
||||
|
||||
CanonicalizedSecretKeyRing signingKeyRing =
|
||||
mProviderHelper.getCanonicalizedSecretKeyRing(signingMasterKeyId);
|
||||
signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId());
|
||||
|
||||
|
||||
// Make sure key is not expired or revoked
|
||||
if (signingKeyRing.isExpired() || signingKeyRing.isRevoked()
|
||||
|| signingKey.isExpired() || signingKey.isRevoked()) {
|
||||
log.add(LogType.MSG_PSE_ERROR_REVOKED_OR_EXPIRED, indent);
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
// Make sure we are allowed to sign here!
|
||||
@@ -281,16 +288,17 @@ public class PgpSignEncryptOperation extends BaseOperation {
|
||||
if (encryptSubKeyIds.isEmpty()) {
|
||||
log.add(LogType.MSG_PSE_KEY_WARN, indent + 1,
|
||||
KeyFormattingUtils.convertKeyIdToHex(id));
|
||||
if (input.isFailOnMissingEncryptionKeyIds()) {
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
// Make sure key is not expired or revoked
|
||||
if (keyRing.isExpired() || keyRing.isRevoked()) {
|
||||
log.add(LogType.MSG_PSE_ERROR_REVOKED_OR_EXPIRED, indent);
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
} catch (ProviderHelper.NotFoundException e) {
|
||||
log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1,
|
||||
KeyFormattingUtils.convertKeyIdToHex(id));
|
||||
if (input.isFailOnMissingEncryptionKeyIds()) {
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,7 +478,13 @@ public class PgpSignEncryptOperation extends BaseOperation {
|
||||
InputStream in = new BufferedInputStream(inputData.getInputStream());
|
||||
|
||||
if (enableCompression) {
|
||||
compressGen = new PGPCompressedDataGenerator(input.getCompressionAlgorithm());
|
||||
// Use preferred compression algo
|
||||
int algo = input.getCompressionAlgorithm();
|
||||
if (algo == PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.USE_DEFAULT) {
|
||||
algo = PgpSecurityConstants.DEFAULT_COMPRESSION_ALGORITHM;
|
||||
}
|
||||
|
||||
compressGen = new PGPCompressedDataGenerator(algo);
|
||||
bcpgOut = new BCPGOutputStream(compressGen.open(out));
|
||||
} else {
|
||||
bcpgOut = new BCPGOutputStream(out);
|
||||
@@ -514,7 +528,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
|
||||
} catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) {
|
||||
// this secret key diverts to a OpenPGP card, throw exception with hash that will be signed
|
||||
log.add(LogType.MSG_PSE_PENDING_NFC, indent);
|
||||
return new PgpSignEncryptResult(log, RequiredInputParcel.createNfcSignOperation(
|
||||
return new PgpSignEncryptResult(log, RequiredInputParcel.createSecurityTokenSignOperation(
|
||||
signingKey.getRing().getMasterKeyId(), signingKey.getKeyId(),
|
||||
e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime()), cryptoInput);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.bouncycastle.bcpg.SignatureSubpacketTags;
|
||||
@@ -42,15 +44,22 @@ import org.bouncycastle.bcpg.UserAttributeSubpacketTags;
|
||||
import org.bouncycastle.bcpg.sig.KeyFlags;
|
||||
import org.bouncycastle.openpgp.PGPKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
|
||||
import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector;
|
||||
import org.bouncycastle.openpgp.PGPUtil;
|
||||
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
@@ -1310,4 +1319,37 @@ public class UncachedKeyRing {
|
||||
|| algorithm == PGPPublicKey.ECDH;
|
||||
}
|
||||
|
||||
// ONLY TO BE USED FOR TESTING!!
|
||||
@VisibleForTesting
|
||||
public static UncachedKeyRing forTestingOnlyAddDummyLocalSignature(
|
||||
UncachedKeyRing uncachedKeyRing, String passphrase) throws Exception {
|
||||
PGPSecretKeyRing sKR = (PGPSecretKeyRing) uncachedKeyRing.mRing;
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
|
||||
PGPPrivateKey masterPrivateKey = sKR.getSecretKey().extractPrivateKey(keyDecryptor);
|
||||
PGPPublicKey masterPublicKey = uncachedKeyRing.mRing.getPublicKey();
|
||||
|
||||
// add packet with "pin" notation data
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterPrivateKey.getPublicKeyPacket().getAlgorithm(),
|
||||
PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
{ // set subpackets
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
hashedPacketsGen.setExportable(false, false);
|
||||
hashedPacketsGen.setNotationData(false, true, "dummynotationdata", "some data");
|
||||
sGen.setHashedSubpackets(hashedPacketsGen.generate());
|
||||
}
|
||||
sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey);
|
||||
PGPSignature emptySig = sGen.generateCertification(masterPublicKey);
|
||||
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, emptySig);
|
||||
sKR = PGPSecretKeyRing.insertSecretKey(sKR,
|
||||
PGPSecretKey.replacePublicKey(sKR.getSecretKey(), masterPublicKey));
|
||||
|
||||
return new UncachedKeyRing(sKR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2014-2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.remote.AccountSettings;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
|
||||
|
||||
public class ApiDataAccessObject {
|
||||
|
||||
private final SimpleContentResolverInterface mQueryInterface;
|
||||
|
||||
public ApiDataAccessObject(Context context) {
|
||||
final ContentResolver contentResolver = context.getContentResolver();
|
||||
mQueryInterface = new SimpleContentResolverInterface() {
|
||||
@Override
|
||||
public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri contentUri, ContentValues values) {
|
||||
return contentResolver.insert(contentUri, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) {
|
||||
return contentResolver.update(contentUri, values, where, selectionArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri contentUri, String where, String[] selectionArgs) {
|
||||
return contentResolver.delete(contentUri, where, selectionArgs);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public ApiDataAccessObject(SimpleContentResolverInterface queryInterface) {
|
||||
mQueryInterface = queryInterface;
|
||||
}
|
||||
|
||||
public ArrayList<String> getRegisteredApiApps() {
|
||||
Cursor cursor = mQueryInterface.query(ApiApps.CONTENT_URI, null, null, null, null);
|
||||
|
||||
ArrayList<String> packageNames = new ArrayList<>();
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
packageNames.add(cursor.getString(packageNameCol));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return packageNames;
|
||||
}
|
||||
|
||||
private ContentValues contentValueForApiApps(AppSettings appSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
|
||||
values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate());
|
||||
return values;
|
||||
}
|
||||
|
||||
private ContentValues contentValueForApiAccounts(AccountSettings accSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeychainContract.ApiAccounts.ACCOUNT_NAME, accSettings.getAccountName());
|
||||
values.put(KeychainContract.ApiAccounts.KEY_ID, accSettings.getKeyId());
|
||||
|
||||
// DEPRECATED and thus hardcoded
|
||||
values.put(KeychainContract.ApiAccounts.COMPRESSION, CompressionAlgorithmTags.ZLIB);
|
||||
values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM,
|
||||
PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT);
|
||||
values.put(KeychainContract.ApiAccounts.HASH_ALORITHM,
|
||||
PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT);
|
||||
return values;
|
||||
}
|
||||
|
||||
public void insertApiApp(AppSettings appSettings) {
|
||||
mQueryInterface.insert(ApiApps.CONTENT_URI,
|
||||
contentValueForApiApps(appSettings));
|
||||
}
|
||||
|
||||
public void insertApiAccount(Uri uri, AccountSettings accSettings) {
|
||||
mQueryInterface.insert(uri, contentValueForApiAccounts(accSettings));
|
||||
}
|
||||
|
||||
public void updateApiAccount(Uri uri, AccountSettings accSettings) {
|
||||
if (mQueryInterface.update(uri, contentValueForApiAccounts(accSettings), null,
|
||||
null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be an uri pointing to an account
|
||||
*/
|
||||
public AppSettings getApiAppSettings(Uri uri) {
|
||||
AppSettings settings = null;
|
||||
|
||||
Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
settings = new AppSettings();
|
||||
settings.setPackageName(cursor.getString(
|
||||
cursor.getColumnIndex(ApiApps.PACKAGE_NAME)));
|
||||
settings.setPackageCertificate(cursor.getBlob(
|
||||
cursor.getColumnIndex(ApiApps.PACKAGE_CERTIFICATE)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public AccountSettings getApiAccountSettings(Uri accountUri) {
|
||||
AccountSettings settings = null;
|
||||
|
||||
Cursor cursor = mQueryInterface.query(accountUri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
settings = new AccountSettings();
|
||||
|
||||
settings.setAccountName(cursor.getString(
|
||||
cursor.getColumnIndex(KeychainContract.ApiAccounts.ACCOUNT_NAME)));
|
||||
settings.setKeyId(cursor.getLong(
|
||||
cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public Set<Long> getAllKeyIdsForApp(Uri uri) {
|
||||
Set<Long> keyIds = new HashSet<>();
|
||||
|
||||
Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID);
|
||||
while (cursor.moveToNext()) {
|
||||
keyIds.add(cursor.getLong(keyIdColumn));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public HashSet<Long> getAllowedKeyIdsForApp(Uri uri) {
|
||||
HashSet<Long> keyIds = new HashSet<>();
|
||||
|
||||
Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int keyIdColumn = cursor.getColumnIndex(ApiAllowedKeys.KEY_ID);
|
||||
while (cursor.moveToNext()) {
|
||||
keyIds.add(cursor.getLong(keyIdColumn));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public void saveAllowedKeyIdsForApp(Uri uri, Set<Long> allowedKeyIds)
|
||||
throws RemoteException, OperationApplicationException {
|
||||
// wipe whole table of allowed keys for this account
|
||||
mQueryInterface.delete(uri, null, null);
|
||||
|
||||
// re-insert allowed key ids
|
||||
for (Long keyId : allowedKeyIds) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiAllowedKeys.KEY_ID, keyId);
|
||||
mQueryInterface.insert(uri, values);
|
||||
}
|
||||
}
|
||||
|
||||
public void addAllowedKeyIdForApp(Uri uri, long allowedKeyId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiAllowedKeys.KEY_ID, allowedKeyId);
|
||||
mQueryInterface.insert(uri, values);
|
||||
}
|
||||
|
||||
public byte[] getApiAppCertificate(String packageName) {
|
||||
Uri queryUri = ApiApps.buildByPackageNameUri(packageName);
|
||||
|
||||
String[] projection = new String[]{ApiApps.PACKAGE_CERTIFICATE};
|
||||
|
||||
Cursor cursor = mQueryInterface.query(queryUri, projection, null, null, null);
|
||||
try {
|
||||
byte[] signature = null;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int signatureCol = 0;
|
||||
|
||||
signature = cursor.getBlob(signatureCol);
|
||||
}
|
||||
return signature;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -60,6 +60,9 @@ public class KeychainContract {
|
||||
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
|
||||
String TYPE = "type"; // not a database id
|
||||
String USER_ID = "user_id"; // not a database id
|
||||
String NAME = "name";
|
||||
String EMAIL = "email";
|
||||
String COMMENT = "comment";
|
||||
String ATTRIBUTE_DATA = "attribute_data"; // not a database id
|
||||
String RANK = "rank"; // ONLY used for sorting! no key, no nothing!
|
||||
String IS_PRIMARY = "is_primary";
|
||||
@@ -359,6 +362,9 @@ public class KeychainContract {
|
||||
|
||||
public static class Certs implements CertsColumns, BaseColumns {
|
||||
public static final String USER_ID = UserPacketsColumns.USER_ID;
|
||||
public static final String NAME = UserPacketsColumns.NAME;
|
||||
public static final String EMAIL = UserPacketsColumns.EMAIL;
|
||||
public static final String COMMENT = UserPacketsColumns.COMMENT;
|
||||
public static final String SIGNER_UID = "signer_user_id";
|
||||
|
||||
public static final int UNVERIFIED = 0;
|
||||
|
||||
+21
-10
@@ -54,7 +54,7 @@ import java.io.IOException;
|
||||
*/
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "openkeychain.db";
|
||||
private static final int DATABASE_VERSION = 14;
|
||||
private static final int DATABASE_VERSION = 17;
|
||||
static Boolean apgHack = false;
|
||||
private Context mContext;
|
||||
|
||||
@@ -115,6 +115,9 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
+ UserPacketsColumns.MASTER_KEY_ID + " INTEGER, "
|
||||
+ UserPacketsColumns.TYPE + " INT, "
|
||||
+ UserPacketsColumns.USER_ID + " TEXT, "
|
||||
+ UserPacketsColumns.NAME + " TEXT, "
|
||||
+ UserPacketsColumns.EMAIL + " TEXT, "
|
||||
+ UserPacketsColumns.COMMENT + " TEXT, "
|
||||
+ UserPacketsColumns.ATTRIBUTE_DATA + " BLOB, "
|
||||
|
||||
+ UserPacketsColumns.IS_PRIMARY + " INTEGER, "
|
||||
@@ -276,37 +279,45 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE user_ids ADD COLUMN type INTEGER");
|
||||
db.execSQL("ALTER TABLE user_ids ADD COLUMN attribute_data BLOB");
|
||||
case 7:
|
||||
// consolidate
|
||||
case 8:
|
||||
// new table for allowed key ids in API
|
||||
try {
|
||||
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
||||
} catch (Exception e) {
|
||||
// never mind, the column probably already existed
|
||||
}
|
||||
case 9:
|
||||
case 8:
|
||||
// tbale name for user_ids changed to user_packets
|
||||
db.execSQL("DROP TABLE IF EXISTS certs");
|
||||
db.execSQL("DROP TABLE IF EXISTS user_ids");
|
||||
db.execSQL(CREATE_USER_PACKETS);
|
||||
db.execSQL(CREATE_CERTS);
|
||||
case 10:
|
||||
case 9:
|
||||
// do nothing here, just consolidate
|
||||
case 11:
|
||||
case 10:
|
||||
// fix problems in database, see #1402 for details
|
||||
// https://github.com/open-keychain/open-keychain/issues/1402
|
||||
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
|
||||
case 12:
|
||||
case 11:
|
||||
db.execSQL(CREATE_UPDATE_KEYS);
|
||||
case 13:
|
||||
case 12:
|
||||
// do nothing here, just consolidate
|
||||
case 14:
|
||||
case 13:
|
||||
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
|
||||
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
||||
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
|
||||
db.execSQL("CREATE INDEX verified_certs ON certs ("
|
||||
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
|
||||
|
||||
case 14:
|
||||
db.execSQL("ALTER TABLE user_packets ADD COLUMN name TEXT");
|
||||
db.execSQL("ALTER TABLE user_packets ADD COLUMN email TEXT");
|
||||
db.execSQL("ALTER TABLE user_packets ADD COLUMN comment TEXT");
|
||||
case 15:
|
||||
db.execSQL("CREATE INDEX uids_by_name ON user_packets (name COLLATE NOCASE)");
|
||||
db.execSQL("CREATE INDEX uids_by_email ON user_packets (email COLLATE NOCASE)");
|
||||
if (oldVersion == 14) {
|
||||
// no consolidate necessary
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// always do consolidate after upgrade
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
|
||||
public class KeychainExternalContract {
|
||||
|
||||
// this is in KeychainExternalContract already, but we want to be double
|
||||
// sure this isn't mixed up with the internal one!
|
||||
public static final String CONTENT_AUTHORITY_EXTERNAL = Constants.PROVIDER_AUTHORITY + ".exported";
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_EXTERNAL = Uri
|
||||
.parse("content://" + CONTENT_AUTHORITY_EXTERNAL);
|
||||
|
||||
public static final String BASE_EMAIL_STATUS = "email_status";
|
||||
|
||||
public static class EmailStatus implements BaseColumns {
|
||||
public static final String EMAIL_ADDRESS = "email_address";
|
||||
public static final String EMAIL_STATUS = "email_status";
|
||||
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon()
|
||||
.appendPath(BASE_EMAIL_STATUS).build();
|
||||
|
||||
public static final String CONTENT_TYPE
|
||||
= "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status";
|
||||
}
|
||||
|
||||
private KeychainExternalContract() {
|
||||
}
|
||||
|
||||
}
|
||||
+13
-8
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org>
|
||||
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
* Copyright (C) 2014-2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* 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
|
||||
@@ -308,13 +308,18 @@ public class KeychainProvider extends ContentProvider {
|
||||
projectionMap.put(KeyRings.ALGORITHM, Tables.KEYS + "." + Keys.ALGORITHM);
|
||||
projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT);
|
||||
projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID);
|
||||
projectionMap.put(KeyRings.NAME, Tables.USER_PACKETS + "." + UserPackets.NAME);
|
||||
projectionMap.put(KeyRings.EMAIL, Tables.USER_PACKETS + "." + UserPackets.EMAIL);
|
||||
projectionMap.put(KeyRings.COMMENT, Tables.USER_PACKETS + "." + UserPackets.COMMENT);
|
||||
projectionMap.put(KeyRings.HAS_DUPLICATE_USER_ID,
|
||||
"(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups"
|
||||
"(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups"
|
||||
+ " WHERE dups." + UserPackets.MASTER_KEY_ID
|
||||
+ " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " AND dups." + UserPackets.RANK + " = 0"
|
||||
+ " AND dups." + UserPackets.USER_ID
|
||||
+ " = "+ Tables.USER_PACKETS + "." + UserPackets.USER_ID
|
||||
+ " AND dups." + UserPackets.NAME
|
||||
+ " = " + Tables.USER_PACKETS + "." + UserPackets.NAME + " COLLATE NOCASE"
|
||||
+ " AND dups." + UserPackets.EMAIL
|
||||
+ " = " + Tables.USER_PACKETS + "." + UserPackets.EMAIL + " COLLATE NOCASE"
|
||||
+ ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID);
|
||||
projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED);
|
||||
projectionMap.put(KeyRings.PUBKEY_DATA,
|
||||
@@ -451,12 +456,12 @@ public class KeychainProvider extends ContentProvider {
|
||||
if (i != 0) {
|
||||
emailWhere += " OR ";
|
||||
}
|
||||
emailWhere += "tmp." + UserPackets.USER_ID + " LIKE ";
|
||||
// match '*<email>', so it has to be at the *end* of the user id
|
||||
if (match == KEY_RINGS_FIND_BY_EMAIL) {
|
||||
emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
|
||||
emailWhere += "tmp." + UserPackets.EMAIL + " LIKE "
|
||||
+ DatabaseUtils.sqlEscapeString(chunks[i]);
|
||||
} else {
|
||||
emailWhere += DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%");
|
||||
emailWhere += "tmp." + UserPackets.USER_ID + " LIKE "
|
||||
+ DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%");
|
||||
}
|
||||
gotCondition = true;
|
||||
}
|
||||
|
||||
+18
-202
@@ -18,6 +18,7 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
@@ -29,19 +30,12 @@ import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
|
||||
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
||||
import org.sufficientlysecure.keychain.operations.ImportOperation;
|
||||
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
@@ -51,24 +45,25 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
|
||||
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
|
||||
import org.sufficientlysecure.keychain.remote.AccountSettings;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableFileCache;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.util.ProgressFixedScaler;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
import org.sufficientlysecure.keychain.util.Utf8Util;
|
||||
@@ -80,10 +75,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -459,11 +452,13 @@ public class ProviderHelper {
|
||||
mIndent += 1;
|
||||
for (byte[] rawUserId : masterKey.getUnorderedRawUserIds()) {
|
||||
String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId);
|
||||
|
||||
UserPacketItem item = new UserPacketItem();
|
||||
uids.add(item);
|
||||
KeyRing.UserId splitUserId = KeyRing.splitUserId(userId);
|
||||
item.userId = userId;
|
||||
|
||||
item.name = splitUserId.name;
|
||||
item.email = splitUserId.email;
|
||||
item.comment = splitUserId.comment;
|
||||
int unknownCerts = 0;
|
||||
|
||||
log(LogType.MSG_IP_UID_PROCESSING, userId);
|
||||
@@ -753,6 +748,9 @@ public class ProviderHelper {
|
||||
private static class UserPacketItem implements Comparable<UserPacketItem> {
|
||||
Integer type;
|
||||
String userId;
|
||||
String name;
|
||||
String email;
|
||||
String comment;
|
||||
byte[] attributeData;
|
||||
boolean isPrimary = false;
|
||||
WrappedSignature selfCert;
|
||||
@@ -1444,6 +1442,9 @@ public class ProviderHelper {
|
||||
values.put(UserPackets.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(UserPackets.TYPE, item.type);
|
||||
values.put(UserPackets.USER_ID, item.userId);
|
||||
values.put(UserPackets.NAME, item.name);
|
||||
values.put(UserPackets.EMAIL, item.email);
|
||||
values.put(UserPackets.COMMENT, item.comment);
|
||||
values.put(UserPackets.ATTRIBUTE_DATA, item.attributeData);
|
||||
values.put(UserPackets.IS_PRIMARY, item.isPrimary);
|
||||
values.put(UserPackets.IS_REVOKED, item.selfRevocation != null);
|
||||
@@ -1481,191 +1482,6 @@ public class ProviderHelper {
|
||||
return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values);
|
||||
}
|
||||
|
||||
public ArrayList<String> getRegisteredApiApps() {
|
||||
Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null);
|
||||
|
||||
ArrayList<String> packageNames = new ArrayList<>();
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
packageNames.add(cursor.getString(packageNameCol));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return packageNames;
|
||||
}
|
||||
|
||||
private ContentValues contentValueForApiApps(AppSettings appSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
|
||||
values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate());
|
||||
return values;
|
||||
}
|
||||
|
||||
private ContentValues contentValueForApiAccounts(AccountSettings accSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeychainContract.ApiAccounts.ACCOUNT_NAME, accSettings.getAccountName());
|
||||
values.put(KeychainContract.ApiAccounts.KEY_ID, accSettings.getKeyId());
|
||||
|
||||
// DEPRECATED and thus hardcoded
|
||||
values.put(KeychainContract.ApiAccounts.COMPRESSION, CompressionAlgorithmTags.ZLIB);
|
||||
values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM,
|
||||
PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT);
|
||||
values.put(KeychainContract.ApiAccounts.HASH_ALORITHM,
|
||||
PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT);
|
||||
return values;
|
||||
}
|
||||
|
||||
public void insertApiApp(AppSettings appSettings) {
|
||||
mContentResolver.insert(KeychainContract.ApiApps.CONTENT_URI,
|
||||
contentValueForApiApps(appSettings));
|
||||
}
|
||||
|
||||
public void insertApiAccount(Uri uri, AccountSettings accSettings) {
|
||||
mContentResolver.insert(uri, contentValueForApiAccounts(accSettings));
|
||||
}
|
||||
|
||||
public void updateApiAccount(Uri uri, AccountSettings accSettings) {
|
||||
if (mContentResolver.update(uri, contentValueForApiAccounts(accSettings), null,
|
||||
null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be an uri pointing to an account
|
||||
*/
|
||||
public AppSettings getApiAppSettings(Uri uri) {
|
||||
AppSettings settings = null;
|
||||
|
||||
Cursor cursor = mContentResolver.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
settings = new AppSettings();
|
||||
settings.setPackageName(cursor.getString(
|
||||
cursor.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME)));
|
||||
settings.setPackageCertificate(cursor.getBlob(
|
||||
cursor.getColumnIndex(KeychainContract.ApiApps.PACKAGE_CERTIFICATE)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public AccountSettings getApiAccountSettings(Uri accountUri) {
|
||||
AccountSettings settings = null;
|
||||
|
||||
Cursor cursor = mContentResolver.query(accountUri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
settings = new AccountSettings();
|
||||
|
||||
settings.setAccountName(cursor.getString(
|
||||
cursor.getColumnIndex(KeychainContract.ApiAccounts.ACCOUNT_NAME)));
|
||||
settings.setKeyId(cursor.getLong(
|
||||
cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public Set<Long> getAllKeyIdsForApp(Uri uri) {
|
||||
Set<Long> keyIds = new HashSet<>();
|
||||
|
||||
Cursor cursor = mContentResolver.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID);
|
||||
while (cursor.moveToNext()) {
|
||||
keyIds.add(cursor.getLong(keyIdColumn));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public HashSet<Long> getAllowedKeyIdsForApp(Uri uri) {
|
||||
HashSet<Long> keyIds = new HashSet<>();
|
||||
|
||||
Cursor cursor = mContentResolver.query(uri, null, null, null, null);
|
||||
try {
|
||||
if (cursor != null) {
|
||||
int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAllowedKeys.KEY_ID);
|
||||
while (cursor.moveToNext()) {
|
||||
keyIds.add(cursor.getLong(keyIdColumn));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public void saveAllowedKeyIdsForApp(Uri uri, Set<Long> allowedKeyIds)
|
||||
throws RemoteException, OperationApplicationException {
|
||||
// wipe whole table of allowed keys for this account
|
||||
mContentResolver.delete(uri, null, null);
|
||||
|
||||
// re-insert allowed key ids
|
||||
for (Long keyId : allowedKeyIds) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiAllowedKeys.KEY_ID, keyId);
|
||||
mContentResolver.insert(uri, values);
|
||||
}
|
||||
}
|
||||
|
||||
public void addAllowedKeyIdForApp(Uri uri, long allowedKeyId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiAllowedKeys.KEY_ID, allowedKeyId);
|
||||
mContentResolver.insert(uri, values);
|
||||
}
|
||||
|
||||
public byte[] getApiAppCertificate(String packageName) {
|
||||
Uri queryUri = ApiApps.buildByPackageNameUri(packageName);
|
||||
|
||||
String[] projection = new String[]{ApiApps.PACKAGE_CERTIFICATE};
|
||||
|
||||
Cursor cursor = mContentResolver.query(queryUri, projection, null, null, null);
|
||||
try {
|
||||
byte[] signature = null;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int signatureCol = 0;
|
||||
|
||||
signature = cursor.getBlob(signatureCol);
|
||||
}
|
||||
return signature;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ContentResolver getContentResolver() {
|
||||
return mContentResolver;
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
/** This interface contains the principal methods for database access
|
||||
* from {#android.content.ContentResolver}. It is used to allow substitution
|
||||
* of a ContentResolver in DAOs.
|
||||
*
|
||||
* @see ApiDataAccessObject
|
||||
*/
|
||||
public interface SimpleContentResolverInterface {
|
||||
Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, String sortOrder);
|
||||
|
||||
Uri insert(Uri contentUri, ContentValues values);
|
||||
|
||||
int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs);
|
||||
|
||||
int delete(Uri contentUri, String where, String[] selectionArgs);
|
||||
}
|
||||
+11
-2
@@ -99,6 +99,12 @@ public class TemporaryFileProvider extends ContentProvider {
|
||||
return context.getContentResolver().insert(CONTENT_URI, contentValues);
|
||||
}
|
||||
|
||||
public static int setName(Context context, Uri uri, String name) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TemporaryFileColumns.COLUMN_NAME, name);
|
||||
return context.getContentResolver().update(uri, values, null, null);
|
||||
}
|
||||
|
||||
public static int setMimeType(Context context, Uri uri, String mimetype) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype);
|
||||
@@ -283,8 +289,11 @@ public class TemporaryFileProvider extends ContentProvider {
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
if (values.size() != 1 || !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) {
|
||||
throw new UnsupportedOperationException("Update supported only for type field!");
|
||||
if (values.size() != 1) {
|
||||
throw new UnsupportedOperationException("Update supported only for one field at a time!");
|
||||
}
|
||||
if (!values.containsKey(TemporaryFileColumns.COLUMN_NAME) && !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) {
|
||||
throw new UnsupportedOperationException("Update supported only for name and type field!");
|
||||
}
|
||||
if (selection != null || selectionArgs != null) {
|
||||
throw new UnsupportedOperationException("Update supported only for plain uri!");
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package org.sufficientlysecure.keychain.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class NetworkReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
ConnectivityManager conn = (ConnectivityManager)
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
|
||||
boolean isTypeWifi = (networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
|
||||
boolean isConnected = networkInfo.isConnected();
|
||||
|
||||
if (isTypeWifi && isConnected) {
|
||||
|
||||
// broadcaster receiver disabled
|
||||
setWifiReceiverComponent(false, context);
|
||||
Intent serviceIntent = new Intent(context, KeyserverSyncAdapterService.class);
|
||||
serviceIntent.setAction(KeyserverSyncAdapterService.ACTION_SYNC_NOW);
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
public void setWifiReceiverComponent(Boolean isEnabled, Context context) {
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
ComponentName compName = new ComponentName(context,
|
||||
NetworkReceiver.class);
|
||||
|
||||
if (isEnabled) {
|
||||
pm.setComponentEnabledSetting(compName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
|
||||
Log.d(Constants.TAG, "Wifi Receiver is enabled!");
|
||||
} else {
|
||||
pm.setComponentEnabledSetting(compName,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
Log.d(Constants.TAG, "Wifi Receiver is disabled!");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -51,10 +51,10 @@ public class ApiPendingIntentFactory {
|
||||
CryptoInputParcel cryptoInput) {
|
||||
|
||||
switch (requiredInput.mType) {
|
||||
case NFC_MOVE_KEY_TO_CARD:
|
||||
case NFC_DECRYPT:
|
||||
case NFC_SIGN: {
|
||||
return createNfcOperationPendingIntent(data, requiredInput, cryptoInput);
|
||||
case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
|
||||
case SECURITY_TOKEN_DECRYPT:
|
||||
case SECURITY_TOKEN_SIGN: {
|
||||
return createSecurityTokenOperationPendingIntent(data, requiredInput, cryptoInput);
|
||||
}
|
||||
|
||||
case PASSPHRASE: {
|
||||
@@ -66,7 +66,7 @@ public class ApiPendingIntentFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent createNfcOperationPendingIntent(Intent data, RequiredInputParcel requiredInput, CryptoInputParcel cryptoInput) {
|
||||
private PendingIntent createSecurityTokenOperationPendingIntent(Intent data, RequiredInputParcel requiredInput, CryptoInputParcel cryptoInput) {
|
||||
Intent intent = new Intent(mContext, RemoteSecurityTokenOperationActivity.class);
|
||||
// pass params through to activity that it can be returned again later to repeat pgp operation
|
||||
intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
|
||||
|
||||
+40
-37
@@ -18,6 +18,10 @@
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
@@ -33,15 +37,10 @@ import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
/**
|
||||
* Abstract service class for remote APIs that handle app registration and user input.
|
||||
@@ -49,13 +48,13 @@ import java.util.Arrays;
|
||||
public class ApiPermissionHelper {
|
||||
|
||||
private final Context mContext;
|
||||
private final ProviderHelper mProviderHelper;
|
||||
private final ApiDataAccessObject mApiDao;
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
public ApiPermissionHelper(Context context) {
|
||||
public ApiPermissionHelper(Context context, ApiDataAccessObject apiDao) {
|
||||
mContext = context;
|
||||
mPackageManager = context.getPackageManager();
|
||||
mProviderHelper = new ProviderHelper(context);
|
||||
mApiDao = apiDao;
|
||||
}
|
||||
|
||||
public static class WrongPackageCertificateException extends Exception {
|
||||
@@ -66,14 +65,24 @@ public class ApiPermissionHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true iff the caller is allowed, or false on any type of problem.
|
||||
* This method should only be used in cases where error handling is dealt with separately.
|
||||
*/
|
||||
protected boolean isAllowedIgnoreErrors() {
|
||||
try {
|
||||
return isCallerAllowed();
|
||||
} catch (WrongPackageCertificateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if caller is allowed to access the API
|
||||
*
|
||||
* @return null if caller is allowed, or a Bundle with a PendingIntent
|
||||
*/
|
||||
protected Intent isAllowed(Intent data) {
|
||||
protected Intent isAllowedOrReturnIntent(Intent data) {
|
||||
ApiPendingIntentFactory piFactory = new ApiPendingIntentFactory(mContext);
|
||||
|
||||
try {
|
||||
if (isCallerAllowed()) {
|
||||
return null;
|
||||
@@ -168,7 +177,7 @@ public class ApiPermissionHelper {
|
||||
|
||||
Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(currentPkg, accountName);
|
||||
|
||||
return mProviderHelper.getApiAccountSettings(uri); // can be null!
|
||||
return mApiDao.getApiAccountSettings(uri); // can be null!
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@@ -224,35 +233,29 @@ public class ApiPermissionHelper {
|
||||
private boolean isPackageAllowed(String packageName) throws WrongPackageCertificateException {
|
||||
Log.d(Constants.TAG, "isPackageAllowed packageName: " + packageName);
|
||||
|
||||
ArrayList<String> allowedPkgs = mProviderHelper.getRegisteredApiApps();
|
||||
Log.d(Constants.TAG, "allowed: " + allowedPkgs);
|
||||
byte[] storedPackageCert = mApiDao.getApiAppCertificate(packageName);
|
||||
|
||||
// check if package is allowed to use our service
|
||||
if (allowedPkgs.contains(packageName)) {
|
||||
Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
|
||||
boolean isKnownPackage = storedPackageCert != null;
|
||||
if (!isKnownPackage) {
|
||||
Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName);
|
||||
return false;
|
||||
}
|
||||
Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
|
||||
|
||||
// check package signature
|
||||
byte[] currentCert;
|
||||
try {
|
||||
currentCert = getPackageCertificate(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
throw new WrongPackageCertificateException(e.getMessage());
|
||||
}
|
||||
|
||||
byte[] storedCert = mProviderHelper.getApiAppCertificate(packageName);
|
||||
if (Arrays.equals(currentCert, storedCert)) {
|
||||
Log.d(Constants.TAG,
|
||||
"Package certificate is correct! (equals certificate from database)");
|
||||
return true;
|
||||
} else {
|
||||
throw new WrongPackageCertificateException(
|
||||
"PACKAGE NOT ALLOWED! Certificate wrong! (Certificate not " +
|
||||
"equals certificate from database)");
|
||||
}
|
||||
byte[] currentPackageCert;
|
||||
try {
|
||||
currentPackageCert = getPackageCertificate(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
throw new WrongPackageCertificateException(e.getMessage());
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName);
|
||||
return false;
|
||||
boolean packageCertMatchesStored = Arrays.equals(currentPackageCert, storedPackageCert);
|
||||
if (packageCertMatchesStored) {
|
||||
Log.d(Constants.TAG,"Package certificate matches expected.");
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new WrongPackageCertificateException("PACKAGE NOT ALLOWED DUE TO CERTIFICATE MISMATCH!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Vincent Breitmoser <look@my.amazin.horse>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.security.AccessControlException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
|
||||
import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
|
||||
public class KeychainExternalProvider extends ContentProvider implements SimpleContentResolverInterface {
|
||||
private static final int EMAIL_STATUS = 101;
|
||||
private static final int API_APPS = 301;
|
||||
private static final int API_APPS_BY_PACKAGE_NAME = 302;
|
||||
|
||||
|
||||
private UriMatcher mUriMatcher;
|
||||
private ApiPermissionHelper mApiPermissionHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Build and return a {@link UriMatcher} that catches all {@link Uri} variations supported by
|
||||
* this {@link ContentProvider}.
|
||||
*/
|
||||
protected UriMatcher buildUriMatcher() {
|
||||
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
String authority = KeychainExternalContract.CONTENT_AUTHORITY_EXTERNAL;
|
||||
|
||||
/**
|
||||
* list email_status
|
||||
*
|
||||
* <pre>
|
||||
* email_status/
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS, EMAIL_STATUS);
|
||||
|
||||
matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS, API_APPS);
|
||||
matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME);
|
||||
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private KeychainDatabase mKeychainDatabase;
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mUriMatcher = buildUriMatcher();
|
||||
mApiPermissionHelper = new ApiPermissionHelper(getContext(), new ApiDataAccessObject(this));
|
||||
return true;
|
||||
}
|
||||
|
||||
public KeychainDatabase getDb() {
|
||||
if(mKeychainDatabase == null)
|
||||
mKeychainDatabase = new KeychainDatabase(getContext());
|
||||
return mKeychainDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
switch (match) {
|
||||
case EMAIL_STATUS:
|
||||
return EmailStatus.CONTENT_TYPE;
|
||||
|
||||
case API_APPS:
|
||||
return ApiApps.CONTENT_TYPE;
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
return ApiApps.CONTENT_ITEM_TYPE;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
Log.v(Constants.TAG, "query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")");
|
||||
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
|
||||
int match = mUriMatcher.match(uri);
|
||||
|
||||
String groupBy = null;
|
||||
|
||||
switch (match) {
|
||||
case EMAIL_STATUS: {
|
||||
boolean callerIsAllowed = mApiPermissionHelper.isAllowedIgnoreErrors();
|
||||
if (!callerIsAllowed) {
|
||||
throw new AccessControlException("An application must register before use of KeychainExternalProvider!");
|
||||
}
|
||||
|
||||
HashMap<String, String> projectionMap = new HashMap<>();
|
||||
projectionMap.put(EmailStatus._ID, "email AS _id");
|
||||
projectionMap.put(EmailStatus.EMAIL_ADDRESS,
|
||||
Tables.USER_PACKETS + "." + UserPackets.USER_ID + " AS " + EmailStatus.EMAIL_ADDRESS);
|
||||
// we take the minimum (>0) here, where "1" is "verified by known secret key", "2" is "self-certified"
|
||||
projectionMap.put(EmailStatus.EMAIL_STATUS, "CASE ( MIN (" + Certs.VERIFIED + " ) ) "
|
||||
// remap to keep this provider contract independent from our internal representation
|
||||
+ " WHEN " + Certs.VERIFIED_SELF + " THEN 1"
|
||||
+ " WHEN " + Certs.VERIFIED_SECRET + " THEN 2"
|
||||
+ " END AS " + EmailStatus.EMAIL_STATUS);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
if (projection == null) {
|
||||
throw new IllegalArgumentException("Please provide a projection!");
|
||||
}
|
||||
|
||||
qb.setTables(
|
||||
Tables.USER_PACKETS
|
||||
+ " INNER JOIN " + Tables.CERTS + " ON ("
|
||||
+ Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = "
|
||||
+ Tables.CERTS + "." + Certs.MASTER_KEY_ID
|
||||
+ " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = "
|
||||
+ Tables.CERTS + "." + Certs.RANK
|
||||
// verified == 0 has no self-cert, which is basically an error case. never return that!
|
||||
+ " AND " + Tables.CERTS + "." + Certs.VERIFIED + " > 0"
|
||||
+ ")"
|
||||
);
|
||||
qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.USER_ID + " IS NOT NULL");
|
||||
// in case there are multiple verifying certificates
|
||||
groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + ", "
|
||||
+ Tables.USER_PACKETS + "." + UserPackets.USER_ID;
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = EmailStatus.EMAIL_ADDRESS + " ASC, " + EmailStatus.EMAIL_STATUS + " DESC";
|
||||
}
|
||||
|
||||
// uri to watch is all /key_rings/
|
||||
uri = KeyRings.CONTENT_URI;
|
||||
|
||||
boolean gotCondition = false;
|
||||
String emailWhere = "";
|
||||
// JAVA ♥
|
||||
for (int i = 0; i < selectionArgs.length; ++i) {
|
||||
if (selectionArgs[i].length() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (i != 0) {
|
||||
emailWhere += " OR ";
|
||||
}
|
||||
emailWhere += UserPackets.USER_ID + " LIKE ";
|
||||
// match '*<email>', so it has to be at the *end* of the user id
|
||||
emailWhere += DatabaseUtils.sqlEscapeString("%<" + selectionArgs[i] + ">");
|
||||
gotCondition = true;
|
||||
}
|
||||
|
||||
if (gotCondition) {
|
||||
qb.appendWhere(" AND (" + emailWhere + ")");
|
||||
} else {
|
||||
// TODO better way to do this?
|
||||
Log.e(Constants.TAG, "Malformed find by email query!");
|
||||
qb.appendWhere(" AND 0");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME: {
|
||||
String requestedPackageName = uri.getLastPathSegment();
|
||||
checkIfPackageBelongsToCaller(getContext(), requestedPackageName);
|
||||
|
||||
qb.setTables(Tables.API_APPS);
|
||||
qb.appendWhere(ApiApps.PACKAGE_NAME + " = ");
|
||||
qb.appendWhereEscapeString(requestedPackageName);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If no sort order is specified use the default
|
||||
String orderBy;
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
orderBy = null;
|
||||
} else {
|
||||
orderBy = sortOrder;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = getDb().getReadableDatabase();
|
||||
|
||||
Cursor cursor = qb.query(db, projection, selection, null, groupBy, null, orderBy);
|
||||
if (cursor != null) {
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG,
|
||||
"Query: " + qb.buildQuery(projection, selection, null, null, orderBy, null));
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private void checkIfPackageBelongsToCaller(Context context, String requestedPackageName) {
|
||||
int callerUid = Binder.getCallingUid();
|
||||
String[] callerPackageNames = context.getPackageManager().getPackagesForUid(callerUid);
|
||||
if (callerPackageNames == null) {
|
||||
throw new IllegalStateException("Failed to retrieve caller package name, this is an error!");
|
||||
}
|
||||
|
||||
boolean packageBelongsToCaller = false;
|
||||
for (String p : callerPackageNames) {
|
||||
if (p.equals(requestedPackageName)) {
|
||||
packageBelongsToCaller = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!packageBelongsToCaller) {
|
||||
throw new SecurityException("ExternalProvider may only check status of caller package!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String additionalSelection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
+139
-59
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2016 Vincent Breitmoser <look@my.amazin.horse>
|
||||
*
|
||||
* 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
|
||||
@@ -17,11 +18,24 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Parcelable;
|
||||
@@ -43,12 +57,15 @@ import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
|
||||
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing.UserId;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
@@ -61,18 +78,12 @@ import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class OpenPgpService extends Service {
|
||||
|
||||
static final String[] EMAIL_SEARCH_PROJECTION = new String[]{
|
||||
public static final List<Integer> SUPPORTED_VERSIONS =
|
||||
Collections.unmodifiableList(Arrays.asList(3, 4, 5, 6, 7, 8, 9, 10, 11));
|
||||
|
||||
static final String[] KEY_SEARCH_PROJECTION = new String[]{
|
||||
KeyRings._ID,
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
KeyRings.IS_EXPIRED,
|
||||
@@ -80,35 +91,50 @@ public class OpenPgpService extends Service {
|
||||
};
|
||||
|
||||
// do not pre-select revoked or expired keys
|
||||
static final String EMAIL_SEARCH_WHERE = Tables.KEYS + "." + KeychainContract.KeyRings.IS_REVOKED
|
||||
static final String KEY_SEARCH_WHERE = Tables.KEYS + "." + KeychainContract.KeyRings.IS_REVOKED
|
||||
+ " = 0 AND " + KeychainContract.KeyRings.IS_EXPIRED + " = 0";
|
||||
|
||||
private ApiPermissionHelper mApiPermissionHelper;
|
||||
private ProviderHelper mProviderHelper;
|
||||
private ApiDataAccessObject mApiDao;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this);
|
||||
mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this));
|
||||
mProviderHelper = new ProviderHelper(this);
|
||||
mApiDao = new ApiDataAccessObject(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search database for key ids based on emails.
|
||||
*/
|
||||
private Intent returnKeyIdsFromEmails(Intent data, String[] encryptionUserIds) {
|
||||
private static class KeyIdResult {
|
||||
final Intent mResultIntent;
|
||||
final HashSet<Long> mKeyIds;
|
||||
|
||||
KeyIdResult(Intent resultIntent) {
|
||||
mResultIntent = resultIntent;
|
||||
mKeyIds = null;
|
||||
}
|
||||
KeyIdResult(HashSet<Long> keyIds) {
|
||||
mResultIntent = null;
|
||||
mKeyIds = keyIds;
|
||||
}
|
||||
}
|
||||
|
||||
private KeyIdResult returnKeyIdsFromEmails(Intent data, String[] encryptionUserIds, boolean isOpportunistic) {
|
||||
boolean noUserIdsCheck = (encryptionUserIds == null || encryptionUserIds.length == 0);
|
||||
boolean missingUserIdsCheck = false;
|
||||
boolean duplicateUserIdsCheck = false;
|
||||
|
||||
ArrayList<Long> keyIds = new ArrayList<>();
|
||||
HashSet<Long> keyIds = new HashSet<>();
|
||||
ArrayList<String> missingEmails = new ArrayList<>();
|
||||
ArrayList<String> duplicateEmails = new ArrayList<>();
|
||||
if (!noUserIdsCheck) {
|
||||
for (String email : encryptionUserIds) {
|
||||
for (String rawUserId : encryptionUserIds) {
|
||||
UserId userId = KeyRing.splitUserId(rawUserId);
|
||||
String email = userId.email != null ? userId.email : rawUserId;
|
||||
// try to find the key for this specific email
|
||||
Uri uri = KeyRings.buildUnifiedKeyRingsFindByEmailUri(email);
|
||||
Cursor cursor = getContentResolver().query(uri, EMAIL_SEARCH_PROJECTION, EMAIL_SEARCH_WHERE, null, null);
|
||||
Cursor cursor = getContentResolver().query(uri, KEY_SEARCH_PROJECTION, KEY_SEARCH_WHERE, null, null);
|
||||
try {
|
||||
// result should be one entry containing the key id
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
@@ -137,15 +163,17 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// convert ArrayList<Long> to long[]
|
||||
long[] keyIdsArray = new long[keyIds.size()];
|
||||
for (int i = 0; i < keyIdsArray.length; i++) {
|
||||
keyIdsArray[i] = keyIds.get(i);
|
||||
if (isOpportunistic && (noUserIdsCheck || missingUserIdsCheck)) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_ERROR,
|
||||
new OpenPgpError(OpenPgpError.OPPORTUNISTIC_MISSING_KEYS, "missing keys in opportunistic mode"));
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
|
||||
return new KeyIdResult(result);
|
||||
}
|
||||
|
||||
if (noUserIdsCheck || missingUserIdsCheck || duplicateUserIdsCheck) {
|
||||
// allow the user to verify pub key selection
|
||||
|
||||
// convert ArrayList<Long> to long[]
|
||||
long[] keyIdsArray = getUnboxedLongArray(keyIds);
|
||||
ApiPendingIntentFactory piFactory = new ApiPendingIntentFactory(getBaseContext());
|
||||
PendingIntent pi = piFactory.createSelectPublicKeyPendingIntent(data, keyIdsArray,
|
||||
missingEmails, duplicateEmails, noUserIdsCheck);
|
||||
@@ -154,19 +182,15 @@ public class OpenPgpService extends Service {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
|
||||
return result;
|
||||
} else {
|
||||
// everything was easy, we have exactly one key for every email
|
||||
|
||||
if (keyIdsArray.length == 0) {
|
||||
Log.e(Constants.TAG, "keyIdsArray.length == 0, should never happen!");
|
||||
}
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_KEY_IDS, keyIdsArray);
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
return result;
|
||||
return new KeyIdResult(result);
|
||||
}
|
||||
|
||||
// everything was easy, we have exactly one key for every email
|
||||
if (keyIds.isEmpty()) {
|
||||
Log.e(Constants.TAG, "keyIdsArray.length == 0, should never happen!");
|
||||
}
|
||||
|
||||
return new KeyIdResult(keyIds);
|
||||
}
|
||||
|
||||
private Intent signImpl(Intent data, InputStream inputStream,
|
||||
@@ -280,20 +304,31 @@ public class OpenPgpService extends Service {
|
||||
compressionId = PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED;
|
||||
}
|
||||
|
||||
// first try to get key ids from non-ambiguous key id extra
|
||||
long[] keyIds = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS);
|
||||
if (keyIds == null) {
|
||||
// get key ids based on given user ids
|
||||
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
|
||||
// give params through to activity...
|
||||
Intent result = returnKeyIdsFromEmails(data, userIds);
|
||||
long[] keyIds;
|
||||
{
|
||||
HashSet<Long> encryptKeyIds = new HashSet<>();
|
||||
|
||||
if (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0) == OpenPgpApi.RESULT_CODE_SUCCESS) {
|
||||
keyIds = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS);
|
||||
} else {
|
||||
// if not success -> result contains a PendingIntent for user interaction
|
||||
return result;
|
||||
// get key ids based on given user ids
|
||||
if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS)) {
|
||||
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
|
||||
boolean isOpportunistic = data.getBooleanExtra(OpenPgpApi.EXTRA_OPPORTUNISTIC_ENCRYPTION, false);
|
||||
// give params through to activity...
|
||||
KeyIdResult result = returnKeyIdsFromEmails(data, userIds, isOpportunistic);
|
||||
|
||||
if (result.mResultIntent != null) {
|
||||
return result.mResultIntent;
|
||||
}
|
||||
encryptKeyIds.addAll(result.mKeyIds);
|
||||
}
|
||||
|
||||
// add key ids from non-ambiguous key id extra
|
||||
if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
|
||||
for (long keyId : data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
|
||||
encryptKeyIds.add(keyId);
|
||||
}
|
||||
}
|
||||
|
||||
keyIds = getUnboxedLongArray(encryptKeyIds);
|
||||
}
|
||||
|
||||
// TODO this is not correct!
|
||||
@@ -305,8 +340,7 @@ public class OpenPgpService extends Service {
|
||||
.setVersionHeader(null)
|
||||
.setCompressionAlgorithm(compressionId)
|
||||
.setSymmetricEncryptionAlgorithm(PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT)
|
||||
.setEncryptionMasterKeyIds(keyIds)
|
||||
.setFailOnMissingEncryptionKeyIds(true);
|
||||
.setEncryptionMasterKeyIds(keyIds);
|
||||
|
||||
if (sign) {
|
||||
|
||||
@@ -405,11 +439,11 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
|
||||
String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
|
||||
HashSet<Long> allowedKeyIds = mProviderHelper.getAllowedKeyIdsForApp(
|
||||
HashSet<Long> allowedKeyIds = mApiDao.getAllowedKeyIdsForApp(
|
||||
KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg));
|
||||
|
||||
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) {
|
||||
allowedKeyIds.addAll(mProviderHelper.getAllKeyIdsForApp(
|
||||
allowedKeyIds.addAll(mApiDao.getAllKeyIdsForApp(
|
||||
ApiAccounts.buildBaseUri(currentPkg)));
|
||||
}
|
||||
|
||||
@@ -422,6 +456,15 @@ public class OpenPgpService extends Service {
|
||||
cryptoInput.mPassphrase =
|
||||
new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE));
|
||||
}
|
||||
if (data.hasExtra(OpenPgpApi.EXTRA_DECRYPTION_RESULT_WRAPPER)) {
|
||||
// this is wrapped in a Bundle to avoid ClassLoader problems
|
||||
Bundle wrapperBundle = data.getBundleExtra(OpenPgpApi.EXTRA_DECRYPTION_RESULT_WRAPPER);
|
||||
wrapperBundle.setClassLoader(getClassLoader());
|
||||
OpenPgpDecryptionResult decryptionResult = wrapperBundle.getParcelable(OpenPgpApi.EXTRA_DECRYPTION_RESULT);
|
||||
if (decryptionResult != null && decryptionResult.hasDecryptedSessionKey()) {
|
||||
cryptoInput.addCryptoData(decryptionResult.sessionKey, decryptionResult.decryptedSessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE);
|
||||
|
||||
@@ -582,7 +625,8 @@ public class OpenPgpService extends Service {
|
||||
try {
|
||||
// try to find key, throws NotFoundException if not in db!
|
||||
CanonicalizedPublicKeyRing keyRing =
|
||||
mProviderHelper.getCanonicalizedPublicKeyRing(masterKeyId);
|
||||
mProviderHelper.getCanonicalizedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(masterKeyId));
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
@@ -669,7 +713,21 @@ public class OpenPgpService extends Service {
|
||||
} else {
|
||||
// get key ids based on given user ids
|
||||
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
|
||||
return returnKeyIdsFromEmails(data, userIds);
|
||||
KeyIdResult keyResult = returnKeyIdsFromEmails(data, userIds, false);
|
||||
if (keyResult.mResultIntent != null) {
|
||||
return keyResult.mResultIntent;
|
||||
}
|
||||
|
||||
if (keyResult.mKeyIds == null) {
|
||||
throw new AssertionError("one of requiredUserInteraction and keyIds must be non-null, this is a bug!");
|
||||
}
|
||||
|
||||
long[] keyIds = getUnboxedLongArray(keyResult.mKeyIds);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
resultIntent.putExtra(OpenPgpApi.RESULT_KEY_IDS, keyIds);
|
||||
return resultIntent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,6 +774,26 @@ public class OpenPgpService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static long[] getUnboxedLongArray(@NonNull Collection<Long> arrayList) {
|
||||
long[] result = new long[arrayList.size()];
|
||||
int i = 0;
|
||||
for (Long e : arrayList) {
|
||||
result[i++] = e;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Intent checkPermissionImpl(@NonNull Intent data) {
|
||||
Intent permissionIntent = mApiPermissionHelper.isAllowedOrReturnIntent(data);
|
||||
if (permissionIntent != null) {
|
||||
return permissionIntent;
|
||||
}
|
||||
Intent result = new Intent();
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Intent getSignKeyMasterId(Intent data) {
|
||||
// NOTE: Accounts are deprecated on API version >= 7
|
||||
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) {
|
||||
@@ -765,20 +843,19 @@ public class OpenPgpService extends Service {
|
||||
|
||||
// version code is required and needs to correspond to version code of service!
|
||||
// History of versions in openpgp-api's CHANGELOG.md
|
||||
List<Integer> supportedVersions = Arrays.asList(3, 4, 5, 6, 7, 8, 9, 10);
|
||||
if (!supportedVersions.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
|
||||
if (!SUPPORTED_VERSIONS.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
|
||||
Intent result = new Intent();
|
||||
OpenPgpError error = new OpenPgpError
|
||||
(OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n"
|
||||
+ "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n"
|
||||
+ "supported API versions: " + supportedVersions);
|
||||
+ "supported API versions: " + SUPPORTED_VERSIONS);
|
||||
result.putExtra(OpenPgpApi.RESULT_ERROR, error);
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
|
||||
return result;
|
||||
}
|
||||
|
||||
// check if caller is allowed to access OpenKeychain
|
||||
Intent result = mApiPermissionHelper.isAllowed(data);
|
||||
Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
@@ -845,6 +922,9 @@ public class OpenPgpService extends Service {
|
||||
|
||||
String action = data.getAction();
|
||||
switch (action) {
|
||||
case OpenPgpApi.ACTION_CHECK_PERMISSION: {
|
||||
return checkPermissionImpl(data);
|
||||
}
|
||||
case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
|
||||
return signImpl(data, inputStream, outputStream, true);
|
||||
}
|
||||
|
||||
+3
-3
@@ -29,7 +29,7 @@ import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.remote.AccountSettings;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -98,7 +98,7 @@ public class AccountSettingsActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void loadData(Uri accountUri) {
|
||||
AccountSettings settings = new ProviderHelper(this).getApiAccountSettings(accountUri);
|
||||
AccountSettings settings = new ApiDataAccessObject(this).getApiAccountSettings(accountUri);
|
||||
mAccountSettingsFragment.setAccSettings(settings);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class AccountSettingsActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void save() {
|
||||
new ProviderHelper(this).updateApiAccount(mAccountUri, mAccountSettingsFragment.getAccSettings());
|
||||
new ApiDataAccessObject(this).updateApiAccount(mAccountUri, mAccountSettingsFragment.getAccSettings());
|
||||
SingletonResult result = new SingletonResult(
|
||||
SingletonResult.RESULT_OK, LogType.MSG_ACC_SAVED);
|
||||
Intent intent = new Intent();
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ import org.bouncycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.AdvancedAppSettingsDialogFragment;
|
||||
@@ -182,7 +182,7 @@ public class AppSettingsActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void loadData(Bundle savedInstanceState, Uri appUri) {
|
||||
mAppSettings = new ProviderHelper(this).getApiAppSettings(appUri);
|
||||
mAppSettings = new ApiDataAccessObject(this).getApiAppSettings(appUri);
|
||||
|
||||
// get application name and icon from package manager
|
||||
String appName;
|
||||
|
||||
+5
-5
@@ -36,8 +36,8 @@ import android.widget.ListView;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.widget.FixedListView;
|
||||
@@ -47,7 +47,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
|
||||
private static final String ARG_DATA_URI = "uri";
|
||||
|
||||
private KeySelectableAdapter mAdapter;
|
||||
private ProviderHelper mProviderHelper;
|
||||
private ApiDataAccessObject mApiDao;
|
||||
|
||||
private Uri mDataUri;
|
||||
|
||||
@@ -69,7 +69,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mProviderHelper = new ProviderHelper(getActivity());
|
||||
mApiDao = new ApiDataAccessObject(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,7 +107,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
|
||||
// application this would come from a resource.
|
||||
setEmptyText(getString(R.string.list_empty));
|
||||
|
||||
Set<Long> checked = mProviderHelper.getAllKeyIdsForApp(mDataUri);
|
||||
Set<Long> checked = mApiDao.getAllKeyIdsForApp(mDataUri);
|
||||
mAdapter = new KeySelectableAdapter(getActivity(), null, 0, checked);
|
||||
setListAdapter(mAdapter);
|
||||
getListView().setOnItemClickListener(mAdapter);
|
||||
@@ -141,7 +141,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
|
||||
|
||||
public void saveAllowedKeys() {
|
||||
try {
|
||||
mProviderHelper.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds());
|
||||
mApiDao.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds());
|
||||
} catch (RemoteException | OperationApplicationException e) {
|
||||
Log.e(Constants.TAG, "Problem saving allowed key ids!", e);
|
||||
}
|
||||
|
||||
+6
-5
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui;
|
||||
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -25,8 +26,8 @@ import android.widget.TextView;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.remote.AccountSettings;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
@@ -56,7 +57,7 @@ public class RemoteCreateAccountActivity extends BaseActivity {
|
||||
final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
|
||||
final String accName = extras.getString(EXTRA_ACC_NAME);
|
||||
|
||||
final ProviderHelper providerHelper = new ProviderHelper(this);
|
||||
final ApiDataAccessObject apiDao = new ApiDataAccessObject(this);
|
||||
|
||||
mAccSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById(
|
||||
R.id.api_account_settings_fragment);
|
||||
@@ -65,7 +66,7 @@ public class RemoteCreateAccountActivity extends BaseActivity {
|
||||
|
||||
// update existing?
|
||||
Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(packageName, accName);
|
||||
AccountSettings settings = providerHelper.getApiAccountSettings(uri);
|
||||
AccountSettings settings = apiDao.getApiAccountSettings(uri);
|
||||
if (settings == null) {
|
||||
// create new account
|
||||
settings = new AccountSettings(accName);
|
||||
@@ -94,11 +95,11 @@ public class RemoteCreateAccountActivity extends BaseActivity {
|
||||
if (mUpdateExistingAccount) {
|
||||
Uri baseUri = KeychainContract.ApiAccounts.buildBaseUri(packageName);
|
||||
Uri accountUri = baseUri.buildUpon().appendEncodedPath(accName).build();
|
||||
providerHelper.updateApiAccount(
|
||||
apiDao.updateApiAccount(
|
||||
accountUri,
|
||||
mAccSettingsFragment.getAccSettings());
|
||||
} else {
|
||||
providerHelper.insertApiAccount(
|
||||
apiDao.insertApiAccount(
|
||||
KeychainContract.ApiAccounts.buildBaseUri(packageName),
|
||||
mAccSettingsFragment.getAccSettings());
|
||||
}
|
||||
|
||||
+4
-4
@@ -17,13 +17,14 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui;
|
||||
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -52,7 +53,7 @@ public class RemoteRegisterActivity extends BaseActivity {
|
||||
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
|
||||
Log.d(Constants.TAG, "ACTION_REGISTER packageName: " + packageName);
|
||||
|
||||
final ProviderHelper providerHelper = new ProviderHelper(this);
|
||||
final ApiDataAccessObject apiDao = new ApiDataAccessObject(this);
|
||||
|
||||
mAppSettingsHeaderFragment = (AppSettingsHeaderFragment) getSupportFragmentManager().findFragmentById(
|
||||
R.id.api_app_settings_fragment);
|
||||
@@ -67,8 +68,7 @@ public class RemoteRegisterActivity extends BaseActivity {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// Allow
|
||||
|
||||
providerHelper.insertApiApp(mAppSettingsHeaderFragment.getAppSettings());
|
||||
apiDao.insertApiApp(mAppSettingsHeaderFragment.getAppSettings());
|
||||
|
||||
// give data through for new service call
|
||||
Intent resultData = extras.getParcelable(EXTRA_DATA);
|
||||
|
||||
+5
-4
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.remote.ui;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -36,9 +37,9 @@ import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.widget.FixedListView;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -49,7 +50,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
|
||||
public static final String ARG_DATA = "data";
|
||||
|
||||
private SelectKeyCursorAdapter mAdapter;
|
||||
private ProviderHelper mProviderHelper;
|
||||
private ApiDataAccessObject mApiDao;
|
||||
|
||||
private Uri mDataUri;
|
||||
|
||||
@@ -72,7 +73,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mProviderHelper = new ProviderHelper(getActivity());
|
||||
mApiDao = new ApiDataAccessObject(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,7 +117,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
|
||||
|
||||
Uri allowedKeysUri = mDataUri.buildUpon().appendPath(KeychainContract.PATH_ALLOWED_KEYS).build();
|
||||
Log.d(Constants.TAG, "allowedKeysUri: " + allowedKeysUri);
|
||||
mProviderHelper.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId);
|
||||
mApiDao.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId);
|
||||
|
||||
resultData.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, masterKeyId);
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CardException extends IOException {
|
||||
private short mResponseCode;
|
||||
|
||||
public CardException(String detailMessage, short responseCode) {
|
||||
super(detailMessage);
|
||||
mResponseCode = responseCode;
|
||||
}
|
||||
|
||||
public short getResponseCode() {
|
||||
return mResponseCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
|
||||
public enum KeyType {
|
||||
SIGN(0, 0xB6, 0xCE, 0xC7),
|
||||
ENCRYPT(1, 0xB8, 0xCF, 0xC8),
|
||||
AUTH(2, 0xA4, 0xD0, 0xC9),;
|
||||
|
||||
private final int mIdx;
|
||||
private final int mSlot;
|
||||
private final int mTimestampObjectId;
|
||||
private final int mFingerprintObjectId;
|
||||
|
||||
KeyType(final int idx, final int slot, final int timestampObjectId, final int fingerprintObjectId) {
|
||||
this.mIdx = idx;
|
||||
this.mSlot = slot;
|
||||
this.mTimestampObjectId = timestampObjectId;
|
||||
this.mFingerprintObjectId = fingerprintObjectId;
|
||||
}
|
||||
|
||||
public static KeyType from(final CanonicalizedSecretKey key) {
|
||||
if (key.canSign() || key.canCertify()) {
|
||||
return SIGN;
|
||||
} else if (key.canEncrypt()) {
|
||||
return ENCRYPT;
|
||||
} else if (key.canAuthenticate()) {
|
||||
return AUTH;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getIdx() {
|
||||
return mIdx;
|
||||
}
|
||||
|
||||
public int getmSlot() {
|
||||
return mSlot;
|
||||
}
|
||||
|
||||
public int getTimestampObjectId() {
|
||||
return mTimestampObjectId;
|
||||
}
|
||||
|
||||
public int getmFingerprintObjectId() {
|
||||
return mFingerprintObjectId;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import android.nfc.Tag;
|
||||
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nordpol.IsoCard;
|
||||
import nordpol.android.AndroidCard;
|
||||
|
||||
public class NfcTransport implements Transport {
|
||||
// timeout is set to 100 seconds to avoid cancellation during calculation
|
||||
private static final int TIMEOUT = 100 * 1000;
|
||||
private final Tag mTag;
|
||||
private IsoCard mIsoCard;
|
||||
|
||||
public NfcTransport(Tag tag) {
|
||||
this.mTag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit and receive data
|
||||
* @param data data to transmit
|
||||
* @return received data
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public byte[] transceive(final byte[] data) throws IOException {
|
||||
return mIsoCard.transceive(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect and release connection
|
||||
*/
|
||||
@Override
|
||||
public void release() {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return mIsoCard != null && mIsoCard.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Transport supports persistent connections e.g connections which can
|
||||
* handle multiple operations in one session
|
||||
* @return true if transport supports persistent connections
|
||||
*/
|
||||
@Override
|
||||
public boolean isPersistentConnectionAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to NFC device.
|
||||
* <p/>
|
||||
* On general communication, see also
|
||||
* http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx
|
||||
* <p/>
|
||||
* References to pages are generally related to the OpenPGP Application
|
||||
* on ISO SmartCard Systems specification.
|
||||
*/
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
mIsoCard = AndroidCard.get(mTag);
|
||||
if (mIsoCard == null) {
|
||||
throw new BaseSecurityTokenActivity.IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)");
|
||||
}
|
||||
|
||||
mIsoCard.setTimeout(TIMEOUT);
|
||||
mIsoCard.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final NfcTransport that = (NfcTransport) o;
|
||||
|
||||
if (mTag != null ? !mTag.equals(that.mTag) : that.mTag != null) return false;
|
||||
if (mIsoCard != null ? !mIsoCard.equals(that.mIsoCard) : that.mIsoCard != null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = mTag != null ? mTag.hashCode() : 0;
|
||||
result = 31 * result + (mIsoCard != null ? mIsoCard.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+756
@@ -0,0 +1,756 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
* Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
* Copyright (C) 2013-2014 Signe Rüsch
|
||||
* Copyright (C) 2013-2014 Philipp Jakubeit
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.util.Iso7816TLV;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.interfaces.RSAPrivateCrtKey;
|
||||
|
||||
import nordpol.Apdu;
|
||||
|
||||
/**
|
||||
* This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
|
||||
* devices.
|
||||
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
|
||||
*/
|
||||
public class SecurityTokenHelper {
|
||||
// Fidesmo constants
|
||||
private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
|
||||
|
||||
private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
private Transport mTransport;
|
||||
|
||||
private Passphrase mPin;
|
||||
private Passphrase mAdminPin;
|
||||
private boolean mPw1ValidForMultipleSignatures;
|
||||
private boolean mPw1ValidatedForSignature;
|
||||
private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
|
||||
private boolean mPw3Validated;
|
||||
|
||||
protected SecurityTokenHelper() {
|
||||
}
|
||||
|
||||
public static SecurityTokenHelper getInstance() {
|
||||
return LazyHolder.SECURITY_TOKEN_HELPER;
|
||||
}
|
||||
|
||||
private static String getHex(byte[] raw) {
|
||||
return new String(Hex.encode(raw));
|
||||
}
|
||||
|
||||
private String getHolderName(String name) {
|
||||
try {
|
||||
String slength;
|
||||
int ilength;
|
||||
name = name.substring(6);
|
||||
slength = name.substring(0, 2);
|
||||
ilength = Integer.parseInt(slength, 16) * 2;
|
||||
name = name.substring(2, ilength + 2);
|
||||
name = (new String(Hex.decode(name))).replace('<', ' ');
|
||||
return name;
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
// try-catch for https://github.com/FluffyKaon/OpenPGP-Card
|
||||
// Note: This should not happen, but happens with
|
||||
// https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now!
|
||||
|
||||
Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public Passphrase getPin() {
|
||||
return mPin;
|
||||
}
|
||||
|
||||
public void setPin(final Passphrase pin) {
|
||||
this.mPin = pin;
|
||||
}
|
||||
|
||||
public Passphrase getAdminPin() {
|
||||
return mAdminPin;
|
||||
}
|
||||
|
||||
public void setAdminPin(final Passphrase adminPin) {
|
||||
this.mAdminPin = adminPin;
|
||||
}
|
||||
|
||||
public void changeKey(CanonicalizedSecretKey secretKey, Passphrase passphrase) throws IOException {
|
||||
long keyGenerationTimestamp = secretKey.getCreationTime().getTime() / 1000;
|
||||
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
|
||||
KeyType keyType = KeyType.from(secretKey);
|
||||
|
||||
if (keyType == null) {
|
||||
throw new IOException("Inappropriate key flags for smart card key.");
|
||||
}
|
||||
|
||||
// Slot is empty, or contains this key already. PUT KEY operation is safe
|
||||
boolean canPutKey = isSlotEmpty(keyType)
|
||||
|| keyMatchesFingerPrint(keyType, secretKey.getFingerprint());
|
||||
|
||||
if (!canPutKey) {
|
||||
throw new IOException(String.format("Key slot occupied; card must be reset to put new %s key.",
|
||||
keyType.toString()));
|
||||
}
|
||||
|
||||
putKey(keyType.getmSlot(), secretKey, passphrase);
|
||||
putData(keyType.getmFingerprintObjectId(), secretKey.getFingerprint());
|
||||
putData(keyType.getTimestampObjectId(), timestampBytes);
|
||||
}
|
||||
|
||||
private boolean isSlotEmpty(KeyType keyType) throws IOException {
|
||||
// Note: special case: This should not happen, but happens with
|
||||
// https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
|
||||
if (getKeyFingerprint(keyType) == null) return true;
|
||||
|
||||
return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT);
|
||||
}
|
||||
|
||||
public boolean keyMatchesFingerPrint(KeyType keyType, byte[] fingerprint) throws IOException {
|
||||
return java.util.Arrays.equals(getKeyFingerprint(keyType), fingerprint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to device and select pgp applet
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void connectToDevice() throws IOException {
|
||||
// Connect on transport layer
|
||||
mTransport.connect();
|
||||
|
||||
// Connect on smartcard layer
|
||||
|
||||
// SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
|
||||
// See specification, page 51
|
||||
String accepted = "9000";
|
||||
|
||||
// Command APDU (page 51) for SELECT FILE command (page 29)
|
||||
String opening =
|
||||
"00" // CLA
|
||||
+ "A4" // INS
|
||||
+ "04" // P1
|
||||
+ "00" // P2
|
||||
+ "06" // Lc (number of bytes)
|
||||
+ "D27600012401" // Data (6 bytes)
|
||||
+ "00"; // Le
|
||||
String response = communicate(opening); // activate connection
|
||||
if (!response.endsWith(accepted)) {
|
||||
throw new CardException("Initialization failed!", parseCardStatus(response));
|
||||
}
|
||||
|
||||
byte[] pwStatusBytes = getPwStatusBytes();
|
||||
mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
|
||||
mPw1ValidatedForSignature = false;
|
||||
mPw1ValidatedForDecrypt = false;
|
||||
mPw3Validated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out the status word from a JavaCard response string.
|
||||
*
|
||||
* @param response A hex string with the response from the card
|
||||
* @return A short indicating the SW1/SW2, or 0 if a status could not be determined.
|
||||
*/
|
||||
private short parseCardStatus(String response) {
|
||||
if (response.length() < 4) {
|
||||
return 0; // invalid input
|
||||
}
|
||||
|
||||
try {
|
||||
return Short.parseShort(response.substring(response.length() - 4), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
|
||||
* conformance to the token's requirements for key length.
|
||||
*
|
||||
* @param pw For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83.
|
||||
* @param newPin The new PW1 or PW3.
|
||||
*/
|
||||
public void modifyPin(int pw, byte[] newPin) throws IOException {
|
||||
final int MAX_PW1_LENGTH_INDEX = 1;
|
||||
final int MAX_PW3_LENGTH_INDEX = 3;
|
||||
|
||||
byte[] pwStatusBytes = getPwStatusBytes();
|
||||
|
||||
if (pw == 0x81) {
|
||||
if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) {
|
||||
throw new IOException("Invalid PIN length");
|
||||
}
|
||||
} else if (pw == 0x83) {
|
||||
if (newPin.length < 8 || newPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) {
|
||||
throw new IOException("Invalid PIN length");
|
||||
}
|
||||
} else {
|
||||
throw new IOException("Invalid PW index for modify PIN operation");
|
||||
}
|
||||
|
||||
byte[] pin;
|
||||
if (pw == 0x83) {
|
||||
pin = mAdminPin.toStringUnsafe().getBytes();
|
||||
} else {
|
||||
pin = mPin.toStringUnsafe().getBytes();
|
||||
}
|
||||
|
||||
// Command APDU for CHANGE REFERENCE DATA command (page 32)
|
||||
String changeReferenceDataApdu = "00" // CLA
|
||||
+ "24" // INS
|
||||
+ "00" // P1
|
||||
+ String.format("%02x", pw) // P2
|
||||
+ String.format("%02x", pin.length + newPin.length) // Lc
|
||||
+ getHex(pin)
|
||||
+ getHex(newPin);
|
||||
String response = communicate(changeReferenceDataApdu); // change PIN
|
||||
if (!response.equals("9000")) {
|
||||
throw new CardException("Failed to change PIN", parseCardStatus(response));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call DECIPHER command
|
||||
*
|
||||
* @param encryptedSessionKey the encoded session key
|
||||
* @return the decoded session key
|
||||
*/
|
||||
public byte[] decryptSessionKey(byte[] encryptedSessionKey) throws IOException {
|
||||
if (!mPw1ValidatedForDecrypt) {
|
||||
verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
|
||||
}
|
||||
|
||||
String firstApdu = "102a8086fe";
|
||||
String secondApdu = "002a808603";
|
||||
String le = "00";
|
||||
|
||||
byte[] one = new byte[254];
|
||||
// leave out first byte:
|
||||
System.arraycopy(encryptedSessionKey, 1, one, 0, one.length);
|
||||
|
||||
byte[] two = new byte[encryptedSessionKey.length - 1 - one.length];
|
||||
for (int i = 0; i < two.length; i++) {
|
||||
two[i] = encryptedSessionKey[i + one.length + 1];
|
||||
}
|
||||
|
||||
communicate(firstApdu + getHex(one));
|
||||
String second = communicate(secondApdu + getHex(two) + le);
|
||||
|
||||
String decryptedSessionKey = getDataField(second);
|
||||
|
||||
return Hex.decode(decryptedSessionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the user's PW1 or PW3 with the appropriate mode.
|
||||
*
|
||||
* @param mode For PW1, this is 0x81 for signing, 0x82 for everything else.
|
||||
* For PW3 (Admin PIN), mode is 0x83.
|
||||
*/
|
||||
private void verifyPin(int mode) throws IOException {
|
||||
if (mPin != null || mode == 0x83) {
|
||||
|
||||
byte[] pin;
|
||||
if (mode == 0x83) {
|
||||
pin = mAdminPin.toStringUnsafe().getBytes();
|
||||
} else {
|
||||
pin = mPin.toStringUnsafe().getBytes();
|
||||
}
|
||||
|
||||
// SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
|
||||
// See specification, page 51
|
||||
String accepted = "9000";
|
||||
String response = tryPin(mode, pin); // login
|
||||
if (!response.equals(accepted)) {
|
||||
throw new CardException("Bad PIN!", parseCardStatus(response));
|
||||
}
|
||||
|
||||
if (mode == 0x81) {
|
||||
mPw1ValidatedForSignature = true;
|
||||
} else if (mode == 0x82) {
|
||||
mPw1ValidatedForDecrypt = true;
|
||||
} else if (mode == 0x83) {
|
||||
mPw3Validated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a data object on the token. Automatically validates the proper PIN for the operation.
|
||||
* Supported for all data objects < 255 bytes in length. Only the cardholder certificate
|
||||
* (0x7F21) can exceed this length.
|
||||
*
|
||||
* @param dataObject The data object to be stored.
|
||||
* @param data The data to store in the object
|
||||
*/
|
||||
private void putData(int dataObject, byte[] data) throws IOException {
|
||||
if (data.length > 254) {
|
||||
throw new IOException("Cannot PUT DATA with length > 254");
|
||||
}
|
||||
if (dataObject == 0x0101 || dataObject == 0x0103) {
|
||||
if (!mPw1ValidatedForDecrypt) {
|
||||
verifyPin(0x82); // (Verify PW1 for non-signing operations)
|
||||
}
|
||||
} else if (!mPw3Validated) {
|
||||
verifyPin(0x83); // (Verify PW3)
|
||||
}
|
||||
|
||||
String putDataApdu = "00" // CLA
|
||||
+ "DA" // INS
|
||||
+ String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
|
||||
+ String.format("%02x", dataObject & 0xFF) // P2
|
||||
+ String.format("%02x", data.length) // Lc
|
||||
+ getHex(data);
|
||||
|
||||
String response = communicate(putDataApdu); // put data
|
||||
if (!response.equals("9000")) {
|
||||
throw new CardException("Failed to put data.", parseCardStatus(response));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a key on the token in the given slot.
|
||||
*
|
||||
* @param slot The slot on the token where the key should be stored:
|
||||
* 0xB6: Signature Key
|
||||
* 0xB8: Decipherment Key
|
||||
* 0xA4: Authentication Key
|
||||
*/
|
||||
private void putKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
|
||||
throws IOException {
|
||||
if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
|
||||
throw new IOException("Invalid key slot");
|
||||
}
|
||||
|
||||
RSAPrivateCrtKey crtSecretKey;
|
||||
try {
|
||||
secretKey.unlock(passphrase);
|
||||
crtSecretKey = secretKey.getCrtSecretKey();
|
||||
} catch (PgpGeneralException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
|
||||
// Shouldn't happen; the UI should block the user from getting an incompatible key this far.
|
||||
if (crtSecretKey.getModulus().bitLength() > 2048) {
|
||||
throw new IOException("Key too large to export to Security Token.");
|
||||
}
|
||||
|
||||
// Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537.
|
||||
if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) {
|
||||
throw new IOException("Invalid public exponent for smart Security Token.");
|
||||
}
|
||||
|
||||
if (!mPw3Validated) {
|
||||
verifyPin(0x83); // (Verify PW3 with mode 83)
|
||||
}
|
||||
|
||||
byte[] header = Hex.decode(
|
||||
"4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23)
|
||||
+ String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length
|
||||
+ "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex)
|
||||
+ "9103" // Public modulus, length 3
|
||||
+ "928180" // Prime P, length 128
|
||||
+ "938180" // Prime Q, length 128
|
||||
+ "948180" // Coefficient (1/q mod p), length 128
|
||||
+ "958180" // Prime exponent P (d mod (p - 1)), length 128
|
||||
+ "968180" // Prime exponent Q (d mod (1 - 1)), length 128
|
||||
+ "97820100" // Modulus, length 256, last item in private key template
|
||||
+ "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow
|
||||
byte[] dataToSend = new byte[934];
|
||||
byte[] currentKeyObject;
|
||||
int offset = 0;
|
||||
|
||||
System.arraycopy(header, 0, dataToSend, offset, header.length);
|
||||
offset += header.length;
|
||||
currentKeyObject = crtSecretKey.getPublicExponent().toByteArray();
|
||||
System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3);
|
||||
offset += 3;
|
||||
// NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0
|
||||
// in the array to represent sign, so we take care to set the offset to 1 if necessary.
|
||||
currentKeyObject = crtSecretKey.getPrimeP().toByteArray();
|
||||
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
|
||||
Arrays.fill(currentKeyObject, (byte) 0);
|
||||
offset += 128;
|
||||
currentKeyObject = crtSecretKey.getPrimeQ().toByteArray();
|
||||
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
|
||||
Arrays.fill(currentKeyObject, (byte) 0);
|
||||
offset += 128;
|
||||
currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray();
|
||||
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
|
||||
Arrays.fill(currentKeyObject, (byte) 0);
|
||||
offset += 128;
|
||||
currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray();
|
||||
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
|
||||
Arrays.fill(currentKeyObject, (byte) 0);
|
||||
offset += 128;
|
||||
currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray();
|
||||
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
|
||||
Arrays.fill(currentKeyObject, (byte) 0);
|
||||
offset += 128;
|
||||
currentKeyObject = crtSecretKey.getModulus().toByteArray();
|
||||
System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256);
|
||||
|
||||
String putKeyCommand = "10DB3FFF";
|
||||
String lastPutKeyCommand = "00DB3FFF";
|
||||
|
||||
// Now we're ready to communicate with the token.
|
||||
offset = 0;
|
||||
String response;
|
||||
while (offset < dataToSend.length) {
|
||||
int dataRemaining = dataToSend.length - offset;
|
||||
if (dataRemaining > 254) {
|
||||
response = communicate(
|
||||
putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254)
|
||||
);
|
||||
offset += 254;
|
||||
} else {
|
||||
int length = dataToSend.length - offset;
|
||||
response = communicate(
|
||||
lastPutKeyCommand + String.format("%02x", length)
|
||||
+ Hex.toHexString(dataToSend, offset, length));
|
||||
offset += length;
|
||||
}
|
||||
|
||||
if (!response.endsWith("9000")) {
|
||||
throw new CardException("Key export to Security Token failed", parseCardStatus(response));
|
||||
}
|
||||
}
|
||||
|
||||
// Clear array with secret data before we return.
|
||||
Arrays.fill(dataToSend, (byte) 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return fingerprints of all keys from application specific data stored
|
||||
* on tag, or null if data not available.
|
||||
*
|
||||
* @return The fingerprints of all subkeys in a contiguous byte array.
|
||||
*/
|
||||
public byte[] getFingerprints() throws IOException {
|
||||
String data = "00CA006E00";
|
||||
byte[] buf = mTransport.transceive(Hex.decode(data));
|
||||
|
||||
Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
|
||||
Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
|
||||
|
||||
Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
|
||||
if (fptlv == null) {
|
||||
return null;
|
||||
}
|
||||
return fptlv.mV;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the PW Status Bytes from the token. This is a simple DO; no TLV decoding needed.
|
||||
*
|
||||
* @return Seven bytes in fixed format, plus 0x9000 status word at the end.
|
||||
*/
|
||||
private byte[] getPwStatusBytes() throws IOException {
|
||||
String data = "00CA00C400";
|
||||
return mTransport.transceive(Hex.decode(data));
|
||||
}
|
||||
|
||||
public byte[] getAid() throws IOException {
|
||||
String info = "00CA004F00";
|
||||
return mTransport.transceive(Hex.decode(info));
|
||||
}
|
||||
|
||||
public String getUserId() throws IOException {
|
||||
String info = "00CA006500";
|
||||
return getHolderName(communicate(info));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value
|
||||
*
|
||||
* @param hash the hash for signing
|
||||
* @return a big integer representing the MPI for the given hash
|
||||
*/
|
||||
public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException {
|
||||
if (!mPw1ValidatedForSignature) {
|
||||
verifyPin(0x81); // (Verify PW1 with mode 81 for signing)
|
||||
}
|
||||
|
||||
// dsi, including Lc
|
||||
String dsi;
|
||||
|
||||
Log.i(Constants.TAG, "Hash: " + hashAlgo);
|
||||
switch (hashAlgo) {
|
||||
case HashAlgorithmTags.SHA1:
|
||||
if (hash.length != 20) {
|
||||
throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
|
||||
}
|
||||
dsi = "23" // Lc
|
||||
+ "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
|
||||
+ "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
|
||||
+ "0605" + "2B0E03021A" // OID of SHA1
|
||||
+ "0500" // TLV coding of ZERO
|
||||
+ "0414" + getHex(hash); // 0x14 are 20 hash bytes
|
||||
break;
|
||||
case HashAlgorithmTags.RIPEMD160:
|
||||
if (hash.length != 20) {
|
||||
throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
|
||||
}
|
||||
dsi = "233021300906052B2403020105000414" + getHex(hash);
|
||||
break;
|
||||
case HashAlgorithmTags.SHA224:
|
||||
if (hash.length != 28) {
|
||||
throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
|
||||
}
|
||||
dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
|
||||
break;
|
||||
case HashAlgorithmTags.SHA256:
|
||||
if (hash.length != 32) {
|
||||
throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
|
||||
}
|
||||
dsi = "333031300D060960864801650304020105000420" + getHex(hash);
|
||||
break;
|
||||
case HashAlgorithmTags.SHA384:
|
||||
if (hash.length != 48) {
|
||||
throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
|
||||
}
|
||||
dsi = "433041300D060960864801650304020205000430" + getHex(hash);
|
||||
break;
|
||||
case HashAlgorithmTags.SHA512:
|
||||
if (hash.length != 64) {
|
||||
throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
|
||||
}
|
||||
dsi = "533051300D060960864801650304020305000440" + getHex(hash);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Not supported hash algo!");
|
||||
}
|
||||
|
||||
// Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
|
||||
String apdu =
|
||||
"002A9E9A" // CLA, INS, P1, P2
|
||||
+ dsi // digital signature input
|
||||
+ "00"; // Le
|
||||
|
||||
String response = communicate(apdu);
|
||||
|
||||
if (response.length() < 4) {
|
||||
throw new CardException("Bad response", (short) 0);
|
||||
}
|
||||
// split up response into signature and status
|
||||
String status = response.substring(response.length() - 4);
|
||||
String signature = response.substring(0, response.length() - 4);
|
||||
|
||||
// while we are getting 0x61 status codes, retrieve more data
|
||||
while (status.substring(0, 2).equals("61")) {
|
||||
Log.d(Constants.TAG, "requesting more data, status " + status);
|
||||
// Send GET RESPONSE command
|
||||
response = communicate("00C00000" + status.substring(2));
|
||||
status = response.substring(response.length() - 4);
|
||||
signature += response.substring(0, response.length() - 4);
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "final response:" + status);
|
||||
|
||||
if (!mPw1ValidForMultipleSignatures) {
|
||||
mPw1ValidatedForSignature = false;
|
||||
}
|
||||
|
||||
if (!"9000".equals(status)) {
|
||||
throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
|
||||
}
|
||||
|
||||
// Make sure the signature we received is actually the expected number of bytes long!
|
||||
if (signature.length() != 256 && signature.length() != 512) {
|
||||
throw new IOException("Bad signature length! Expected 128 or 256 bytes, got " + signature.length() / 2);
|
||||
}
|
||||
|
||||
return Hex.decode(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transceive data via NFC encoded as Hex
|
||||
*/
|
||||
private String communicate(String apdu) throws IOException {
|
||||
return getHex(mTransport.transceive(Hex.decode(apdu)));
|
||||
}
|
||||
|
||||
public Transport getTransport() {
|
||||
return mTransport;
|
||||
}
|
||||
|
||||
public void setTransport(Transport mTransport) {
|
||||
this.mTransport = mTransport;
|
||||
}
|
||||
|
||||
public boolean isFidesmoToken() {
|
||||
if (isConnected()) { // Check if we can still talk to the card
|
||||
try {
|
||||
// By trying to select any apps that have the Fidesmo AID prefix we can
|
||||
// see if it is a Fidesmo device or not
|
||||
byte[] mSelectResponse = mTransport.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
|
||||
// Compare the status returned by our select with the OK status code
|
||||
return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Card communication failed!", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a key on the card in the given slot. If the slot is 0xB6 (the signature key),
|
||||
* this command also has the effect of resetting the digital signature counter.
|
||||
* NOTE: This does not set the key fingerprint data object! After calling this command, you
|
||||
* must construct a public key packet using the returned public key data objects, compute the
|
||||
* key fingerprint, and store it on the card using: putData(0xC8, key.getFingerprint())
|
||||
*
|
||||
* @param slot The slot on the card where the key should be generated:
|
||||
* 0xB6: Signature Key
|
||||
* 0xB8: Decipherment Key
|
||||
* 0xA4: Authentication Key
|
||||
* @return the public key data objects, in TLV format. For RSA this will be the public modulus
|
||||
* (0x81) and exponent (0x82). These may come out of order; proper TLV parsing is required.
|
||||
*/
|
||||
public byte[] generateKey(int slot) throws IOException {
|
||||
if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
|
||||
throw new IOException("Invalid key slot");
|
||||
}
|
||||
|
||||
if (!mPw3Validated) {
|
||||
verifyPin(0x83); // (Verify PW3 with mode 83)
|
||||
}
|
||||
|
||||
String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000";
|
||||
String getResponseApdu = "00C00000";
|
||||
|
||||
String first = communicate(generateKeyApdu);
|
||||
String second = communicate(getResponseApdu);
|
||||
|
||||
if (!second.endsWith("9000")) {
|
||||
throw new IOException("On-card key generation failed");
|
||||
}
|
||||
|
||||
String publicKeyData = getDataField(first) + getDataField(second);
|
||||
|
||||
Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData);
|
||||
|
||||
return Hex.decode(publicKeyData);
|
||||
}
|
||||
|
||||
private String getDataField(String output) {
|
||||
return output.substring(0, output.length() - 4);
|
||||
}
|
||||
|
||||
private String tryPin(int mode, byte[] pin) throws IOException {
|
||||
// Command APDU for VERIFY command (page 32)
|
||||
String login =
|
||||
"00" // CLA
|
||||
+ "20" // INS
|
||||
+ "00" // P1
|
||||
+ String.format("%02x", mode) // P2
|
||||
+ String.format("%02x", pin.length) // Lc
|
||||
+ Hex.toHexString(pin);
|
||||
|
||||
return communicate(login);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets security token, which deletes all keys and data objects.
|
||||
* This works by entering a wrong PIN and then Admin PIN 4 times respectively.
|
||||
* Afterwards, the token is reactivated.
|
||||
*/
|
||||
public void resetAndWipeToken() throws IOException {
|
||||
String accepted = "9000";
|
||||
|
||||
// try wrong PIN 4 times until counter goes to C0
|
||||
byte[] pin = "XXXXXX".getBytes();
|
||||
for (int i = 0; i <= 4; i++) {
|
||||
String response = tryPin(0x81, pin);
|
||||
if (response.equals(accepted)) { // Should NOT accept!
|
||||
throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
|
||||
}
|
||||
}
|
||||
|
||||
// try wrong Admin PIN 4 times until counter goes to C0
|
||||
byte[] adminPin = "XXXXXXXX".getBytes();
|
||||
for (int i = 0; i <= 4; i++) {
|
||||
String response = tryPin(0x83, adminPin);
|
||||
if (response.equals(accepted)) { // Should NOT accept!
|
||||
throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
|
||||
}
|
||||
}
|
||||
|
||||
// reactivate token!
|
||||
String reactivate1 = "00" + "e6" + "00" + "00";
|
||||
String reactivate2 = "00" + "44" + "00" + "00";
|
||||
String response1 = communicate(reactivate1);
|
||||
String response2 = communicate(reactivate2);
|
||||
if (!response1.equals(accepted) || !response2.equals(accepted)) {
|
||||
throw new CardException("Reactivating failed!", parseCardStatus(response1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fingerprint from application specific data stored on tag, or
|
||||
* null if it doesn't exist.
|
||||
*
|
||||
* @param keyType key type
|
||||
* @return The fingerprint of the requested key, or null if not found.
|
||||
*/
|
||||
public byte[] getKeyFingerprint(@NonNull KeyType keyType) throws IOException {
|
||||
byte[] data = getFingerprints();
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// return the master key fingerprint
|
||||
ByteBuffer fpbuf = ByteBuffer.wrap(data);
|
||||
byte[] fp = new byte[20];
|
||||
fpbuf.position(keyType.getIdx() * 20);
|
||||
fpbuf.get(fp, 0, 20);
|
||||
|
||||
return fp;
|
||||
}
|
||||
|
||||
public boolean isPersistentConnectionAllowed() {
|
||||
return mTransport != null && mTransport.isPersistentConnectionAllowed();
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return mTransport != null && mTransport.isConnected();
|
||||
}
|
||||
|
||||
private static class LazyHolder {
|
||||
private static final SecurityTokenHelper SECURITY_TOKEN_HELPER = new SecurityTokenHelper();
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Abstraction for transmitting APDU commands
|
||||
*/
|
||||
public interface Transport {
|
||||
/**
|
||||
* Transmit and receive data
|
||||
* @param data data to transmit
|
||||
* @return received data
|
||||
* @throws IOException
|
||||
*/
|
||||
byte[] transceive(byte[] data) throws IOException;
|
||||
|
||||
/**
|
||||
* Disconnect and release connection
|
||||
*/
|
||||
void release();
|
||||
|
||||
/**
|
||||
* Check if device is was connected to and still is connected
|
||||
* @return connection status
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
* Check if Transport supports persistent connections e.g connections which can
|
||||
* handle multiple operations in one session
|
||||
* @return true if transport supports persistent connections
|
||||
*/
|
||||
boolean isPersistentConnectionAllowed();
|
||||
|
||||
|
||||
/**
|
||||
* Connect to device
|
||||
* @throws IOException
|
||||
*/
|
||||
void connect() throws IOException;
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Based on USB CCID Specification rev. 1.1
|
||||
* http://www.usb.org/developers/docs/devclass_docs/DWG_Smart-Card_CCID_Rev110.pdf
|
||||
* Implements small subset of these features
|
||||
*/
|
||||
public class UsbTransport implements Transport {
|
||||
private static final int USB_CLASS_SMARTCARD = 11;
|
||||
private static final int TIMEOUT = 20 * 1000; // 20s
|
||||
|
||||
private final UsbManager mUsbManager;
|
||||
private final UsbDevice mUsbDevice;
|
||||
private UsbInterface mUsbInterface;
|
||||
private UsbEndpoint mBulkIn;
|
||||
private UsbEndpoint mBulkOut;
|
||||
private UsbDeviceConnection mConnection;
|
||||
private byte mCounter;
|
||||
|
||||
public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) {
|
||||
mUsbDevice = usbDevice;
|
||||
mUsbManager = usbManager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Manage ICC power, Yubikey requires to power on ICC
|
||||
* Spec: 6.1.1 PC_to_RDR_IccPowerOn; 6.1.2 PC_to_RDR_IccPowerOff
|
||||
*
|
||||
* @param on true to turn ICC on, false to turn it off
|
||||
* @throws UsbTransportException
|
||||
*/
|
||||
private void setIccPower(boolean on) throws UsbTransportException {
|
||||
final byte[] iccPowerCommand = {
|
||||
(byte) (on ? 0x62 : 0x63),
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
mCounter++,
|
||||
0x00,
|
||||
0x00, 0x00
|
||||
};
|
||||
|
||||
sendRaw(iccPowerCommand);
|
||||
byte[] bytes;
|
||||
do {
|
||||
bytes = receive();
|
||||
} while (isDataBlockNotReady(bytes));
|
||||
checkDataBlockResponse(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first class 11 (Chip/Smartcard) interface of the device
|
||||
*
|
||||
* @param device {@link UsbDevice} which will be searched
|
||||
* @return {@link UsbInterface} of smartcard or null if it doesn't exist
|
||||
*/
|
||||
@Nullable
|
||||
private static UsbInterface getSmartCardInterface(UsbDevice device) {
|
||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||
UsbInterface anInterface = device.getInterface(i);
|
||||
if (anInterface.getInterfaceClass() == USB_CLASS_SMARTCARD) {
|
||||
return anInterface;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device's bulk-in and bulk-out endpoints
|
||||
*
|
||||
* @param usbInterface usb device interface
|
||||
* @return pair of builk-in and bulk-out endpoints respectively
|
||||
*/
|
||||
@NonNull
|
||||
private static Pair<UsbEndpoint, UsbEndpoint> getIoEndpoints(final UsbInterface usbInterface) {
|
||||
UsbEndpoint bulkIn = null, bulkOut = null;
|
||||
for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
|
||||
final UsbEndpoint endpoint = usbInterface.getEndpoint(i);
|
||||
if (endpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||
bulkIn = endpoint;
|
||||
} else if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||
bulkOut = endpoint;
|
||||
}
|
||||
}
|
||||
return new Pair<>(bulkIn, bulkOut);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release interface and disconnect
|
||||
*/
|
||||
@Override
|
||||
public void release() {
|
||||
if (mConnection != null) {
|
||||
mConnection.releaseInterface(mUsbInterface);
|
||||
mConnection.close();
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "Usb transport disconnected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is was connected to and still is connected
|
||||
* @return true if device is connected
|
||||
*/
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return mConnection != null && mUsbManager.getDeviceList().containsValue(mUsbDevice) &&
|
||||
mConnection.getSerial() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Transport supports persistent connections e.g connections which can
|
||||
* handle multiple operations in one session
|
||||
* @return true if transport supports persistent connections
|
||||
*/
|
||||
@Override
|
||||
public boolean isPersistentConnectionAllowed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to OTG device
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
mCounter = 0;
|
||||
mUsbInterface = getSmartCardInterface(mUsbDevice);
|
||||
if (mUsbInterface == null) {
|
||||
// Shouldn't happen as we whitelist only class 11 devices
|
||||
throw new UsbTransportException("USB error - device doesn't have class 11 interface");
|
||||
}
|
||||
|
||||
final Pair<UsbEndpoint, UsbEndpoint> ioEndpoints = getIoEndpoints(mUsbInterface);
|
||||
mBulkIn = ioEndpoints.first;
|
||||
mBulkOut = ioEndpoints.second;
|
||||
|
||||
if (mBulkIn == null || mBulkOut == null) {
|
||||
throw new UsbTransportException("USB error - invalid class 11 interface");
|
||||
}
|
||||
|
||||
mConnection = mUsbManager.openDevice(mUsbDevice);
|
||||
if (mConnection == null) {
|
||||
throw new UsbTransportException("USB error - failed to connect to device");
|
||||
}
|
||||
|
||||
if (!mConnection.claimInterface(mUsbInterface, true)) {
|
||||
throw new UsbTransportException("USB error - failed to claim interface");
|
||||
}
|
||||
|
||||
setIccPower(true);
|
||||
Log.d(Constants.TAG, "Usb transport connected");
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit and receive data
|
||||
* @param data data to transmit
|
||||
* @return received data
|
||||
* @throws UsbTransportException
|
||||
*/
|
||||
@Override
|
||||
public byte[] transceive(byte[] data) throws UsbTransportException {
|
||||
sendXfrBlock(data);
|
||||
byte[] bytes;
|
||||
do {
|
||||
bytes = receive();
|
||||
} while (isDataBlockNotReady(bytes));
|
||||
|
||||
checkDataBlockResponse(bytes);
|
||||
// Discard header
|
||||
return Arrays.copyOfRange(bytes, 10, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits XfrBlock
|
||||
* 6.1.4 PC_to_RDR_XfrBlock
|
||||
* @param payload payload to transmit
|
||||
* @throws UsbTransportException
|
||||
*/
|
||||
private void sendXfrBlock(byte[] payload) throws UsbTransportException {
|
||||
int l = payload.length;
|
||||
byte[] data = Arrays.concatenate(new byte[]{
|
||||
0x6f,
|
||||
(byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24),
|
||||
0x00,
|
||||
mCounter++,
|
||||
0x00,
|
||||
0x00, 0x00},
|
||||
payload);
|
||||
|
||||
int send = 0;
|
||||
while (send < data.length) {
|
||||
final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send);
|
||||
sendRaw(Arrays.copyOfRange(data, send, send + len));
|
||||
send += len;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] receive() throws UsbTransportException {
|
||||
byte[] buffer = new byte[mBulkIn.getMaxPacketSize()];
|
||||
byte[] result = null;
|
||||
int readBytes = 0, totalBytes = 0;
|
||||
|
||||
do {
|
||||
int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT);
|
||||
if (res < 0) {
|
||||
throw new UsbTransportException("USB error - failed to receive response " + res);
|
||||
}
|
||||
if (result == null) {
|
||||
if (res < 10) {
|
||||
throw new UsbTransportException("USB-CCID error - failed to receive CCID header");
|
||||
}
|
||||
totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10;
|
||||
result = new byte[totalBytes];
|
||||
}
|
||||
System.arraycopy(buffer, 0, result, readBytes, res);
|
||||
readBytes += res;
|
||||
} while (readBytes < totalBytes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void sendRaw(final byte[] data) throws UsbTransportException {
|
||||
final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT);
|
||||
if (tr1 != data.length) {
|
||||
throw new UsbTransportException("USB error - failed to transmit data " + tr1);
|
||||
}
|
||||
}
|
||||
|
||||
private byte getStatus(byte[] bytes) {
|
||||
return (byte) ((bytes[7] >> 6) & 0x03);
|
||||
}
|
||||
|
||||
private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException {
|
||||
final byte status = getStatus(bytes);
|
||||
if (status != 0) {
|
||||
throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDataBlockNotReady(byte[] bytes) {
|
||||
return getStatus(bytes) == 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
final UsbTransport that = (UsbTransport) o;
|
||||
|
||||
return mUsbDevice != null ? mUsbDevice.equals(that.mUsbDevice) : that.mUsbDevice == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mUsbDevice != null ? mUsbDevice.hashCode() : 0;
|
||||
}
|
||||
|
||||
public UsbDevice getUsbDevice() {
|
||||
return mUsbDevice;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.securitytoken;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class UsbTransportException extends IOException {
|
||||
public UsbTransportException() {
|
||||
}
|
||||
|
||||
public UsbTransportException(final String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public UsbTransportException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public UsbTransportException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
package org.sufficientlysecure.keychain.service;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
public class ChangeUnlockParcel implements Parcelable {
|
||||
|
||||
// the master key id of keyring.
|
||||
public Long mMasterKeyId;
|
||||
// the key fingerprint, for safety.
|
||||
public byte[] mFingerprint;
|
||||
// The new passphrase to use
|
||||
public final Passphrase mNewPassphrase;
|
||||
|
||||
public ChangeUnlockParcel(Passphrase newPassphrase) {
|
||||
mNewPassphrase = newPassphrase;
|
||||
}
|
||||
|
||||
public ChangeUnlockParcel(Long masterKeyId, byte[] fingerprint, Passphrase newPassphrase) {
|
||||
if (newPassphrase == null) {
|
||||
throw new AssertionError("newPassphrase must be non-null. THIS IS A BUG!");
|
||||
}
|
||||
|
||||
mMasterKeyId = masterKeyId;
|
||||
mFingerprint = fingerprint;
|
||||
mNewPassphrase = newPassphrase;
|
||||
}
|
||||
|
||||
public ChangeUnlockParcel(Parcel source) {
|
||||
mMasterKeyId = source.readInt() != 0 ? source.readLong() : null;
|
||||
mFingerprint = source.createByteArray();
|
||||
mNewPassphrase = source.readParcelable(Passphrase.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel destination, int flags) {
|
||||
destination.writeInt(mMasterKeyId == null ? 0 : 1);
|
||||
if (mMasterKeyId != null) {
|
||||
destination.writeLong(mMasterKeyId);
|
||||
}
|
||||
destination.writeByteArray(mFingerprint);
|
||||
destination.writeParcelable(mNewPassphrase, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<ChangeUnlockParcel> CREATOR = new Creator<ChangeUnlockParcel>() {
|
||||
public ChangeUnlockParcel createFromParcel(final Parcel source) {
|
||||
return new ChangeUnlockParcel(source);
|
||||
}
|
||||
|
||||
public ChangeUnlockParcel[] newArray(final int size) {
|
||||
return new ChangeUnlockParcel[size];
|
||||
}
|
||||
};
|
||||
|
||||
public String toString() {
|
||||
String out = "mMasterKeyId: " + mMasterKeyId + "\n";
|
||||
out += "passphrase (" + mNewPassphrase + ")";
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -143,6 +143,14 @@ public class ContactSyncAdapterService extends Service {
|
||||
}
|
||||
|
||||
public static void requestContactsSync() {
|
||||
// if user has disabled automatic sync, do nothing
|
||||
boolean isSyncEnabled = ContentResolver.getSyncAutomatically(new Account
|
||||
(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE), ContactsContract.AUTHORITY);
|
||||
|
||||
if (!isSyncEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
// no need to wait, do it immediately
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
|
||||
|
||||
+3
@@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.operations.BackupOperation;
|
||||
import org.sufficientlysecure.keychain.operations.ImportOperation;
|
||||
import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation;
|
||||
import org.sufficientlysecure.keychain.operations.InputDataOperation;
|
||||
import org.sufficientlysecure.keychain.operations.ChangeUnlockOperation;
|
||||
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
|
||||
import org.sufficientlysecure.keychain.operations.RevokeOperation;
|
||||
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
|
||||
@@ -116,6 +117,8 @@ public class KeychainService extends Service implements Progressable {
|
||||
op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
|
||||
} else if (inputParcel instanceof SaveKeyringParcel) {
|
||||
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
|
||||
} else if (inputParcel instanceof ChangeUnlockParcel) {
|
||||
op = new ChangeUnlockOperation(outerThis, new ProviderHelper(outerThis), outerThis);
|
||||
} else if (inputParcel instanceof RevokeKeyringParcel) {
|
||||
op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis);
|
||||
} else if (inputParcel instanceof CertifyActionsParcel) {
|
||||
|
||||
+47
-8
@@ -11,11 +11,14 @@ import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.PeriodicSync;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@@ -35,6 +38,7 @@ import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.receiver.NetworkReceiver;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
@@ -68,7 +72,7 @@ public class KeyserverSyncAdapterService extends Service {
|
||||
|
||||
private static final String ACTION_IGNORE_TOR = "ignore_tor";
|
||||
private static final String ACTION_UPDATE_ALL = "update_all";
|
||||
private static final String ACTION_SYNC_NOW = "sync_now";
|
||||
public static final String ACTION_SYNC_NOW = "sync_now";
|
||||
private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync";
|
||||
private static final String ACTION_START_ORBOT = "start_orbot";
|
||||
private static final String ACTION_CANCEL = "cancel";
|
||||
@@ -176,8 +180,25 @@ public class KeyserverSyncAdapterService extends Service {
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority,
|
||||
ContentProviderClient provider, SyncResult syncResult) {
|
||||
Log.d(Constants.TAG, "Performing a keyserver sync!");
|
||||
|
||||
Preferences prefs = Preferences.getPreferences(getContext());
|
||||
|
||||
// for a wifi-ONLY sync
|
||||
if (prefs.getWifiOnlySync()) {
|
||||
|
||||
ConnectivityManager connMgr = (ConnectivityManager)
|
||||
getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
|
||||
boolean isNotOnWifi = !(networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
|
||||
boolean isNotConnected = !(networkInfo.isConnected());
|
||||
|
||||
// if Wi-Fi connection doesn't exist then receiver is enabled
|
||||
if (isNotOnWifi && isNotConnected) {
|
||||
new NetworkReceiver().setWifiReceiverComponent(true, getContext());
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.d(Constants.TAG, "Performing a keyserver sync!");
|
||||
PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this
|
||||
.getSystemService(Context.POWER_SERVICE);
|
||||
@SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20
|
||||
@@ -509,6 +530,10 @@ public class KeyserverSyncAdapterService extends Service {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new sync if one does not exist, or updates an existing sync if the sync interval
|
||||
* has changed.
|
||||
*/
|
||||
public static void enableKeyserverSync(Context context) {
|
||||
Account account = KeychainApplication.createAccountIfNecessary(context);
|
||||
|
||||
@@ -519,12 +544,26 @@ public class KeyserverSyncAdapterService extends Service {
|
||||
|
||||
ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1);
|
||||
ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, true);
|
||||
ContentResolver.addPeriodicSync(
|
||||
account,
|
||||
Constants.PROVIDER_AUTHORITY,
|
||||
new Bundle(),
|
||||
SYNC_INTERVAL
|
||||
);
|
||||
|
||||
boolean intervalChanged = false;
|
||||
boolean syncExists = Preferences.getKeyserverSyncEnabled(context);
|
||||
|
||||
if (syncExists) {
|
||||
long oldInterval = ContentResolver.getPeriodicSyncs(
|
||||
account, Constants.PROVIDER_AUTHORITY).get(0).period;
|
||||
if (oldInterval != SYNC_INTERVAL) {
|
||||
intervalChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!syncExists || intervalChanged) {
|
||||
ContentResolver.addPeriodicSync(
|
||||
account,
|
||||
Constants.PROVIDER_AUTHORITY,
|
||||
new Bundle(),
|
||||
SYNC_INTERVAL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSyncEnabled() {
|
||||
|
||||
+15
-60
@@ -49,8 +49,6 @@ public class SaveKeyringParcel implements Parcelable {
|
||||
// the key fingerprint, for safety. MUST be null for a new key.
|
||||
public byte[] mFingerprint;
|
||||
|
||||
public ChangeUnlockParcel mNewUnlock;
|
||||
|
||||
public ArrayList<String> mAddUserIds;
|
||||
public ArrayList<WrappedUserAttribute> mAddUserAttribute;
|
||||
public ArrayList<SubkeyAdd> mAddSubKeys;
|
||||
@@ -70,6 +68,9 @@ public class SaveKeyringParcel implements Parcelable {
|
||||
private boolean mUploadAtomic;
|
||||
private String mKeyserver;
|
||||
|
||||
// private because we have to set other details like key id
|
||||
private ChangeUnlockParcel mNewUnlock;
|
||||
|
||||
public SaveKeyringParcel() {
|
||||
reset();
|
||||
}
|
||||
@@ -102,6 +103,18 @@ public class SaveKeyringParcel implements Parcelable {
|
||||
mKeyserver = keysever;
|
||||
}
|
||||
|
||||
public void setNewUnlock(ChangeUnlockParcel parcel) {
|
||||
mNewUnlock = parcel;
|
||||
}
|
||||
|
||||
public ChangeUnlockParcel getChangeUnlockParcel() {
|
||||
if(mNewUnlock != null) {
|
||||
mNewUnlock.mMasterKeyId = mMasterKeyId;
|
||||
mNewUnlock.mFingerprint = mFingerprint;
|
||||
}
|
||||
return mNewUnlock;
|
||||
}
|
||||
|
||||
public boolean isUpload() {
|
||||
return mUpload;
|
||||
}
|
||||
@@ -344,64 +357,6 @@ public class SaveKeyringParcel implements Parcelable {
|
||||
// BRAINPOOL_P256, BRAINPOOL_P384, BRAINPOOL_P512
|
||||
}
|
||||
|
||||
/** This subclass contains information on how the passphrase should be changed.
|
||||
*
|
||||
* If no changes are to be made, this class should NOT be used!
|
||||
*
|
||||
* At this point, there must be *exactly one* non-null value here, which specifies the type
|
||||
* of unlocking mechanism to use.
|
||||
*
|
||||
*/
|
||||
public static class ChangeUnlockParcel implements Parcelable {
|
||||
|
||||
// The new passphrase to use
|
||||
public final Passphrase mNewPassphrase;
|
||||
// A new pin to use. Must only contain [0-9]+
|
||||
public final Passphrase mNewPin;
|
||||
|
||||
public ChangeUnlockParcel(Passphrase newPassphrase) {
|
||||
this(newPassphrase, null);
|
||||
}
|
||||
public ChangeUnlockParcel(Passphrase newPassphrase, Passphrase newPin) {
|
||||
if (newPassphrase == null && newPin == null) {
|
||||
throw new RuntimeException("Cannot set both passphrase and pin. THIS IS A BUG!");
|
||||
}
|
||||
mNewPassphrase = newPassphrase;
|
||||
mNewPin = newPin;
|
||||
}
|
||||
|
||||
public ChangeUnlockParcel(Parcel source) {
|
||||
mNewPassphrase = source.readParcelable(Passphrase.class.getClassLoader());
|
||||
mNewPin = source.readParcelable(Passphrase.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel destination, int flags) {
|
||||
destination.writeParcelable(mNewPassphrase, flags);
|
||||
destination.writeParcelable(mNewPin, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<ChangeUnlockParcel> CREATOR = new Creator<ChangeUnlockParcel>() {
|
||||
public ChangeUnlockParcel createFromParcel(final Parcel source) {
|
||||
return new ChangeUnlockParcel(source);
|
||||
}
|
||||
|
||||
public ChangeUnlockParcel[] newArray(final int size) {
|
||||
return new ChangeUnlockParcel[size];
|
||||
}
|
||||
};
|
||||
|
||||
public String toString() {
|
||||
return mNewPassphrase != null
|
||||
? ("passphrase (" + mNewPassphrase + ")")
|
||||
: ("pin (" + mNewPin + ")");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -17,18 +17,18 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.service.input;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.sufficientlysecure.keychain.util.ParcelableProxy;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This is a base class for the input of crypto operations.
|
||||
*/
|
||||
|
||||
+16
-16
@@ -14,8 +14,8 @@ import java.util.Date;
|
||||
public class RequiredInputParcel implements Parcelable {
|
||||
|
||||
public enum RequiredInputType {
|
||||
PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, NFC_SIGN, NFC_DECRYPT,
|
||||
NFC_MOVE_KEY_TO_CARD, NFC_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
|
||||
PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, SECURITY_TOKEN_SIGN, SECURITY_TOKEN_DECRYPT,
|
||||
SECURITY_TOKEN_MOVE_KEY_TO_CARD, SECURITY_TOKEN_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
|
||||
}
|
||||
|
||||
public Date mSignatureTime;
|
||||
@@ -89,22 +89,22 @@ public class RequiredInputParcel implements Parcelable {
|
||||
return new RequiredInputParcel(RequiredInputType.ENABLE_ORBOT, null, null, null, 0L, 0L);
|
||||
}
|
||||
|
||||
public static RequiredInputParcel createNfcSignOperation(
|
||||
public static RequiredInputParcel createSecurityTokenSignOperation(
|
||||
long masterKeyId, long subKeyId,
|
||||
byte[] inputHash, int signAlgo, Date signatureTime) {
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_SIGN,
|
||||
return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
|
||||
new byte[][] { inputHash }, new int[] { signAlgo },
|
||||
signatureTime, masterKeyId, subKeyId);
|
||||
}
|
||||
|
||||
public static RequiredInputParcel createNfcDecryptOperation(
|
||||
public static RequiredInputParcel createSecurityTokenDecryptOperation(
|
||||
long masterKeyId, long subKeyId, byte[] encryptedSessionKey) {
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_DECRYPT,
|
||||
return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_DECRYPT,
|
||||
new byte[][] { encryptedSessionKey }, null, null, masterKeyId, subKeyId);
|
||||
}
|
||||
|
||||
public static RequiredInputParcel createNfcReset() {
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_RESET_CARD,
|
||||
public static RequiredInputParcel createSecurityTokenReset() {
|
||||
return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_RESET_CARD,
|
||||
null, null, null, null, null);
|
||||
}
|
||||
|
||||
@@ -188,14 +188,14 @@ public class RequiredInputParcel implements Parcelable {
|
||||
}
|
||||
};
|
||||
|
||||
public static class NfcSignOperationsBuilder {
|
||||
public static class SecurityTokenSignOperationsBuilder {
|
||||
Date mSignatureTime;
|
||||
ArrayList<Integer> mSignAlgos = new ArrayList<>();
|
||||
ArrayList<byte[]> mInputHashes = new ArrayList<>();
|
||||
long mMasterKeyId;
|
||||
long mSubKeyId;
|
||||
|
||||
public NfcSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) {
|
||||
public SecurityTokenSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) {
|
||||
mSignatureTime = signatureTime;
|
||||
mMasterKeyId = masterKeyId;
|
||||
mSubKeyId = subKeyId;
|
||||
@@ -209,7 +209,7 @@ public class RequiredInputParcel implements Parcelable {
|
||||
signAlgos[i] = mSignAlgos.get(i);
|
||||
}
|
||||
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_SIGN,
|
||||
return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
|
||||
inputHashes, signAlgos, mSignatureTime, mMasterKeyId, mSubKeyId);
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ public class RequiredInputParcel implements Parcelable {
|
||||
if (!mSignatureTime.equals(input.mSignatureTime)) {
|
||||
throw new AssertionError("input times must match, this is a programming error!");
|
||||
}
|
||||
if (input.mType != RequiredInputType.NFC_SIGN) {
|
||||
if (input.mType != RequiredInputType.SECURITY_TOKEN_SIGN) {
|
||||
throw new AssertionError("operation types must match, this is a progrmming error!");
|
||||
}
|
||||
|
||||
@@ -238,13 +238,13 @@ public class RequiredInputParcel implements Parcelable {
|
||||
|
||||
}
|
||||
|
||||
public static class NfcKeyToCardOperationsBuilder {
|
||||
public static class SecurityTokenKeyToCardOperationsBuilder {
|
||||
ArrayList<byte[]> mSubkeysToExport = new ArrayList<>();
|
||||
Long mMasterKeyId;
|
||||
byte[] mPin;
|
||||
byte[] mAdminPin;
|
||||
|
||||
public NfcKeyToCardOperationsBuilder(Long masterKeyId) {
|
||||
public SecurityTokenKeyToCardOperationsBuilder(Long masterKeyId) {
|
||||
mMasterKeyId = masterKeyId;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ public class RequiredInputParcel implements Parcelable {
|
||||
ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0));
|
||||
|
||||
// We need to pass in a subkey here...
|
||||
return new RequiredInputParcel(RequiredInputType.NFC_MOVE_KEY_TO_CARD,
|
||||
return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD,
|
||||
inputData, null, null, mMasterKeyId, buf.getLong());
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ public class RequiredInputParcel implements Parcelable {
|
||||
if (!mMasterKeyId.equals(input.mMasterKeyId)) {
|
||||
throw new AssertionError("Master keys must match, this is a programming error!");
|
||||
}
|
||||
if (input.mType != RequiredInputType.NFC_MOVE_KEY_TO_CARD) {
|
||||
if (input.mType != RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD) {
|
||||
throw new AssertionError("Operation types must match, this is a programming error!");
|
||||
}
|
||||
|
||||
|
||||
+132
-75
@@ -18,14 +18,6 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
@@ -43,7 +35,7 @@ import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -53,12 +45,9 @@ import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.github.pinball83.maskededittext.MaskedEditText;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
@@ -73,6 +62,14 @@ import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringParcel, ExportResult>
|
||||
implements OnBackStackChangedListener {
|
||||
|
||||
@@ -100,7 +97,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
String mBackupCode;
|
||||
private boolean mExecuteBackupOperation;
|
||||
|
||||
private MaskedEditText mCodeEditText;
|
||||
private EditText[] mCodeEditText;
|
||||
|
||||
private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator;
|
||||
private Integer mBackStackLevel;
|
||||
@@ -152,8 +149,13 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
boolean newCheckedState = !item.isChecked();
|
||||
item.setChecked(newCheckedState);
|
||||
mDebugModeAcceptAnyCode = newCheckedState;
|
||||
if (newCheckedState) {
|
||||
mCodeEditText.setText("ABCD-EFGH-IJKL-MNOP-QRST-UVWX");
|
||||
if (newCheckedState && TextUtils.isEmpty(mCodeEditText[0].getText())) {
|
||||
mCodeEditText[0].setText("ABCD");
|
||||
mCodeEditText[1].setText("EFGH");
|
||||
mCodeEditText[2].setText("IJKL");
|
||||
mCodeEditText[3].setText("MNOP");
|
||||
mCodeEditText[4].setText("QRST");
|
||||
mCodeEditText[5].setText("UVWX");
|
||||
Notify.create(getActivity(), "Actual backup code is all 'A's", Style.WARN).show();
|
||||
}
|
||||
return true;
|
||||
@@ -177,11 +179,9 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
mTitleAnimator.setDisplayedChild(1, animate);
|
||||
mStatusAnimator.setDisplayedChild(1, animate);
|
||||
mCodeFieldsAnimator.setDisplayedChild(1, animate);
|
||||
// use non-breaking spaces to enlarge the empty EditText appropriately
|
||||
String empty = "\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" +
|
||||
"-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" +
|
||||
"-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0";
|
||||
mCodeEditText.setText(empty);
|
||||
for (EditText editText : mCodeEditText) {
|
||||
editText.setText("");
|
||||
}
|
||||
|
||||
pushBackStackEntry();
|
||||
|
||||
@@ -195,7 +195,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
hideKeyboard();
|
||||
|
||||
if (animate) {
|
||||
@ColorInt int black = mCodeEditText.getCurrentTextColor();
|
||||
@ColorInt int black = mCodeEditText[0].getCurrentTextColor();
|
||||
@ColorInt int red = getResources().getColor(R.color.android_red_dark);
|
||||
animateFlashText(mCodeEditText, black, red, false);
|
||||
}
|
||||
@@ -214,14 +214,18 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
|
||||
hideKeyboard();
|
||||
|
||||
mCodeEditText.setEnabled(false);
|
||||
for (EditText editText : mCodeEditText) {
|
||||
editText.setEnabled(false);
|
||||
}
|
||||
|
||||
@ColorInt int green = getResources().getColor(R.color.android_green_dark);
|
||||
if (animate) {
|
||||
@ColorInt int black = mCodeEditText.getCurrentTextColor();
|
||||
@ColorInt int black = mCodeEditText[0].getCurrentTextColor();
|
||||
animateFlashText(mCodeEditText, black, green, true);
|
||||
} else {
|
||||
mCodeEditText.setTextColor(green);
|
||||
for (TextView textView : mCodeEditText) {
|
||||
textView.setTextColor(green);
|
||||
}
|
||||
}
|
||||
|
||||
popBackStackNoAction();
|
||||
@@ -257,23 +261,39 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
mExportSecret = args.getBoolean(ARG_EXPORT_SECRET);
|
||||
mExecuteBackupOperation = args.getBoolean(ARG_EXECUTE_BACKUP_OPERATION, true);
|
||||
|
||||
// NOTE: order of these method calls matter, see setupAutomaticLinebreak()
|
||||
mCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code_input);
|
||||
mCodeEditText.setInputType(
|
||||
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
||||
setupAutomaticLinebreak(mCodeEditText);
|
||||
mCodeEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
mCodeEditText = new EditText[6];
|
||||
mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
|
||||
mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
|
||||
mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
|
||||
mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
|
||||
mCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
|
||||
mCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
|
||||
|
||||
{
|
||||
TextView[] codeDisplayText = new TextView[6];
|
||||
codeDisplayText[0] = (TextView) view.findViewById(R.id.backup_code_display_1);
|
||||
codeDisplayText[1] = (TextView) view.findViewById(R.id.backup_code_display_2);
|
||||
codeDisplayText[2] = (TextView) view.findViewById(R.id.backup_code_display_3);
|
||||
codeDisplayText[3] = (TextView) view.findViewById(R.id.backup_code_display_4);
|
||||
codeDisplayText[4] = (TextView) view.findViewById(R.id.backup_code_display_5);
|
||||
codeDisplayText[5] = (TextView) view.findViewById(R.id.backup_code_display_6);
|
||||
|
||||
// set backup code in code TextViews
|
||||
char[] backupCode = mBackupCode.toCharArray();
|
||||
for (int i = 0; i < codeDisplayText.length; i++) {
|
||||
codeDisplayText[i].setText(backupCode, i * 5, 4);
|
||||
}
|
||||
|
||||
// set background to null in TextViews - this will retain padding from EditText style!
|
||||
for (TextView textView : codeDisplayText) {
|
||||
// noinspection deprecation, setBackground(Drawable) is API level >=16
|
||||
textView.setBackgroundDrawable(null);
|
||||
}
|
||||
}
|
||||
|
||||
setupEditTextFocusNext(mCodeEditText);
|
||||
setupEditTextSuccessListener(mCodeEditText);
|
||||
|
||||
TextView codeDisplayText = (TextView) view.findViewById(R.id.backup_code_display);
|
||||
setupAutomaticLinebreak(codeDisplayText);
|
||||
|
||||
// set background to null in TextViews - this will retain padding from EditText style!
|
||||
// noinspection deprecation, setBackground(Drawable) is API level >=16
|
||||
codeDisplayText.setBackgroundDrawable(null);
|
||||
|
||||
codeDisplayText.setText(mBackupCode);
|
||||
|
||||
mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator);
|
||||
mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator);
|
||||
mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator);
|
||||
@@ -350,67 +370,76 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic line break with max 6 lines for smaller displays
|
||||
* <p/>
|
||||
* NOTE: I was not able to get this behaviour using XML!
|
||||
* Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307
|
||||
*/
|
||||
private void setupAutomaticLinebreak(TextView textview) {
|
||||
textview.setSingleLine(true);
|
||||
textview.setMaxLines(6);
|
||||
textview.setHorizontallyScrolling(false);
|
||||
}
|
||||
private void setupEditTextSuccessListener(final EditText[] backupCodes) {
|
||||
for (EditText backupCode : backupCodes) {
|
||||
|
||||
private void setupEditTextSuccessListener(final MaskedEditText backupCode) {
|
||||
backupCode.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
backupCode.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
|
||||
|| mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
|
||||
boolean partIsComplete = (backupCode.getText().toString().indexOf(' ') == -1)
|
||||
&& (backupCode.getText().toString().indexOf('\u00a0') == -1);
|
||||
if (!inInputState || !partIsComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkIfCodeIsCorrect(backupCode);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.length() > 4) {
|
||||
throw new AssertionError("max length of each field is 4!");
|
||||
}
|
||||
|
||||
boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
|
||||
|| mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
|
||||
boolean partIsComplete = s.length() == 4;
|
||||
if (!inInputState || !partIsComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkIfCodeIsCorrect();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIfCodeIsCorrect(EditText backupCode) {
|
||||
private void checkIfCodeIsCorrect() {
|
||||
|
||||
if (Constants.DEBUG && mDebugModeAcceptAnyCode) {
|
||||
switchState(BackupCodeState.STATE_OK, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (backupCode.toString().equals(mBackupCode)) {
|
||||
StringBuilder backupCodeInput = new StringBuilder(26);
|
||||
for (EditText editText : mCodeEditText) {
|
||||
if (editText.getText().length() < 4) {
|
||||
return;
|
||||
}
|
||||
backupCodeInput.append(editText.getText());
|
||||
backupCodeInput.append('-');
|
||||
}
|
||||
backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
|
||||
|
||||
// if they don't match, do nothing
|
||||
if (backupCodeInput.toString().equals(mBackupCode)) {
|
||||
switchState(BackupCodeState.STATE_OK, true);
|
||||
return;
|
||||
}
|
||||
|
||||
switchState(BackupCodeState.STATE_INPUT_ERROR, true);
|
||||
|
||||
}
|
||||
|
||||
private static void animateFlashText(
|
||||
final TextView textView, int color1, int color2, boolean staySecondColor) {
|
||||
final TextView[] textViews, int color1, int color2, boolean staySecondColor) {
|
||||
|
||||
ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2);
|
||||
anim.addUpdateListener(new AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
textView.setTextColor((Integer) animator.getAnimatedValue());
|
||||
for (TextView textView : textViews) {
|
||||
textView.setTextColor((Integer) animator.getAnimatedValue());
|
||||
}
|
||||
}
|
||||
});
|
||||
anim.setRepeatMode(ValueAnimator.REVERSE);
|
||||
@@ -421,6 +450,34 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
|
||||
|
||||
}
|
||||
|
||||
private static void setupEditTextFocusNext(final EditText[] backupCodes) {
|
||||
for (int i = 0; i < backupCodes.length - 1; i++) {
|
||||
|
||||
final int next = i + 1;
|
||||
|
||||
backupCodes[i].addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
boolean inserting = before < count;
|
||||
boolean cursorAtEnd = (start + count) == 4;
|
||||
|
||||
if (inserting && cursorAtEnd) {
|
||||
backupCodes[next].requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void pushBackStackEntry() {
|
||||
if (mBackStackLevel != null) {
|
||||
return;
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
public class CertifyKeyActivity extends BaseActivity {
|
||||
|
||||
public static final String EXTRA_RESULT = "operation_result";
|
||||
public static final String EXTRA_KEY_IDS = "extra_key_ids";
|
||||
// For sending masterKeyIds to MultiUserIdsFragment to display list of keys
|
||||
public static final String EXTRA_KEY_IDS = MultiUserIdsFragment.EXTRA_KEY_IDS ;
|
||||
public static final String EXTRA_CERTIFY_KEY_ID = "certify_key_id";
|
||||
|
||||
@Override
|
||||
|
||||
+8
-172
@@ -62,58 +62,26 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
public class CertifyKeyFragment
|
||||
extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult>
|
||||
implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
public static final String ARG_CHECK_STATES = "check_states";
|
||||
extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult> {
|
||||
|
||||
private CheckBox mUploadKeyCheckbox;
|
||||
ListView mUserIds;
|
||||
|
||||
private CertifyKeySpinner mCertifyKeySpinner;
|
||||
|
||||
private long[] mPubMasterKeyIds;
|
||||
|
||||
public static final String[] USER_IDS_PROJECTION = new String[]{
|
||||
UserPackets._ID,
|
||||
UserPackets.MASTER_KEY_ID,
|
||||
UserPackets.USER_ID,
|
||||
UserPackets.IS_PRIMARY,
|
||||
UserPackets.IS_REVOKED
|
||||
};
|
||||
private static final int INDEX_MASTER_KEY_ID = 1;
|
||||
private static final int INDEX_USER_ID = 2;
|
||||
@SuppressWarnings("unused")
|
||||
private static final int INDEX_IS_PRIMARY = 3;
|
||||
@SuppressWarnings("unused")
|
||||
private static final int INDEX_IS_REVOKED = 4;
|
||||
|
||||
private MultiUserIdsAdapter mUserIdsAdapter;
|
||||
private MultiUserIdsFragment mMultiUserIdsFragment;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(CertifyKeyActivity.EXTRA_KEY_IDS);
|
||||
if (mPubMasterKeyIds == null) {
|
||||
Log.e(Constants.TAG, "List of key ids to certify missing!");
|
||||
getActivity().finish();
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<Boolean> checkedStates;
|
||||
if (savedInstanceState != null) {
|
||||
checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES);
|
||||
// key spinner and the checkbox keep their own state
|
||||
} else {
|
||||
checkedStates = null;
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// preselect certify key id if given
|
||||
long certifyKeyId = getActivity().getIntent()
|
||||
.getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none);
|
||||
if (certifyKeyId != Constants.key.none) {
|
||||
try {
|
||||
CachedPublicKeyRing key = (new ProviderHelper(getActivity())).getCachedPublicKeyRing(certifyKeyId);
|
||||
CachedPublicKeyRing key = (new ProviderHelper(getActivity()))
|
||||
.getCachedPublicKeyRing(certifyKeyId);
|
||||
if (key.canCertify()) {
|
||||
mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId);
|
||||
}
|
||||
@@ -121,15 +89,8 @@ public class CertifyKeyFragment
|
||||
Log.e(Constants.TAG, "certify certify check failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates);
|
||||
mUserIds.setAdapter(mUserIdsAdapter);
|
||||
mUserIds.setDividerHeight(0);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
|
||||
OperationResult result = getActivity().getIntent().getParcelableExtra(CertifyKeyActivity.EXTRA_RESULT);
|
||||
if (result != null) {
|
||||
// display result from import
|
||||
@@ -137,22 +98,14 @@ public class CertifyKeyFragment
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates();
|
||||
// no proper parceling method available :(
|
||||
outState.putSerializable(ARG_CHECK_STATES, states);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.certify_key_fragment, null);
|
||||
|
||||
mCertifyKeySpinner = (CertifyKeySpinner) view.findViewById(R.id.certify_key_spinner);
|
||||
mUploadKeyCheckbox = (CheckBox) view.findViewById(R.id.sign_key_upload_checkbox);
|
||||
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
|
||||
mMultiUserIdsFragment = (MultiUserIdsFragment)
|
||||
getChildFragmentManager().findFragmentById(R.id.multi_user_ids_fragment);
|
||||
|
||||
// make certify image gray, like action icons
|
||||
ImageView vActionCertifyImage =
|
||||
@@ -183,128 +136,11 @@ public class CertifyKeyFragment
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
Uri uri = UserPackets.buildUserIdsUri();
|
||||
|
||||
String selection, ids[];
|
||||
{
|
||||
// generate placeholders and string selection args
|
||||
ids = new String[mPubMasterKeyIds.length];
|
||||
StringBuilder placeholders = new StringBuilder("?");
|
||||
for (int i = 0; i < mPubMasterKeyIds.length; i++) {
|
||||
ids[i] = Long.toString(mPubMasterKeyIds[i]);
|
||||
if (i != 0) {
|
||||
placeholders.append(",?");
|
||||
}
|
||||
}
|
||||
// put together selection string
|
||||
selection = UserPackets.IS_REVOKED + " = 0" + " AND "
|
||||
+ Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
|
||||
+ " IN (" + placeholders + ")";
|
||||
}
|
||||
|
||||
return new CursorLoader(getActivity(), uri,
|
||||
USER_IDS_PROJECTION, selection, ids,
|
||||
Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " ASC"
|
||||
+ ", " + Tables.USER_PACKETS + "." + UserPackets.USER_ID + " ASC"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
|
||||
MatrixCursor matrix = new MatrixCursor(new String[]{
|
||||
"_id", "user_data", "grouped"
|
||||
}) {
|
||||
@Override
|
||||
public byte[] getBlob(int column) {
|
||||
return super.getBlob(column);
|
||||
}
|
||||
};
|
||||
data.moveToFirst();
|
||||
|
||||
long lastMasterKeyId = 0;
|
||||
String lastName = "";
|
||||
ArrayList<String> uids = new ArrayList<>();
|
||||
|
||||
boolean header = true;
|
||||
|
||||
// Iterate over all rows
|
||||
while (!data.isAfterLast()) {
|
||||
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
|
||||
String userId = data.getString(INDEX_USER_ID);
|
||||
KeyRing.UserId pieces = KeyRing.splitUserId(userId);
|
||||
|
||||
// Two cases:
|
||||
|
||||
boolean grouped = masterKeyId == lastMasterKeyId;
|
||||
boolean subGrouped = data.isFirst() || grouped && lastName.equals(pieces.name);
|
||||
// Remember for next loop
|
||||
lastName = pieces.name;
|
||||
|
||||
Log.d(Constants.TAG, Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped"));
|
||||
|
||||
if (!subGrouped) {
|
||||
// 1. This name should NOT be grouped with the previous, so we flush the buffer
|
||||
|
||||
Parcel p = Parcel.obtain();
|
||||
p.writeStringList(uids);
|
||||
byte[] d = p.marshall();
|
||||
p.recycle();
|
||||
|
||||
matrix.addRow(new Object[]{
|
||||
lastMasterKeyId, d, header ? 1 : 0
|
||||
});
|
||||
// indicate that we have a header for this masterKeyId
|
||||
header = false;
|
||||
|
||||
// Now clear the buffer, and add the new user id, for the next round
|
||||
uids.clear();
|
||||
|
||||
}
|
||||
|
||||
// 2. This name should be grouped with the previous, just add to buffer
|
||||
uids.add(userId);
|
||||
lastMasterKeyId = masterKeyId;
|
||||
|
||||
// If this one wasn't grouped, the next one's gotta be a header
|
||||
if (!grouped) {
|
||||
header = true;
|
||||
}
|
||||
|
||||
// Regardless of the outcome, move to next entry
|
||||
data.moveToNext();
|
||||
|
||||
}
|
||||
|
||||
// If there is anything left in the buffer, flush it one last time
|
||||
if (!uids.isEmpty()) {
|
||||
|
||||
Parcel p = Parcel.obtain();
|
||||
p.writeStringList(uids);
|
||||
byte[] d = p.marshall();
|
||||
p.recycle();
|
||||
|
||||
matrix.addRow(new Object[]{
|
||||
lastMasterKeyId, d, header ? 1 : 0
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
mUserIdsAdapter.swapCursor(matrix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
mUserIdsAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CertifyActionsParcel createOperationInput() {
|
||||
|
||||
// Bail out if there is not at least one user id selected
|
||||
ArrayList<CertifyAction> certifyActions = mUserIdsAdapter.getSelectedCertifyActions();
|
||||
ArrayList<CertifyAction> certifyActions = mMultiUserIdsFragment.getSelectedCertifyActions();
|
||||
if (certifyActions.isEmpty()) {
|
||||
Notify.create(getActivity(), "No identities selected!",
|
||||
Notify.Style.ERROR).show();
|
||||
|
||||
+36
-28
@@ -21,6 +21,7 @@ import android.content.Intent;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
@@ -28,7 +29,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
@@ -36,7 +37,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
public class CreateKeyActivity extends BaseSecurityTokenActivity {
|
||||
|
||||
public static final String EXTRA_NAME = "name";
|
||||
public static final String EXTRA_EMAIL = "email";
|
||||
@@ -47,9 +48,9 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
public static final String EXTRA_SECURITY_TOKEN_PIN = "yubi_key_pin";
|
||||
public static final String EXTRA_SECURITY_TOKEN_ADMIN_PIN = "yubi_key_admin_pin";
|
||||
|
||||
public static final String EXTRA_NFC_USER_ID = "nfc_user_id";
|
||||
public static final String EXTRA_NFC_AID = "nfc_aid";
|
||||
public static final String EXTRA_NFC_FINGERPRINTS = "nfc_fingerprints";
|
||||
public static final String EXTRA_SECURITY_TOKEN_USER_ID = "nfc_user_id";
|
||||
public static final String EXTRA_SECURITY_TOKEN_AID = "nfc_aid";
|
||||
public static final String EXTRA_SECURITY_FINGERPRINTS = "nfc_fingerprints";
|
||||
|
||||
public static final String FRAGMENT_TAG = "currentFragment";
|
||||
|
||||
@@ -66,8 +67,8 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
|
||||
|
||||
byte[] mScannedFingerprints;
|
||||
byte[] mNfcAid;
|
||||
String mNfcUserId;
|
||||
byte[] mSecurityTokenAid;
|
||||
String mSecurityTokenUserId;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
@@ -77,7 +78,7 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
// NOTE: ACTION_NDEF_DISCOVERED and not ACTION_TAG_DISCOVERED like in BaseNfcActivity
|
||||
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
|
||||
|
||||
mTagDispatcher.interceptIntent(getIntent());
|
||||
mNfcTagDispatcher.interceptIntent(getIntent());
|
||||
|
||||
setTitle(R.string.title_manage_my_keys);
|
||||
|
||||
@@ -107,10 +108,10 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false);
|
||||
mCreateSecurityToken = intent.getBooleanExtra(EXTRA_CREATE_SECURITY_TOKEN, false);
|
||||
|
||||
if (intent.hasExtra(EXTRA_NFC_FINGERPRINTS)) {
|
||||
byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS);
|
||||
String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID);
|
||||
byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID);
|
||||
if (intent.hasExtra(EXTRA_SECURITY_FINGERPRINTS)) {
|
||||
byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_SECURITY_FINGERPRINTS);
|
||||
String nfcUserId = intent.getStringExtra(EXTRA_SECURITY_TOKEN_USER_ID);
|
||||
byte[] nfcAid = intent.getByteArrayExtra(EXTRA_SECURITY_TOKEN_AID);
|
||||
|
||||
if (containsKeys(nfcFingerprints)) {
|
||||
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
|
||||
@@ -143,24 +144,32 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doNfcInBackground() throws IOException {
|
||||
if (mCurrentFragment instanceof NfcListenerFragment) {
|
||||
((NfcListenerFragment) mCurrentFragment).doNfcInBackground();
|
||||
protected void doSecurityTokenInBackground() throws IOException {
|
||||
if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
|
||||
((SecurityTokenListenerFragment) mCurrentFragment).doSecurityTokenInBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
mScannedFingerprints = nfcGetFingerprints();
|
||||
mNfcAid = nfcGetAid();
|
||||
mNfcUserId = nfcGetUserId();
|
||||
mScannedFingerprints = mSecurityTokenHelper.getFingerprints();
|
||||
mSecurityTokenAid = mSecurityTokenHelper.getAid();
|
||||
mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNfcPostExecute() {
|
||||
if (mCurrentFragment instanceof NfcListenerFragment) {
|
||||
((NfcListenerFragment) mCurrentFragment).onNfcPostExecute();
|
||||
protected void onSecurityTokenPostExecute() {
|
||||
if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
|
||||
((SecurityTokenListenerFragment) mCurrentFragment).onSecurityTokenPostExecute();
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't want get back to wait activity mainly because it looks weird with otg token
|
||||
if (mCurrentFragment instanceof CreateSecurityTokenWaitFragment) {
|
||||
// hack from http://stackoverflow.com/a/11253987
|
||||
CreateSecurityTokenWaitFragment.sDisableFragmentAnimations = true;
|
||||
getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
CreateSecurityTokenWaitFragment.sDisableFragmentAnimations = false;
|
||||
}
|
||||
|
||||
if (containsKeys(mScannedFingerprints)) {
|
||||
try {
|
||||
long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints);
|
||||
@@ -169,15 +178,15 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
|
||||
Intent intent = new Intent(this, ViewKeyActivity.class);
|
||||
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mScannedFingerprints);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
|
||||
mScannedFingerprints, mNfcAid, mNfcUserId);
|
||||
mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId);
|
||||
loadFragment(frag, FragAction.TO_RIGHT);
|
||||
}
|
||||
} else {
|
||||
@@ -252,12 +261,11 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
|
||||
|
||||
// do it immediately!
|
||||
getSupportFragmentManager().executePendingTransactions();
|
||||
|
||||
}
|
||||
|
||||
interface NfcListenerFragment {
|
||||
void doNfcInBackground() throws IOException;
|
||||
void onNfcPostExecute();
|
||||
interface SecurityTokenListenerFragment {
|
||||
void doSecurityTokenInBackground() throws IOException;
|
||||
void onSecurityTokenPostExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+8
-27
@@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.ui.widget.EmailEditText;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CreateKeyEmailFragment extends Fragment {
|
||||
private CreateKeyActivity mCreateKeyActivity;
|
||||
@@ -52,10 +51,6 @@ public class CreateKeyEmailFragment extends Fragment {
|
||||
private ArrayList<EmailAdapter.ViewModel> mAdditionalEmailModels = new ArrayList<>();
|
||||
private EmailAdapter mEmailAdapter;
|
||||
|
||||
// NOTE: Do not use more complicated pattern like defined in android.util.Patterns.EMAIL_ADDRESS
|
||||
// EMAIL_ADDRESS fails for mails with umlauts for example
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\S]+@[\\S]+\\.[a-z]+$");
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
@@ -76,16 +71,15 @@ public class CreateKeyEmailFragment extends Fragment {
|
||||
* @return true if EditText is not empty
|
||||
*/
|
||||
private boolean isMainEmailValid(EditText editText) {
|
||||
boolean output = true;
|
||||
if (!checkEmail(editText.getText().toString(), false)) {
|
||||
if (editText.getText().length() == 0) {
|
||||
editText.setError(getString(R.string.create_key_empty));
|
||||
editText.requestFocus();
|
||||
output = false;
|
||||
} else {
|
||||
editText.setError(null);
|
||||
return false;
|
||||
} else if (!checkEmail(editText.getText().toString(), false)){
|
||||
return false;
|
||||
}
|
||||
|
||||
return output;
|
||||
editText.setError(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,10 +140,9 @@ public class CreateKeyEmailFragment extends Fragment {
|
||||
* @return
|
||||
*/
|
||||
private boolean checkEmail(String email, boolean additionalEmail) {
|
||||
// check for email format or if the user did any input
|
||||
if (!isEmailFormatValid(email)) {
|
||||
if (email.isEmpty()) {
|
||||
Notify.create(getActivity(),
|
||||
getString(R.string.create_key_email_invalid_email),
|
||||
getString(R.string.create_key_email_empty_email),
|
||||
Notify.LENGTH_LONG, Notify.Style.ERROR).show(CreateKeyEmailFragment.this);
|
||||
return false;
|
||||
}
|
||||
@@ -166,18 +159,6 @@ public class CreateKeyEmailFragment extends Fragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the email format
|
||||
* Uses the default Android Email Pattern
|
||||
*
|
||||
* @param email
|
||||
* @return
|
||||
*/
|
||||
private boolean isEmailFormatValid(String email) {
|
||||
// check for email format or if the user did any input
|
||||
return !(email.length() == 0 || !EMAIL_PATTERN.matcher(email).matches());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicated emails inside the additional email adapter.
|
||||
*
|
||||
|
||||
+47
-9
@@ -44,9 +44,9 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
||||
@@ -57,6 +57,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CreateKeyFinalFragment extends Fragment {
|
||||
|
||||
@@ -81,6 +82,10 @@ public class CreateKeyFinalFragment extends Fragment {
|
||||
private OperationResult mQueuedFinishResult;
|
||||
private EditKeyResult mQueuedDisplayResult;
|
||||
|
||||
// NOTE: Do not use more complicated pattern like defined in android.util.Patterns.EMAIL_ADDRESS
|
||||
// EMAIL_ADDRESS fails for mails with umlauts for example
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\S]+@[\\S]+\\.[a-z]+$");
|
||||
|
||||
public static CreateKeyFinalFragment newInstance() {
|
||||
CreateKeyFinalFragment frag = new CreateKeyFinalFragment();
|
||||
frag.setRetainInstance(true);
|
||||
@@ -106,7 +111,11 @@ public class CreateKeyFinalFragment extends Fragment {
|
||||
CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
|
||||
|
||||
// set values
|
||||
mNameEdit.setText(createKeyActivity.mName);
|
||||
if (createKeyActivity.mName != null) {
|
||||
mNameEdit.setText(createKeyActivity.mName);
|
||||
} else {
|
||||
mNameEdit.setText(getString(R.string.user_id_no_name));
|
||||
}
|
||||
if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
|
||||
String emailText = createKeyActivity.mEmail + ", ";
|
||||
Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator();
|
||||
@@ -122,6 +131,8 @@ public class CreateKeyFinalFragment extends Fragment {
|
||||
mEmailEdit.setText(createKeyActivity.mEmail);
|
||||
}
|
||||
|
||||
checkEmailValidity();
|
||||
|
||||
mCreateButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -278,18 +289,20 @@ public class CreateKeyFinalFragment extends Fragment {
|
||||
2048, null, KeyFlags.AUTHENTICATION, 0L));
|
||||
|
||||
// use empty passphrase
|
||||
saveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase(), null);
|
||||
saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(new Passphrase()));
|
||||
} else {
|
||||
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
|
||||
4096, null, KeyFlags.CERTIFY_OTHER, 0L));
|
||||
3072, null, KeyFlags.CERTIFY_OTHER, 0L));
|
||||
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
|
||||
4096, null, KeyFlags.SIGN_DATA, 0L));
|
||||
3072, null, KeyFlags.SIGN_DATA, 0L));
|
||||
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
|
||||
4096, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L));
|
||||
3072, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L));
|
||||
|
||||
saveKeyringParcel.mNewUnlock = createKeyActivity.mPassphrase != null
|
||||
? new ChangeUnlockParcel(createKeyActivity.mPassphrase, null)
|
||||
: null;
|
||||
if (createKeyActivity.mPassphrase != null) {
|
||||
saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(createKeyActivity.mPassphrase));
|
||||
} else {
|
||||
saveKeyringParcel.setNewUnlock(null);
|
||||
}
|
||||
}
|
||||
String userId = KeyRing.createUserId(
|
||||
new KeyRing.UserId(createKeyActivity.mName, createKeyActivity.mEmail, null)
|
||||
@@ -309,6 +322,31 @@ public class CreateKeyFinalFragment extends Fragment {
|
||||
return saveKeyringParcel;
|
||||
}
|
||||
|
||||
private void checkEmailValidity() {
|
||||
CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
|
||||
|
||||
boolean emailsValid = true;
|
||||
if (!EMAIL_PATTERN.matcher(createKeyActivity.mEmail).matches()) {
|
||||
emailsValid = false;
|
||||
}
|
||||
if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
|
||||
for (Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator(); it.hasNext(); ) {
|
||||
if (!EMAIL_PATTERN.matcher(it.next().toString()).matches()) {
|
||||
emailsValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!emailsValid) {
|
||||
mEmailEdit.setError(getString(R.string.create_key_final_email_valid_warning));
|
||||
mEmailEdit.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mNameEdit.requestFocus(); // Workaround to remove focus from email
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void createKey() {
|
||||
CreateKeyActivity activity = (CreateKeyActivity) getActivity();
|
||||
if (activity == null) {
|
||||
|
||||
+4
-29
@@ -18,13 +18,11 @@
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
||||
@@ -50,27 +48,6 @@ public class CreateKeyNameFragment extends Fragment {
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text of given EditText is not empty. If it is empty an error is
|
||||
* set and the EditText gets the focus.
|
||||
*
|
||||
* @param context
|
||||
* @param editText
|
||||
* @return true if EditText is not empty
|
||||
*/
|
||||
private static boolean isEditTextNotEmpty(Context context, EditText editText) {
|
||||
boolean output = true;
|
||||
if (editText.getText().length() == 0) {
|
||||
editText.setError(context.getString(R.string.create_key_empty));
|
||||
editText.requestFocus();
|
||||
output = false;
|
||||
} else {
|
||||
editText.setError(null);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.create_key_name_fragment, container, false);
|
||||
@@ -109,13 +86,11 @@ public class CreateKeyNameFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void nextClicked() {
|
||||
if (isEditTextNotEmpty(getActivity(), mNameEdit)) {
|
||||
// save state
|
||||
mCreateKeyActivity.mName = mNameEdit.getText().toString();
|
||||
// save state
|
||||
mCreateKeyActivity.mName = mNameEdit.getText().length() == 0 ? null : mNameEdit.getText().toString();
|
||||
|
||||
CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
|
||||
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
|
||||
}
|
||||
CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
|
||||
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-9
@@ -25,7 +25,6 @@ import java.util.ArrayList;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -43,7 +42,7 @@ import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.NfcListenerFragment;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.SecurityTokenListenerFragment;
|
||||
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
@@ -51,7 +50,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
public class CreateSecurityTokenImportResetFragment
|
||||
extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult>
|
||||
implements NfcListenerFragment {
|
||||
implements SecurityTokenListenerFragment {
|
||||
|
||||
private static final int REQUEST_CODE_RESET = 0x00005001;
|
||||
|
||||
@@ -231,7 +230,7 @@ public class CreateSecurityTokenImportResetFragment
|
||||
|
||||
public void resetCard() {
|
||||
Intent intent = new Intent(getActivity(), SecurityTokenOperationActivity.class);
|
||||
RequiredInputParcel resetP = RequiredInputParcel.createNfcReset();
|
||||
RequiredInputParcel resetP = RequiredInputParcel.createSecurityTokenReset();
|
||||
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, resetP);
|
||||
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, new CryptoInputParcel());
|
||||
startActivityForResult(intent, REQUEST_CODE_RESET);
|
||||
@@ -248,11 +247,11 @@ public class CreateSecurityTokenImportResetFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doNfcInBackground() throws IOException {
|
||||
public void doSecurityTokenInBackground() throws IOException {
|
||||
|
||||
mTokenFingerprints = mCreateKeyActivity.nfcGetFingerprints();
|
||||
mTokenAid = mCreateKeyActivity.nfcGetAid();
|
||||
mTokenUserId = mCreateKeyActivity.nfcGetUserId();
|
||||
mTokenFingerprints = mCreateKeyActivity.getSecurityTokenHelper().getFingerprints();
|
||||
mTokenAid = mCreateKeyActivity.getSecurityTokenHelper().getAid();
|
||||
mTokenUserId = mCreateKeyActivity.getSecurityTokenHelper().getUserId();
|
||||
|
||||
byte[] fp = new byte[20];
|
||||
ByteBuffer.wrap(fp).put(mTokenFingerprints, 0, 20);
|
||||
@@ -260,7 +259,7 @@ public class CreateSecurityTokenImportResetFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNfcPostExecute() {
|
||||
public void onSecurityTokenPostExecute() {
|
||||
|
||||
setData();
|
||||
|
||||
|
||||
+30
-4
@@ -17,22 +17,35 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
|
||||
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||
|
||||
public class CreateSecurityTokenWaitFragment extends Fragment {
|
||||
|
||||
public static boolean sDisableFragmentAnimations = false;
|
||||
|
||||
CreateKeyActivity mCreateKeyActivity;
|
||||
View mBackButton;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
if (this.getActivity() instanceof BaseSecurityTokenActivity) {
|
||||
((BaseSecurityTokenActivity) this.getActivity()).checkDeviceConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.create_security_token_wait_fragment, container, false);
|
||||
@@ -50,9 +63,22 @@ public class CreateSecurityTokenWaitFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
mCreateKeyActivity = (CreateKeyActivity) getActivity();
|
||||
}
|
||||
|
||||
/**
|
||||
* hack from http://stackoverflow.com/a/11253987
|
||||
*/
|
||||
@Override
|
||||
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
|
||||
if (sDisableFragmentAnimations) {
|
||||
Animation a = new Animation() {};
|
||||
a.setDuration(0);
|
||||
return a;
|
||||
}
|
||||
return super.onCreateAnimation(transit, enter, nextAnim);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+45
-57
@@ -35,6 +35,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
|
||||
@@ -49,8 +50,8 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter;
|
||||
@@ -128,7 +129,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.edit_key_fragment, null);
|
||||
View view = inflater.inflate(R.layout.edit_key_fragment, superContainer, false);
|
||||
|
||||
mUserIdsList = (ListView) view.findViewById(R.id.edit_key_user_ids);
|
||||
mSubkeysList = (ListView) view.findViewById(R.id.edit_key_keys);
|
||||
@@ -338,10 +339,8 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
|
||||
Bundle data = message.getData();
|
||||
|
||||
// cache new returned passphrase!
|
||||
mSaveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(
|
||||
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE),
|
||||
null
|
||||
);
|
||||
mSaveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(
|
||||
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -441,50 +440,45 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: {
|
||||
// TODO: enable later when Admin PIN handling is resolved
|
||||
Notify.create(getActivity(),
|
||||
"This feature will be available in an upcoming OpenKeychain version.",
|
||||
Notify.Style.WARN).show();
|
||||
break;
|
||||
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
|
||||
SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
|
||||
if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
|
||||
secretKeyType == SecretKeyType.GNU_DUMMY) {
|
||||
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
// Activity activity = EditKeyFragment.this.getActivity();
|
||||
// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
|
||||
// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
|
||||
// secretKeyType == SecretKeyType.GNU_DUMMY) {
|
||||
// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR)
|
||||
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
|
||||
// break;
|
||||
// }
|
||||
// int algorithm = mSubkeysAdapter.getAlgorithm(position);
|
||||
// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN
|
||||
// if (algorithm != 1 && algorithm != 2 && algorithm != 3) {
|
||||
// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR)
|
||||
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
|
||||
// break;
|
||||
// }
|
||||
// if (mSubkeysAdapter.getKeySize(position) != 2048) {
|
||||
// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR)
|
||||
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// SubkeyChange change;
|
||||
// change = mSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
// if (change == null) {
|
||||
// mSaveKeyringParcel.mChangeSubKeys.add(
|
||||
// new SubkeyChange(keyId, false, true)
|
||||
// );
|
||||
// break;
|
||||
// }
|
||||
// // toggle
|
||||
// change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
|
||||
// if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
|
||||
// // User had chosen to strip key, but now wants to divert it.
|
||||
// change.mDummyStrip = false;
|
||||
// }
|
||||
// break;
|
||||
int algorithm = mSubkeysAdapter.getAlgorithm(position);
|
||||
if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
|
||||
&& algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
|
||||
&& algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
|
||||
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
if (mSubkeysAdapter.getKeySize(position) != 2048) {
|
||||
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
SubkeyChange change;
|
||||
change = mSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
if (change == null) {
|
||||
mSaveKeyringParcel.mChangeSubKeys.add(
|
||||
new SubkeyChange(keyId, false, true)
|
||||
);
|
||||
break;
|
||||
}
|
||||
// toggle
|
||||
change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
|
||||
if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
|
||||
// User had chosen to strip key, but now wants to divert it.
|
||||
change.mDummyStrip = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
|
||||
@@ -562,15 +556,9 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
|
||||
}
|
||||
|
||||
private void addSubkey() {
|
||||
boolean willBeMasterKey;
|
||||
if (mSubkeysAdapter != null) {
|
||||
willBeMasterKey = mSubkeysAdapter.getCount() == 0 && mSubkeysAddedAdapter.getCount() == 0;
|
||||
} else {
|
||||
willBeMasterKey = mSubkeysAddedAdapter.getCount() == 0;
|
||||
}
|
||||
|
||||
// new subkey will never be a masterkey, as masterkey cannot be removed
|
||||
AddSubkeyDialogFragment addSubkeyDialogFragment =
|
||||
AddSubkeyDialogFragment.newInstance(willBeMasterKey);
|
||||
AddSubkeyDialogFragment.newInstance(false);
|
||||
addSubkeyDialogFragment
|
||||
.setOnAlgorithmSelectedListener(
|
||||
new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {
|
||||
|
||||
+4
-2
@@ -247,10 +247,10 @@ public class EncryptFilesFragment
|
||||
try {
|
||||
mFilesAdapter.add(inputUri);
|
||||
} catch (IOException e) {
|
||||
String fileName = FileHelper.getFilename(getActivity(), inputUri);
|
||||
Notify.create(getActivity(),
|
||||
getActivity().getString(R.string.error_file_added_already, FileHelper.getFilename(getActivity(), inputUri)),
|
||||
getActivity().getString(R.string.error_file_added_already, fileName),
|
||||
Notify.Style.ERROR).show(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove from pending input uris
|
||||
@@ -729,6 +729,8 @@ public class EncryptFilesFragment
|
||||
// make sure this is correct at this point
|
||||
mAfterEncryptAction = AfterEncryptAction.SAVE;
|
||||
cryptoOperation(new CryptoInputParcel(new Date()));
|
||||
} else if (resultCode == Activity.RESULT_CANCELED) {
|
||||
onCryptoOperationCancelled();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
+9
-3
@@ -21,6 +21,7 @@ import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import com.tokenautocomplete.TokenCompleteTextView;
|
||||
@@ -79,9 +80,6 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
|
||||
mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign);
|
||||
mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list);
|
||||
mEncryptKeyView.setThreshold(1); // Start working from first character
|
||||
// TODO: workaround for bug in TokenAutoComplete,
|
||||
// see https://github.com/open-keychain/open-keychain/issues/1636
|
||||
mEncryptKeyView.setDeletionStyle(TokenCompleteTextView.TokenDeleteStyle.ToString);
|
||||
|
||||
final ViewAnimator vSignatureIcon = (ViewAnimator) view.findViewById(R.id.result_signature_icon);
|
||||
mSignKeySpinner.setOnKeyChangedListener(new OnKeyChangedListener() {
|
||||
@@ -112,6 +110,14 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
|
||||
}
|
||||
});
|
||||
|
||||
ImageView addRecipientImgView = (ImageView) view.findViewById(R.id.add_recipient);
|
||||
addRecipientImgView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mEncryptKeyView.showAllKeys();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
+15
-5
@@ -131,8 +131,11 @@ public class ImportKeysActivity extends BaseActivity
|
||||
if (Intent.ACTION_VIEW.equals(action)) {
|
||||
if (FacebookKeyserver.isFacebookHost(dataUri)) {
|
||||
action = ACTION_IMPORT_KEY_FROM_FACEBOOK;
|
||||
} else if ("http".equals(scheme) || "https".equals(scheme)) {
|
||||
} else if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
|
||||
action = ACTION_SEARCH_KEYSERVER_FROM_URL;
|
||||
} else if ("openpgp4fpr".equalsIgnoreCase(scheme)) {
|
||||
action = ACTION_IMPORT_KEY_FROM_KEYSERVER;
|
||||
extras.putString(EXTRA_FINGERPRINT, dataUri.getSchemeSpecificPart());
|
||||
} else {
|
||||
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
|
||||
// delegate action to ACTION_IMPORT_KEY
|
||||
@@ -413,11 +416,18 @@ public class ImportKeysActivity extends BaseActivity
|
||||
intent.putExtra(ImportKeyResult.EXTRA_RESULT, result);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
} else if (result.isOkNew() || result.isOkUpdated()) {
|
||||
// User has successfully imported a key, hide first time dialog
|
||||
Preferences.getPreferences(this).setFirstTime(false);
|
||||
|
||||
result.createNotify(ImportKeysActivity.this)
|
||||
.show((ViewGroup) findViewById(R.id.import_snackbar));
|
||||
// Close activities opened for importing keys and go to the list of keys
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
result.createNotify(ImportKeysActivity.this)
|
||||
.show((ViewGroup) findViewById(R.id.import_snackbar));
|
||||
}
|
||||
}
|
||||
|
||||
// methods from CryptoOperationHelper.Callback
|
||||
|
||||
+97
-6
@@ -17,29 +17,46 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.ui.ImportKeysListFragment.BytesLoaderState;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class ImportKeysFileFragment extends Fragment {
|
||||
private ImportKeysActivity mImportActivity;
|
||||
private View mBrowse;
|
||||
private View mClipboardButton;
|
||||
|
||||
public static final int REQUEST_CODE_FILE = 0x00007003;
|
||||
private Uri mCurrentUri;
|
||||
|
||||
private static final int REQUEST_CODE_FILE = 0x00007003;
|
||||
private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
@@ -83,10 +100,10 @@ public class ImportKeysFileFragment extends Fragment {
|
||||
sendText = clipboardText.toString();
|
||||
sendText = PgpHelper.getPgpKeyContent(sendText);
|
||||
if (sendText == null) {
|
||||
Notify.create(mImportActivity, "Bad data!", Style.ERROR).show();
|
||||
Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
|
||||
return;
|
||||
}
|
||||
mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(sendText.getBytes(), null));
|
||||
mImportActivity.loadCallback(new BytesLoaderState(sendText.getBytes(), null));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -106,11 +123,12 @@ public class ImportKeysFileFragment extends Fragment {
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_FILE: {
|
||||
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
mCurrentUri = data.getData();
|
||||
|
||||
// load data
|
||||
mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(null, data.getData()));
|
||||
if (checkAndRequestReadPermission(mCurrentUri)) {
|
||||
startImportingKeys();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -121,4 +139,77 @@ public class ImportKeysFileFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void startImportingKeys() {
|
||||
boolean isEncrypted;
|
||||
try {
|
||||
isEncrypted = FileHelper.isEncryptedFile(mImportActivity, mCurrentUri);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Error opening file", e);
|
||||
|
||||
Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEncrypted) {
|
||||
Intent intent = new Intent(mImportActivity, DecryptActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setData(mCurrentUri);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
mImportActivity.loadCallback(new BytesLoaderState(null, mCurrentUri));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris.
|
||||
* <p/>
|
||||
* This method returns true on Android < 6, or if permission is already granted. It
|
||||
* requests the permission and returns false otherwise.
|
||||
* <p/>
|
||||
* see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
|
||||
*/
|
||||
private boolean checkAndRequestReadPermission(final Uri uri) {
|
||||
if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
REQUEST_PERMISSION_READ_EXTERNAL_STORAGE);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode,
|
||||
@NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
|
||||
if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean permissionWasGranted = grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (permissionWasGranted) {
|
||||
startImportingKeys();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.error_denied_storage_permission, Toast.LENGTH_LONG).show();
|
||||
getActivity().setResult(Activity.RESULT_CANCELED);
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-1
@@ -330,9 +330,16 @@ public class ImportKeysListFragment extends ListFragment implements
|
||||
}
|
||||
|
||||
public void loadNew(LoaderState loaderState) {
|
||||
|
||||
mLoaderState = loaderState;
|
||||
|
||||
if (mLoaderState instanceof BytesLoaderState) {
|
||||
BytesLoaderState ls = (BytesLoaderState) mLoaderState;
|
||||
|
||||
if ( ls.mDataUri != null && ! checkAndRequestReadPermission(ls.mDataUri)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
restartLoaders();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,11 +40,11 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||
import org.sufficientlysecure.keychain.util.FabContainer;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
public class MainActivity extends BaseSecurityTokenNfcActivity implements FabContainer, OnBackStackChangedListener {
|
||||
public class MainActivity extends BaseSecurityTokenActivity implements FabContainer, OnBackStackChangedListener {
|
||||
|
||||
static final int ID_KEYS = 1;
|
||||
static final int ID_ENCRYPT_DECRYPT = 2;
|
||||
@@ -90,8 +90,9 @@ public class MainActivity extends BaseSecurityTokenNfcActivity implements FabCon
|
||||
@Override
|
||||
public boolean onItemClick(View view, int position, IDrawerItem drawerItem) {
|
||||
if (drawerItem != null) {
|
||||
PrimaryDrawerItem item = (PrimaryDrawerItem) drawerItem;
|
||||
Intent intent = null;
|
||||
switch (drawerItem.getIdentifier()) {
|
||||
switch ((int) item.getIdentifier()) {
|
||||
case ID_KEYS:
|
||||
onKeysSelected();
|
||||
break;
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.MultiUserIdsAdapter;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MultiUserIdsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>{
|
||||
public static final String ARG_CHECK_STATES = "check_states";
|
||||
public static final String EXTRA_KEY_IDS = "extra_key_ids";
|
||||
private boolean checkboxVisibility = true;
|
||||
|
||||
ListView mUserIds;
|
||||
private MultiUserIdsAdapter mUserIdsAdapter;
|
||||
|
||||
private long[] mPubMasterKeyIds;
|
||||
|
||||
public static final String[] USER_IDS_PROJECTION = new String[]{
|
||||
KeychainContract.UserPackets._ID,
|
||||
KeychainContract.UserPackets.MASTER_KEY_ID,
|
||||
KeychainContract.UserPackets.USER_ID,
|
||||
KeychainContract.UserPackets.IS_PRIMARY,
|
||||
KeychainContract.UserPackets.IS_REVOKED
|
||||
};
|
||||
private static final int INDEX_MASTER_KEY_ID = 1;
|
||||
private static final int INDEX_USER_ID = 2;
|
||||
@SuppressWarnings("unused")
|
||||
private static final int INDEX_IS_PRIMARY = 3;
|
||||
@SuppressWarnings("unused")
|
||||
private static final int INDEX_IS_REVOKED = 4;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.multi_user_ids_fragment, null);
|
||||
|
||||
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(EXTRA_KEY_IDS);
|
||||
if (mPubMasterKeyIds == null) {
|
||||
Log.e(Constants.TAG, "List of key ids to certify missing!");
|
||||
getActivity().finish();
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<Boolean> checkedStates = null;
|
||||
if (savedInstanceState != null) {
|
||||
checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES);
|
||||
}
|
||||
|
||||
mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates, checkboxVisibility);
|
||||
mUserIds.setAdapter(mUserIdsAdapter);
|
||||
mUserIds.setDividerHeight(0);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates();
|
||||
// no proper parceling method available :(
|
||||
outState.putSerializable(ARG_CHECK_STATES, states);
|
||||
}
|
||||
|
||||
public ArrayList<CertifyActionsParcel.CertifyAction> getSelectedCertifyActions() {
|
||||
if (!checkboxVisibility) {
|
||||
throw new AssertionError("Item selection not allowed");
|
||||
}
|
||||
|
||||
return mUserIdsAdapter.getSelectedCertifyActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
Uri uri = KeychainContract.UserPackets.buildUserIdsUri();
|
||||
|
||||
String selection, ids[];
|
||||
{
|
||||
// generate placeholders and string selection args
|
||||
ids = new String[mPubMasterKeyIds.length];
|
||||
StringBuilder placeholders = new StringBuilder("?");
|
||||
for (int i = 0; i < mPubMasterKeyIds.length; i++) {
|
||||
ids[i] = Long.toString(mPubMasterKeyIds[i]);
|
||||
if (i != 0) {
|
||||
placeholders.append(",?");
|
||||
}
|
||||
}
|
||||
// put together selection string
|
||||
selection = KeychainContract.UserPackets.IS_REVOKED + " = 0" + " AND "
|
||||
+ KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID
|
||||
+ " IN (" + placeholders + ")";
|
||||
}
|
||||
|
||||
return new CursorLoader(getActivity(), uri,
|
||||
USER_IDS_PROJECTION, selection, ids,
|
||||
KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID + " ASC"
|
||||
+ ", " + KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.USER_ID + " ASC"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
|
||||
MatrixCursor matrix = new MatrixCursor(new String[]{
|
||||
"_id", "user_data", "grouped"
|
||||
}) {
|
||||
@Override
|
||||
public byte[] getBlob(int column) {
|
||||
return super.getBlob(column);
|
||||
}
|
||||
};
|
||||
data.moveToFirst();
|
||||
|
||||
long lastMasterKeyId = 0;
|
||||
String lastName = "";
|
||||
ArrayList<String> uids = new ArrayList<>();
|
||||
|
||||
boolean header = true;
|
||||
|
||||
// Iterate over all rows
|
||||
while (!data.isAfterLast()) {
|
||||
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
|
||||
String userId = data.getString(INDEX_USER_ID);
|
||||
KeyRing.UserId pieces = KeyRing.splitUserId(userId);
|
||||
|
||||
// Two cases:
|
||||
|
||||
boolean grouped = masterKeyId == lastMasterKeyId;
|
||||
boolean subGrouped = data.isFirst() || grouped && lastName.equals(pieces.name);
|
||||
// Remember for next loop
|
||||
lastName = pieces.name;
|
||||
|
||||
Log.d(Constants.TAG, Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped"));
|
||||
|
||||
if (!subGrouped) {
|
||||
// 1. This name should NOT be grouped with the previous, so we flush the buffer
|
||||
|
||||
Parcel p = Parcel.obtain();
|
||||
p.writeStringList(uids);
|
||||
byte[] d = p.marshall();
|
||||
p.recycle();
|
||||
|
||||
matrix.addRow(new Object[]{
|
||||
lastMasterKeyId, d, header ? 1 : 0
|
||||
});
|
||||
// indicate that we have a header for this masterKeyId
|
||||
header = false;
|
||||
|
||||
// Now clear the buffer, and add the new user id, for the next round
|
||||
uids.clear();
|
||||
|
||||
}
|
||||
|
||||
// 2. This name should be grouped with the previous, just add to buffer
|
||||
uids.add(userId);
|
||||
lastMasterKeyId = masterKeyId;
|
||||
|
||||
// If this one wasn't grouped, the next one's gotta be a header
|
||||
if (!grouped) {
|
||||
header = true;
|
||||
}
|
||||
|
||||
// Regardless of the outcome, move to next entry
|
||||
data.moveToNext();
|
||||
|
||||
}
|
||||
|
||||
// If there is anything left in the buffer, flush it one last time
|
||||
if (!uids.isEmpty()) {
|
||||
|
||||
Parcel p = Parcel.obtain();
|
||||
p.writeStringList(uids);
|
||||
byte[] d = p.marshall();
|
||||
p.recycle();
|
||||
|
||||
matrix.addRow(new Object[]{
|
||||
lastMasterKeyId, d, header ? 1 : 0
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
mUserIdsAdapter.swapCursor(matrix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
mUserIdsAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
public void setCheckboxVisibility(boolean checkboxVisibility) {
|
||||
this.checkboxVisibility = checkboxVisibility;
|
||||
}
|
||||
}
|
||||
+75
-43
@@ -46,8 +46,6 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import com.github.pinball83.maskededittext.MaskedEditText;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
@@ -60,7 +58,6 @@ import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
@@ -157,7 +154,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
|
||||
public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener {
|
||||
private EditText mPassphraseEditText;
|
||||
private TextView mPassphraseText;
|
||||
private MaskedEditText mBackupCodeEditText;
|
||||
private EditText[] mBackupCodeEditText;
|
||||
|
||||
private boolean mIsCancelled = false;
|
||||
private RequiredInputParcel mRequiredInput;
|
||||
@@ -184,13 +181,15 @@ public class PassphraseDialogActivity extends FragmentActivity {
|
||||
View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null);
|
||||
alert.setView(view);
|
||||
|
||||
mBackupCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code);
|
||||
// NOTE: order of these method calls matter, see setupAutomaticLinebreak()
|
||||
mBackupCodeEditText.setInputType(
|
||||
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
||||
setupAutomaticLinebreak(mBackupCodeEditText);
|
||||
mBackupCodeEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
|
||||
mBackupCodeEditText.setOnEditorActionListener(this);
|
||||
mBackupCodeEditText = new EditText[6];
|
||||
mBackupCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
|
||||
mBackupCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
|
||||
mBackupCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
|
||||
mBackupCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
|
||||
mBackupCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
|
||||
mBackupCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
|
||||
|
||||
setupEditTextFocusNext(mBackupCodeEditText);
|
||||
|
||||
AlertDialog dialog = alert.create();
|
||||
dialog.setButton(DialogInterface.BUTTON_POSITIVE,
|
||||
@@ -281,28 +280,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
|
||||
mPassphraseText.setText(message);
|
||||
mPassphraseEditText.setHint(hint);
|
||||
|
||||
// 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
|
||||
mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
mPassphraseEditText.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getActivity() == null || mPassphraseEditText == null) {
|
||||
return;
|
||||
}
|
||||
InputMethodManager imm = (InputMethodManager) getActivity()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
mPassphraseEditText.requestFocus();
|
||||
openKeyboard(mPassphraseEditText);
|
||||
|
||||
mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
|
||||
mPassphraseEditText.setOnEditorActionListener(this);
|
||||
@@ -325,17 +303,62 @@ public class PassphraseDialogActivity extends FragmentActivity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic line break with max 6 lines for smaller displays
|
||||
* <p/>
|
||||
* NOTE: I was not able to get this behaviour using XML!
|
||||
* Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307
|
||||
* 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
|
||||
*/
|
||||
private void setupAutomaticLinebreak(TextView textview) {
|
||||
textview.setSingleLine(true);
|
||||
textview.setMaxLines(6);
|
||||
textview.setHorizontallyScrolling(false);
|
||||
private void openKeyboard(final TextView textView) {
|
||||
textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
textView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getActivity() == null || textView == null) {
|
||||
return;
|
||||
}
|
||||
InputMethodManager imm = (InputMethodManager) getActivity()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(textView, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
textView.requestFocus();
|
||||
}
|
||||
|
||||
private static void setupEditTextFocusNext(final EditText[] backupCodes) {
|
||||
for (int i = 0; i < backupCodes.length - 1; i++) {
|
||||
|
||||
final int next = i + 1;
|
||||
|
||||
backupCodes[i].addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
boolean inserting = before < count;
|
||||
boolean cursorAtEnd = (start + count) == 4;
|
||||
|
||||
if (inserting && cursorAtEnd) {
|
||||
backupCodes[next].requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
@@ -347,8 +370,17 @@ public class PassphraseDialogActivity extends FragmentActivity {
|
||||
public void onClick(View v) {
|
||||
|
||||
if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) {
|
||||
Passphrase passphrase =
|
||||
new Passphrase(mBackupCodeEditText.getText().toString());
|
||||
StringBuilder backupCodeInput = new StringBuilder(26);
|
||||
for (EditText editText : mBackupCodeEditText) {
|
||||
if (editText.getText().length() < 4) {
|
||||
return;
|
||||
}
|
||||
backupCodeInput.append(editText.getText());
|
||||
backupCodeInput.append('-');
|
||||
}
|
||||
backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
|
||||
|
||||
Passphrase passphrase = new Passphrase(backupCodeInput.toString());
|
||||
finishCaching(passphrase);
|
||||
|
||||
return;
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
|
||||
public class RedirectImportKeysActivity extends BaseActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
startQrCodeCaptureActivity();
|
||||
}
|
||||
|
||||
private void startQrCodeCaptureActivity() {
|
||||
final Intent scanQrCode = new Intent(this, ImportKeysProxyActivity.class);
|
||||
scanQrCode.setAction(ImportKeysProxyActivity.ACTION_QR_CODE_API);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.redirect_import_key_title)
|
||||
.setMessage(R.string.redirect_import_key_message)
|
||||
.setPositiveButton(R.string.redirect_import_key_yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// directly scan with OpenKeychain
|
||||
startActivity(scanQrCode);
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.redirect_import_key_no, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// close window
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
+87
-93
@@ -3,6 +3,7 @@
|
||||
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
* Copyright (C) 2013-2014 Signe Rüsch
|
||||
* Copyright (C) 2013-2014 Philipp Jakubeit
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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
|
||||
@@ -35,10 +36,12 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.securitytoken.KeyType;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.OrientationUtils;
|
||||
@@ -55,7 +58,7 @@ import nordpol.android.NfcGuideView;
|
||||
* NFC devices.
|
||||
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
|
||||
*/
|
||||
public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity {
|
||||
public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity {
|
||||
|
||||
public static final String EXTRA_REQUIRED_INPUT = "required_input";
|
||||
public static final String EXTRA_CRYPTO_INPUT = "crypto_input";
|
||||
@@ -69,8 +72,6 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
|
||||
private RequiredInputParcel mRequiredInput;
|
||||
|
||||
private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
private CryptoInputParcel mInputParcel;
|
||||
|
||||
@Override
|
||||
@@ -137,9 +138,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
|
||||
private void obtainPassphraseIfRequired() {
|
||||
// obtain passphrase for this subkey
|
||||
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD
|
||||
&& mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_RESET_CARD) {
|
||||
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD
|
||||
&& mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_RESET_CARD) {
|
||||
obtainSecurityTokenPin(mRequiredInput);
|
||||
checkPinAvailability();
|
||||
} else {
|
||||
checkDeviceConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (REQUEST_CODE_PIN == requestCode) {
|
||||
checkPinAvailability();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkPinAvailability() {
|
||||
try {
|
||||
Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
|
||||
mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
|
||||
if (passphrase != null) {
|
||||
checkDeviceConnection();
|
||||
}
|
||||
} catch (PassphraseCacheService.KeyNotFoundException e) {
|
||||
throw new AssertionError(
|
||||
"tried to find passphrase for non-existing key. this is a programming error!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,39 +174,53 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNfcPreExecute() {
|
||||
public void onSecurityTokenPreExecute() {
|
||||
// start with indeterminate progress
|
||||
vAnimator.setDisplayedChild(1);
|
||||
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doNfcInBackground() throws IOException {
|
||||
protected void doSecurityTokenInBackground() throws IOException {
|
||||
|
||||
switch (mRequiredInput.mType) {
|
||||
case NFC_DECRYPT: {
|
||||
case SECURITY_TOKEN_DECRYPT: {
|
||||
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
|
||||
mSecurityTokenHelper.getKeyFingerprint(KeyType.ENCRYPT));
|
||||
|
||||
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
|
||||
throw new IOException(getString(R.string.error_wrong_security_token));
|
||||
}
|
||||
|
||||
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
|
||||
byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
|
||||
byte[] decryptedSessionKey = nfcDecryptSessionKey(encryptedSessionKey);
|
||||
byte[] decryptedSessionKey = mSecurityTokenHelper.decryptSessionKey(encryptedSessionKey);
|
||||
mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NFC_SIGN: {
|
||||
case SECURITY_TOKEN_SIGN: {
|
||||
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
|
||||
mSecurityTokenHelper.getKeyFingerprint(KeyType.SIGN));
|
||||
|
||||
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
|
||||
throw new IOException(getString(R.string.error_wrong_security_token));
|
||||
}
|
||||
|
||||
mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
|
||||
|
||||
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
|
||||
byte[] hash = mRequiredInput.mInputData[i];
|
||||
int algo = mRequiredInput.mSignAlgos[i];
|
||||
byte[] signedHash = nfcCalculateSignature(hash, algo);
|
||||
byte[] signedHash = mSecurityTokenHelper.calculateSignature(hash, algo);
|
||||
mInputParcel.addCryptoData(hash, signedHash);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NFC_MOVE_KEY_TO_CARD: {
|
||||
case SECURITY_TOKEN_MOVE_KEY_TO_CARD: {
|
||||
// TODO: assume PIN and Admin PIN to be default for this operation
|
||||
mPin = new Passphrase("123456");
|
||||
mAdminPin = new Passphrase("12345678");
|
||||
mSecurityTokenHelper.setPin(new Passphrase("123456"));
|
||||
mSecurityTokenHelper.setAdminPin(new Passphrase("12345678"));
|
||||
|
||||
ProviderHelper providerHelper = new ProviderHelper(this);
|
||||
CanonicalizedSecretKeyRing secretKeyRing;
|
||||
@@ -202,11 +241,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
long subkeyId = buf.getLong();
|
||||
|
||||
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
|
||||
|
||||
long keyGenerationTimestampMillis = key.getCreationTime().getTime();
|
||||
long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
|
||||
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
|
||||
byte[] tokenSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
|
||||
byte[] tokenSerialNumber = Arrays.copyOf(mSecurityTokenHelper.getAid(), 16);
|
||||
|
||||
Passphrase passphrase;
|
||||
try {
|
||||
@@ -216,46 +251,20 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
throw new IOException("Unable to get cached passphrase!");
|
||||
}
|
||||
|
||||
if (key.canSign() || key.canCertify()) {
|
||||
if (shouldPutKey(key.getFingerprint(), 0)) {
|
||||
nfcPutKey(0xB6, key, passphrase);
|
||||
nfcPutData(0xCE, timestampBytes);
|
||||
nfcPutData(0xC7, key.getFingerprint());
|
||||
} else {
|
||||
throw new IOException("Key slot occupied; token must be reset to put new signature key.");
|
||||
}
|
||||
} else if (key.canEncrypt()) {
|
||||
if (shouldPutKey(key.getFingerprint(), 1)) {
|
||||
nfcPutKey(0xB8, key, passphrase);
|
||||
nfcPutData(0xCF, timestampBytes);
|
||||
nfcPutData(0xC8, key.getFingerprint());
|
||||
} else {
|
||||
throw new IOException("Key slot occupied; token must be reset to put new decryption key.");
|
||||
}
|
||||
} else if (key.canAuthenticate()) {
|
||||
if (shouldPutKey(key.getFingerprint(), 2)) {
|
||||
nfcPutKey(0xA4, key, passphrase);
|
||||
nfcPutData(0xD0, timestampBytes);
|
||||
nfcPutData(0xC9, key.getFingerprint());
|
||||
} else {
|
||||
throw new IOException("Key slot occupied; token must be reset to put new authentication key.");
|
||||
}
|
||||
} else {
|
||||
throw new IOException("Inappropriate key flags for Security Token key.");
|
||||
}
|
||||
mSecurityTokenHelper.changeKey(key, passphrase);
|
||||
|
||||
// TODO: Is this really used anywhere?
|
||||
mInputParcel.addCryptoData(subkeyBytes, tokenSerialNumber);
|
||||
}
|
||||
|
||||
// change PINs afterwards
|
||||
nfcModifyPIN(0x81, newPin);
|
||||
nfcModifyPIN(0x83, newAdminPin);
|
||||
mSecurityTokenHelper.modifyPin(0x81, newPin);
|
||||
mSecurityTokenHelper.modifyPin(0x83, newAdminPin);
|
||||
|
||||
break;
|
||||
}
|
||||
case NFC_RESET_CARD: {
|
||||
nfcResetCard();
|
||||
case SECURITY_TOKEN_RESET_CARD: {
|
||||
mSecurityTokenHelper.resetAndWipeToken();
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -267,7 +276,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void onNfcPostExecute() {
|
||||
protected final void onSecurityTokenPostExecute() {
|
||||
handleResult(mInputParcel);
|
||||
|
||||
// show finish
|
||||
@@ -275,28 +284,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
|
||||
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
// check all 200ms if Security Token has been taken away
|
||||
while (true) {
|
||||
if (isNfcConnected()) {
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ignored) {
|
||||
if (mSecurityTokenHelper.isPersistentConnectionAllowed()) {
|
||||
// Just close
|
||||
finish();
|
||||
} else {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
// check all 200ms if Security Token has been taken away
|
||||
while (true) {
|
||||
if (isSecurityTokenConnected()) {
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
super.onPostExecute(result);
|
||||
finish();
|
||||
}
|
||||
}.execute();
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
super.onPostExecute(result);
|
||||
finish();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,7 +325,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNfcError(String error) {
|
||||
protected void onSecurityTokenError(String error) {
|
||||
pauseTagHandling();
|
||||
|
||||
vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text));
|
||||
@@ -321,31 +335,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNfcPinError(String error) {
|
||||
onNfcError(error);
|
||||
public void onSecurityTokenPinError(String error) {
|
||||
onSecurityTokenError(error);
|
||||
|
||||
// clear (invalid) passphrase
|
||||
PassphraseCacheService.clearCachedPassphrase(
|
||||
this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
|
||||
}
|
||||
|
||||
private boolean shouldPutKey(byte[] fingerprint, int idx) throws IOException {
|
||||
byte[] tokenFingerprint = nfcGetMasterKeyFingerprint(idx);
|
||||
|
||||
// Note: special case: This should not happen, but happens with
|
||||
// https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
|
||||
if (tokenFingerprint == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Slot is empty, or contains this key already. PUT KEY operation is safe
|
||||
if (Arrays.equals(tokenFingerprint, BLANK_FINGERPRINT) ||
|
||||
Arrays.equals(tokenFingerprint, fingerprint)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Slot already contains a different key; don't overwrite it.
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
-7
@@ -19,8 +19,6 @@
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.Manifest;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
@@ -49,6 +47,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.KeychainApplication;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
@@ -59,6 +58,8 @@ import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
|
||||
public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005;
|
||||
@@ -405,7 +406,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
}
|
||||
|
||||
/**
|
||||
* This fragment shows the keyserver/contacts sync preferences
|
||||
* This fragment shows the keyserver/wifi-only-sync/contacts sync preferences
|
||||
*/
|
||||
public static class SyncPrefsFragment extends PresetPreferenceFragment {
|
||||
|
||||
@@ -422,8 +423,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
super.onResume();
|
||||
// this needs to be done in onResume since the user can change sync values from Android
|
||||
// settings and we need to reflect that change when the user navigates back
|
||||
AccountManager manager = AccountManager.get(getActivity());
|
||||
final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0];
|
||||
final Account account = KeychainApplication.createAccountIfNecessary(getActivity());
|
||||
// for keyserver sync
|
||||
initializeSyncCheckBox(
|
||||
(SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
|
||||
@@ -441,8 +441,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
private void initializeSyncCheckBox(final SwitchPreference syncCheckBox,
|
||||
final Account account,
|
||||
final String authority) {
|
||||
boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority)
|
||||
&& checkContactsPermission(authority);
|
||||
// account is null if it could not be created for some reason
|
||||
boolean syncEnabled =
|
||||
account != null
|
||||
&& ContentResolver.getSyncAutomatically(account, authority)
|
||||
&& checkContactsPermission(authority);
|
||||
syncCheckBox.setChecked(syncEnabled);
|
||||
setSummary(syncCheckBox, authority, syncEnabled);
|
||||
|
||||
@@ -464,6 +467,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (account == null) {
|
||||
// if account could not be created for some reason,
|
||||
// we can't have our sync
|
||||
return false;
|
||||
}
|
||||
// disable syncs
|
||||
ContentResolver.setSyncAutomatically(account, authority, false);
|
||||
// immediately delete any linked contacts
|
||||
|
||||
+4
-3
@@ -40,6 +40,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback;
|
||||
@@ -312,19 +313,19 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC
|
||||
public void showAsSelectedKeyserver() {
|
||||
isSelectedKeyserver = true;
|
||||
selectedServerLabel.setVisibility(View.VISIBLE);
|
||||
outerLayout.setBackgroundColor(getResources().getColor(R.color.android_green_dark));
|
||||
outerLayout.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorPrimaryDark));
|
||||
}
|
||||
|
||||
public void showAsUnselectedKeyserver() {
|
||||
isSelectedKeyserver = false;
|
||||
selectedServerLabel.setVisibility(View.GONE);
|
||||
outerLayout.setBackgroundColor(Color.WHITE);
|
||||
outerLayout.setBackgroundColor(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemSelected() {
|
||||
selectedServerLabel.setVisibility(View.GONE);
|
||||
itemView.setBackgroundColor(Color.LTGRAY);
|
||||
itemView.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorBrightToolbar));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+7
-14
@@ -31,10 +31,7 @@ import android.widget.Spinner;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.UploadResult;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||
@@ -53,7 +50,6 @@ public class UploadKeyActivity extends BaseActivity
|
||||
|
||||
// CryptoOperationHelper.Callback vars
|
||||
private String mKeyserver;
|
||||
private long mMasterKeyId;
|
||||
private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper;
|
||||
|
||||
@Override
|
||||
@@ -63,6 +59,10 @@ public class UploadKeyActivity extends BaseActivity
|
||||
mUploadButton = findViewById(R.id.upload_key_action_upload);
|
||||
mKeyServerSpinner = (Spinner) findViewById(R.id.upload_key_keyserver);
|
||||
|
||||
MultiUserIdsFragment mMultiUserIdsFragment = (MultiUserIdsFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.multi_user_ids_fragment);
|
||||
mMultiUserIdsFragment.setCheckboxVisibility(false);
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
|
||||
.getKeyServers()
|
||||
@@ -89,15 +89,6 @@ public class UploadKeyActivity extends BaseActivity
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mMasterKeyId = new ProviderHelper(this).getCachedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingUri(mDataUri)).getMasterKeyId();
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
Log.e(Constants.TAG, "Intent data pointed to bad key!");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,7 +127,9 @@ public class UploadKeyActivity extends BaseActivity
|
||||
|
||||
@Override
|
||||
public UploadKeyringParcel createOperationInput() {
|
||||
return new UploadKeyringParcel(mKeyserver, mMasterKeyId);
|
||||
long[] masterKeyIds = getIntent().getLongArrayExtra(MultiUserIdsFragment.EXTRA_KEY_IDS);
|
||||
|
||||
return new UploadKeyringParcel(mKeyserver, masterKeyIds[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class UsbEventReceiverActivity extends Activity {
|
||||
public static final String ACTION_USB_PERMISSION =
|
||||
"org.sufficientlysecure.keychain.ui.USB_PERMISSION";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
|
||||
Log.d(Constants.TAG, "Requesting permission for " + usbDevice.getDeviceName());
|
||||
usbManager.requestPermission(usbDevice,
|
||||
PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Close the activity
|
||||
finish();
|
||||
}
|
||||
}
|
||||
+31
-30
@@ -32,6 +32,7 @@ import android.app.ActivityOptions;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.AsyncTask;
|
||||
@@ -80,11 +81,11 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
|
||||
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
|
||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
|
||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
@@ -102,7 +103,7 @@ import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
|
||||
public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
public class ViewKeyActivity extends BaseSecurityTokenActivity implements
|
||||
LoaderManager.LoaderCallbacks<Cursor>,
|
||||
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
|
||||
|
||||
@@ -129,8 +130,8 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
private String mKeyserver;
|
||||
private ArrayList<ParcelableKeyRing> mKeyList;
|
||||
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper;
|
||||
private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditOpHelper;
|
||||
private SaveKeyringParcel mSaveKeyringParcel;
|
||||
private CryptoOperationHelper<ChangeUnlockParcel, EditKeyResult> mEditOpHelper;
|
||||
private ChangeUnlockParcel mChangeUnlockParcel;
|
||||
|
||||
private TextView mStatusText;
|
||||
private ImageView mStatusImage;
|
||||
@@ -170,9 +171,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
private byte[] mFingerprint;
|
||||
private String mFingerprintString;
|
||||
|
||||
private byte[] mNfcFingerprints;
|
||||
private String mNfcUserId;
|
||||
private byte[] mNfcAid;
|
||||
private byte[] mSecurityTokenFingerprints;
|
||||
private String mSecurityTokenUserId;
|
||||
private byte[] mSecurityTokenAid;
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@Override
|
||||
@@ -428,13 +429,11 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
}
|
||||
|
||||
private void changePassword() {
|
||||
mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint);
|
||||
|
||||
CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback
|
||||
= new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() {
|
||||
CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult> editKeyCallback
|
||||
= new CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult>() {
|
||||
@Override
|
||||
public SaveKeyringParcel createOperationInput() {
|
||||
return mSaveKeyringParcel;
|
||||
public ChangeUnlockParcel createOperationInput() {
|
||||
return mChangeUnlockParcel;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -468,9 +467,10 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
Bundle data = message.getData();
|
||||
|
||||
// use new passphrase!
|
||||
mSaveKeyringParcel.mNewUnlock = new SaveKeyringParcel.ChangeUnlockParcel(
|
||||
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE),
|
||||
null
|
||||
mChangeUnlockParcel = new ChangeUnlockParcel(
|
||||
mMasterKeyId,
|
||||
mFingerprint,
|
||||
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)
|
||||
);
|
||||
|
||||
mEditOpHelper.cryptoOperation();
|
||||
@@ -646,17 +646,17 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doNfcInBackground() throws IOException {
|
||||
protected void doSecurityTokenInBackground() throws IOException {
|
||||
|
||||
mNfcFingerprints = nfcGetFingerprints();
|
||||
mNfcUserId = nfcGetUserId();
|
||||
mNfcAid = nfcGetAid();
|
||||
mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
|
||||
mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
|
||||
mSecurityTokenAid = mSecurityTokenHelper.getAid();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNfcPostExecute() {
|
||||
protected void onSecurityTokenPostExecute() {
|
||||
|
||||
long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints);
|
||||
long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
|
||||
|
||||
try {
|
||||
|
||||
@@ -667,7 +667,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
|
||||
// if the master key of that key matches this one, just show the token dialog
|
||||
if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) {
|
||||
showSecurityTokenFragment(mNfcFingerprints, mNfcUserId, mNfcAid);
|
||||
showSecurityTokenFragment(mSecurityTokenFingerprints, mSecurityTokenUserId, mSecurityTokenAid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -680,9 +680,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
Intent intent = new Intent(
|
||||
ViewKeyActivity.this, ViewKeyActivity.class);
|
||||
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
@@ -695,9 +695,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
public void onAction() {
|
||||
Intent intent = new Intent(
|
||||
ViewKeyActivity.this, CreateKeyActivity.class);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
@@ -924,6 +924,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
|
||||
}
|
||||
|
||||
mPhoto.setImageBitmap(photo);
|
||||
mPhoto.setColorFilter(getResources().getColor(R.color.toolbar_photo_tint), PorterDuff.Mode.SRC_ATOP);
|
||||
mPhotoLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
};
|
||||
|
||||
+12
-1
@@ -238,7 +238,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
|
||||
|
||||
// let user choose application
|
||||
Intent sendIntent = new Intent(Intent.ACTION_SEND);
|
||||
sendIntent.setType("text/plain");
|
||||
sendIntent.setType(Constants.MIME_TYPE_KEYS);
|
||||
|
||||
// NOTE: Don't use Intent.EXTRA_TEXT to send the key
|
||||
// better send it via a Uri!
|
||||
@@ -455,8 +455,19 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
|
||||
}
|
||||
|
||||
private void uploadToKeyserver() {
|
||||
long keyId;
|
||||
try {
|
||||
keyId = new ProviderHelper(getActivity())
|
||||
.getCachedPublicKeyRing(mDataUri)
|
||||
.extractOrGetMasterKeyId();
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
Log.e(Constants.TAG, "key not found!", e);
|
||||
Notify.create(getActivity(), "key not found", Style.ERROR).show();
|
||||
return;
|
||||
}
|
||||
Intent uploadIntent = new Intent(getActivity(), UploadKeyActivity.class);
|
||||
uploadIntent.setData(mDataUri);
|
||||
uploadIntent.putExtra(MultiUserIdsFragment.EXTRA_KEY_IDS, new long[]{keyId});
|
||||
startActivityForResult(uploadIntent, 0);
|
||||
}
|
||||
|
||||
|
||||
+39
-43
@@ -39,6 +39,7 @@ import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
|
||||
@@ -346,50 +347,45 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: {
|
||||
// TODO: enable later when Admin PIN handling is resolved
|
||||
Notify.create(getActivity(),
|
||||
"This feature will be available in an upcoming OpenKeychain version.",
|
||||
Notify.Style.WARN).show();
|
||||
break;
|
||||
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
|
||||
SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
|
||||
if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
|
||||
secretKeyType == SecretKeyType.GNU_DUMMY) {
|
||||
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
// Activity activity = EditKeyFragment.this.getActivity();
|
||||
// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
|
||||
// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
|
||||
// secretKeyType == SecretKeyType.GNU_DUMMY) {
|
||||
// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR)
|
||||
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
|
||||
// break;
|
||||
// }
|
||||
// int algorithm = mSubkeysAdapter.getAlgorithm(position);
|
||||
// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN
|
||||
// if (algorithm != 1 && algorithm != 2 && algorithm != 3) {
|
||||
// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR)
|
||||
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
|
||||
// break;
|
||||
// }
|
||||
// if (mSubkeysAdapter.getKeySize(position) != 2048) {
|
||||
// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR)
|
||||
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// SubkeyChange change;
|
||||
// change = mSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
// if (change == null) {
|
||||
// mSaveKeyringParcel.mChangeSubKeys.add(
|
||||
// new SubkeyChange(keyId, false, true)
|
||||
// );
|
||||
// break;
|
||||
// }
|
||||
// // toggle
|
||||
// change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
|
||||
// if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
|
||||
// // User had chosen to strip key, but now wants to divert it.
|
||||
// change.mDummyStrip = false;
|
||||
// }
|
||||
// break;
|
||||
int algorithm = mSubkeysAdapter.getAlgorithm(position);
|
||||
if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
|
||||
&& algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
|
||||
&& algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
|
||||
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
if (mSubkeysAdapter.getKeySize(position) != 2048) {
|
||||
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
SubkeyChange change;
|
||||
change = mEditModeSaveKeyringParcel.getSubkeyChange(keyId);
|
||||
if (change == null) {
|
||||
mEditModeSaveKeyringParcel.mChangeSubKeys.add(
|
||||
new SubkeyChange(keyId, false, true)
|
||||
);
|
||||
break;
|
||||
}
|
||||
// toggle
|
||||
change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
|
||||
if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
|
||||
// User had chosen to strip key, but now wants to divert it.
|
||||
change.mDummyStrip = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
|
||||
|
||||
+1
@@ -141,6 +141,7 @@ public class ImportKeysListLoader
|
||||
OperationResult.OperationLog log = new OperationResult.OperationLog();
|
||||
log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0);
|
||||
GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log);
|
||||
mData.clear();
|
||||
mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-8
@@ -66,6 +66,7 @@ public class KeyAdapter extends CursorAdapter {
|
||||
KeyRings.HAS_DUPLICATE_USER_ID,
|
||||
KeyRings.FINGERPRINT,
|
||||
KeyRings.CREATION,
|
||||
KeyRings.HAS_ENCRYPT
|
||||
};
|
||||
|
||||
public static final int INDEX_MASTER_KEY_ID = 1;
|
||||
@@ -77,6 +78,7 @@ public class KeyAdapter extends CursorAdapter {
|
||||
public static final int INDEX_HAS_DUPLICATE_USER_ID = 7;
|
||||
public static final int INDEX_FINGERPRINT = 8;
|
||||
public static final int INDEX_CREATION = 9;
|
||||
public static final int INDEX_HAS_ENCRYPT = 10;
|
||||
|
||||
public KeyAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
@@ -289,6 +291,7 @@ public class KeyAdapter extends CursorAdapter {
|
||||
public final KeyRing.UserId mUserId;
|
||||
public final long mKeyId;
|
||||
public final boolean mHasDuplicate;
|
||||
public final boolean mHasEncrypt;
|
||||
public final Date mCreation;
|
||||
public final String mFingerprint;
|
||||
public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
|
||||
@@ -299,6 +302,7 @@ public class KeyAdapter extends CursorAdapter {
|
||||
mUserIdFull = userId;
|
||||
mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0;
|
||||
mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
|
||||
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
|
||||
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
|
||||
cursor.getBlob(INDEX_FINGERPRINT));
|
||||
@@ -315,6 +319,7 @@ public class KeyAdapter extends CursorAdapter {
|
||||
mUserIdFull = userId;
|
||||
mKeyId = ring.getMasterKeyId();
|
||||
mHasDuplicate = false;
|
||||
mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0;
|
||||
mCreation = key.getCreationTime();
|
||||
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
|
||||
ring.getFingerprint());
|
||||
@@ -333,14 +338,6 @@ public class KeyAdapter extends CursorAdapter {
|
||||
return mUserId.email;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: workaround for bug in TokenAutoComplete,
|
||||
// see https://github.com/open-keychain/open-keychain/issues/1636
|
||||
@Override
|
||||
public String toString() {
|
||||
return " ";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static String[] getProjectionWith(String[] projection) {
|
||||
|
||||
+7
@@ -39,6 +39,7 @@ import java.util.ArrayList;
|
||||
public class MultiUserIdsAdapter extends CursorAdapter {
|
||||
private LayoutInflater mInflater;
|
||||
private final ArrayList<Boolean> mCheckStates;
|
||||
private boolean checkboxVisibility = true;
|
||||
|
||||
public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates) {
|
||||
super(context, c, flags);
|
||||
@@ -46,6 +47,11 @@ public class MultiUserIdsAdapter extends CursorAdapter {
|
||||
mCheckStates = preselectStates == null ? new ArrayList<Boolean>() : preselectStates;
|
||||
}
|
||||
|
||||
public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates, boolean checkboxVisibility) {
|
||||
this(context,c,flags,preselectStates);
|
||||
this.checkboxVisibility = checkboxVisibility;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
if (newCursor != null) {
|
||||
@@ -138,6 +144,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
|
||||
}
|
||||
});
|
||||
vCheckBox.setClickable(false);
|
||||
vCheckBox.setVisibility(checkboxVisibility?View.VISIBLE:View.GONE);
|
||||
|
||||
View vUidBody = view.findViewById(R.id.user_id_body);
|
||||
vUidBody.setClickable(true);
|
||||
|
||||
+38
-10
@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui.adapter;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -32,6 +33,7 @@ import android.widget.TextView;
|
||||
import org.bouncycastle.bcpg.sig.KeyFlags;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
|
||||
import java.util.Calendar;
|
||||
@@ -65,10 +67,11 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
|
||||
public SaveKeyringParcel.SubkeyAdd mModel;
|
||||
}
|
||||
|
||||
|
||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
// Not recycled, inflate a new view
|
||||
convertView = mInflater.inflate(R.layout.view_key_adv_subkey_item, null);
|
||||
convertView = mInflater.inflate(R.layout.view_key_adv_subkey_item, parent, false);
|
||||
final ViewHolder holder = new ViewHolder();
|
||||
holder.vKeyId = (TextView) convertView.findViewById(R.id.subkey_item_key_id);
|
||||
holder.vKeyDetails = (TextView) convertView.findViewById(R.id.subkey_item_details);
|
||||
@@ -88,16 +91,8 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
|
||||
vStatus.setVisibility(View.GONE);
|
||||
|
||||
convertView.setTag(holder);
|
||||
|
||||
holder.vDelete.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// remove reference model item from adapter (data and notify about change)
|
||||
SubkeysAddedAdapter.this.remove(holder.mModel);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
final ViewHolder holder = (ViewHolder) convertView.getTag();
|
||||
|
||||
// save reference to model item
|
||||
@@ -113,8 +108,41 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
|
||||
boolean isMasterKey = mNewKeyring && position == 0;
|
||||
if (isMasterKey) {
|
||||
holder.vKeyId.setTypeface(null, Typeface.BOLD);
|
||||
holder.vDelete.setImageResource(R.drawable.ic_change_grey_24dp);
|
||||
holder.vDelete.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// swapping out the old master key with newly set master key
|
||||
AddSubkeyDialogFragment addSubkeyDialogFragment =
|
||||
AddSubkeyDialogFragment.newInstance(true);
|
||||
addSubkeyDialogFragment
|
||||
.setOnAlgorithmSelectedListener(
|
||||
new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {
|
||||
@Override
|
||||
public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey) {
|
||||
// calculate manually as the provided position variable
|
||||
// is not always accurate
|
||||
int pos = SubkeysAddedAdapter.this.getPosition(holder.mModel);
|
||||
SubkeysAddedAdapter.this.remove(holder.mModel);
|
||||
SubkeysAddedAdapter.this.insert(newSubkey, pos);
|
||||
}
|
||||
}
|
||||
);
|
||||
addSubkeyDialogFragment.show(
|
||||
((FragmentActivity)mActivity).getSupportFragmentManager()
|
||||
, "addSubkeyDialog");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
holder.vKeyId.setTypeface(null, Typeface.NORMAL);
|
||||
holder.vDelete.setImageResource(R.drawable.ic_close_grey_24dp);
|
||||
holder.vDelete.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// remove reference model item from adapter (data and notify about change)
|
||||
SubkeysAddedAdapter.this.remove(holder.mModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
holder.vKeyId.setText(R.string.edit_key_new_subkey);
|
||||
|
||||
+12
-1
@@ -26,6 +26,7 @@ import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
@@ -45,8 +46,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
initTheme();
|
||||
super.onCreate(savedInstanceState);
|
||||
initTheme();
|
||||
initLayout();
|
||||
initToolbar();
|
||||
}
|
||||
@@ -65,6 +66,16 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home :
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public static void onResumeChecks(Context context) {
|
||||
KeyserverSyncAdapterService.cancelUpdates(context);
|
||||
// in case user has disabled sync from Android account settings
|
||||
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
|
||||
* Copyright (C) 2013-2014 Signe Rüsch
|
||||
* Copyright (C) 2013-2014 Philipp Jakubeit
|
||||
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.base;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.TagLostException;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.securitytoken.CardException;
|
||||
import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
|
||||
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
|
||||
import org.sufficientlysecure.keychain.securitytoken.Transport;
|
||||
import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher;
|
||||
import org.sufficientlysecure.keychain.securitytoken.UsbTransport;
|
||||
import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
|
||||
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
|
||||
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.FidesmoInstallDialog;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.FidesmoPgpInstallDialog;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Passphrase;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nordpol.android.OnDiscoveredTagListener;
|
||||
import nordpol.android.TagDispatcher;
|
||||
|
||||
public abstract class BaseSecurityTokenActivity extends BaseActivity
|
||||
implements OnDiscoveredTagListener, UsbConnectionDispatcher.OnDiscoveredUsbDeviceListener {
|
||||
public static final int REQUEST_CODE_PIN = 1;
|
||||
|
||||
public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled";
|
||||
|
||||
private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android";
|
||||
|
||||
protected SecurityTokenHelper mSecurityTokenHelper = SecurityTokenHelper.getInstance();
|
||||
protected TagDispatcher mNfcTagDispatcher;
|
||||
protected UsbConnectionDispatcher mUsbDispatcher;
|
||||
private boolean mTagHandlingEnabled;
|
||||
|
||||
private byte[] mSecurityTokenFingerprints;
|
||||
private String mSecurityTokenUserId;
|
||||
private byte[] mSecurityTokenAid;
|
||||
|
||||
/**
|
||||
* Override to change UI before SecurityToken handling (UI thread)
|
||||
*/
|
||||
protected void onSecurityTokenPreExecute() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to implement SecurityToken operations (background thread)
|
||||
*/
|
||||
protected void doSecurityTokenInBackground() throws IOException {
|
||||
mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
|
||||
mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
|
||||
mSecurityTokenAid = mSecurityTokenHelper.getAid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to handle result of SecurityToken operations (UI thread)
|
||||
*/
|
||||
protected void onSecurityTokenPostExecute() {
|
||||
|
||||
final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
|
||||
|
||||
try {
|
||||
CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(
|
||||
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId));
|
||||
long masterKeyId = ring.getMasterKeyId();
|
||||
|
||||
Intent intent = new Intent(this, ViewKeyActivity.class);
|
||||
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
|
||||
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
|
||||
startActivity(intent);
|
||||
} catch (PgpKeyNotFoundException e) {
|
||||
Intent intent = new Intent(this, CreateKeyActivity.class);
|
||||
intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
|
||||
intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
|
||||
intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_FINGERPRINTS, mSecurityTokenFingerprints);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to use something different than Notify (UI thread)
|
||||
*/
|
||||
protected void onSecurityTokenError(String error) {
|
||||
Notify.create(this, error, Style.WARN).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to do something when PIN is wrong, e.g., clear passphrases (UI thread)
|
||||
*/
|
||||
protected void onSecurityTokenPinError(String error) {
|
||||
onSecurityTokenError(error);
|
||||
}
|
||||
|
||||
public void tagDiscovered(final Tag tag) {
|
||||
// Actual NFC operations are executed in doInBackground to not block the UI thread
|
||||
if (!mTagHandlingEnabled)
|
||||
return;
|
||||
|
||||
securityTokenDiscovered(new NfcTransport(tag));
|
||||
}
|
||||
|
||||
public void usbDeviceDiscovered(final UsbDevice usbDevice) {
|
||||
// Actual USB operations are executed in doInBackground to not block the UI thread
|
||||
if (!mTagHandlingEnabled)
|
||||
return;
|
||||
|
||||
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||
securityTokenDiscovered(new UsbTransport(usbDevice, usbManager));
|
||||
}
|
||||
|
||||
public void securityTokenDiscovered(final Transport transport) {
|
||||
// Actual Security Token operations are executed in doInBackground to not block the UI thread
|
||||
if (!mTagHandlingEnabled)
|
||||
return;
|
||||
new AsyncTask<Void, Void, IOException>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
onSecurityTokenPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IOException doInBackground(Void... params) {
|
||||
try {
|
||||
handleSecurityToken(transport);
|
||||
} catch (IOException e) {
|
||||
return e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(IOException exception) {
|
||||
super.onPostExecute(exception);
|
||||
|
||||
if (exception != null) {
|
||||
handleSecurityTokenError(exception);
|
||||
return;
|
||||
}
|
||||
|
||||
onSecurityTokenPostExecute();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
protected void pauseTagHandling() {
|
||||
mTagHandlingEnabled = false;
|
||||
}
|
||||
|
||||
protected void resumeTagHandling() {
|
||||
mTagHandlingEnabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mNfcTagDispatcher = TagDispatcher.get(this, this, false, false, true, false);
|
||||
mUsbDispatcher = new UsbConnectionDispatcher(this, this);
|
||||
|
||||
// Check whether we're recreating a previously destroyed instance
|
||||
if (savedInstanceState != null) {
|
||||
// Restore value of members from saved state
|
||||
mTagHandlingEnabled = savedInstanceState.getBoolean(EXTRA_TAG_HANDLING_ENABLED);
|
||||
} else {
|
||||
mTagHandlingEnabled = true;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
|
||||
throw new AssertionError("should not happen: NfcOperationActivity.onCreate is called instead of onNewIntent!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putBoolean(EXTRA_TAG_HANDLING_ENABLED, mTagHandlingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* This activity is started as a singleTop activity.
|
||||
* All new NFC Intents which are delivered to this activity are handled here
|
||||
*/
|
||||
@Override
|
||||
public void onNewIntent(final Intent intent) {
|
||||
mNfcTagDispatcher.interceptIntent(intent);
|
||||
}
|
||||
|
||||
private void handleSecurityTokenError(IOException e) {
|
||||
|
||||
if (e instanceof TagLostException) {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_tag_lost));
|
||||
return;
|
||||
}
|
||||
|
||||
if (e instanceof IsoDepNotSupportedException) {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported));
|
||||
return;
|
||||
}
|
||||
|
||||
short status;
|
||||
if (e instanceof CardException) {
|
||||
status = ((CardException) e).getResponseCode();
|
||||
} else {
|
||||
status = -1;
|
||||
}
|
||||
|
||||
// Wrong PIN, a status of 63CX indicates X attempts remaining.
|
||||
// NOTE: Used in ykneo-openpgp version < 1.0.10, changed to 0x6982 in 1.0.11
|
||||
// https://github.com/Yubico/ykneo-openpgp/commit/90c2b91e86fb0e43ee234dd258834e75e3416410
|
||||
if ((status & (short) 0xFFF0) == 0x63C0) {
|
||||
int tries = status & 0x000F;
|
||||
// hook to do something different when PIN is wrong
|
||||
onSecurityTokenPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, all status codes are fixed values.
|
||||
switch (status) {
|
||||
|
||||
// These error conditions are likely to be experienced by an end user.
|
||||
|
||||
/* OpenPGP Card Spec: Security status not satisfied, PW wrong,
|
||||
PW not checked (command not allowed), Secure messaging incorrect (checksum and/or cryptogram) */
|
||||
// NOTE: Used in ykneo-openpgp >= 1.0.11 for wrong PIN
|
||||
case 0x6982: {
|
||||
// hook to do something different when PIN is wrong
|
||||
onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Selected file in termination state */
|
||||
case 0x6285: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_terminated));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Wrong length (Lc and/or Le) */
|
||||
// NOTE: Used in ykneo-openpgp < 1.0.10 for too short PIN, changed in 1.0.11 to 0x6A80 for too short PIN
|
||||
// https://github.com/Yubico/ykneo-openpgp/commit/b49ce8241917e7c087a4dab7b2c755420ff4500f
|
||||
case 0x6700: {
|
||||
// hook to do something different when PIN is wrong
|
||||
onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Incorrect parameters in the data field */
|
||||
// NOTE: Used in ykneo-openpgp >= 1.0.11 for too short PIN
|
||||
case 0x6A80: {
|
||||
// hook to do something different when PIN is wrong
|
||||
onSecurityTokenPinError(getString(R.string.security_token_error_bad_data));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Authentication method blocked, PW blocked (error counter zero) */
|
||||
case 0x6983: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_authentication_blocked));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Condition of use not satisfied */
|
||||
case 0x6985: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_conditions_not_satisfied));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: SM data objects incorrect (e.g. wrong TLV-structure in command data) */
|
||||
// NOTE: 6A88 is "Not Found" in the spec, but ykneo-openpgp also returns 6A83 for this in some cases.
|
||||
case 0x6A88:
|
||||
case 0x6A83: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_data_not_found));
|
||||
break;
|
||||
}
|
||||
// 6F00 is a JavaCard proprietary status code, SW_UNKNOWN, and usually represents an
|
||||
// unhandled exception on the security token.
|
||||
case 0x6F00: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_unknown));
|
||||
break;
|
||||
}
|
||||
// 6A82 app not installed on security token!
|
||||
case 0x6A82: {
|
||||
if (mSecurityTokenHelper.isFidesmoToken()) {
|
||||
// Check if the Fidesmo app is installed
|
||||
if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
|
||||
promptFidesmoPgpInstall();
|
||||
} else {
|
||||
promptFidesmoAppInstall();
|
||||
}
|
||||
} else { // Other (possibly) compatible hardware
|
||||
onSecurityTokenError(getString(R.string.security_token_error_pgp_app_not_installed));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// These errors should not occur in everyday use; if they are returned, it means we
|
||||
// made a mistake sending data to the token, or the token is misbehaving.
|
||||
|
||||
/* OpenPGP Card Spec: Last command of the chain expected */
|
||||
case 0x6883: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Wrong parameters P1-P2 */
|
||||
case 0x6B00: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Instruction (INS) not supported */
|
||||
case 0x6D00: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
|
||||
break;
|
||||
}
|
||||
/* OpenPGP Card Spec: Class (CLA) not supported */
|
||||
case 0x6E00: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the system is about to start resuming a previous activity,
|
||||
* disables NFC Foreground Dispatch
|
||||
*/
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
Log.d(Constants.TAG, "BaseNfcActivity.onPause");
|
||||
|
||||
mNfcTagDispatcher.disableExclusiveNfc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity will start interacting with the user,
|
||||
* enables NFC Foreground Dispatch
|
||||
*/
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Log.d(Constants.TAG, "BaseNfcActivity.onResume");
|
||||
mNfcTagDispatcher.enableExclusiveNfc();
|
||||
}
|
||||
|
||||
protected void obtainSecurityTokenPin(RequiredInputParcel requiredInput) {
|
||||
|
||||
try {
|
||||
Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
|
||||
requiredInput.getMasterKeyId(), requiredInput.getSubKeyId());
|
||||
if (passphrase != null) {
|
||||
mSecurityTokenHelper.setPin(passphrase);
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, PassphraseDialogActivity.class);
|
||||
intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
|
||||
RequiredInputParcel.createRequiredPassphrase(requiredInput));
|
||||
startActivityForResult(intent, REQUEST_CODE_PIN);
|
||||
} catch (PassphraseCacheService.KeyNotFoundException e) {
|
||||
throw new AssertionError(
|
||||
"tried to find passphrase for non-existing key. this is a programming error!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_PIN: {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
setResult(resultCode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
|
||||
mSecurityTokenHelper.setPin(input.getPassphrase());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleSecurityToken(Transport transport) throws IOException {
|
||||
// Don't reconnect if device was already connected
|
||||
if (!(mSecurityTokenHelper.isPersistentConnectionAllowed()
|
||||
&& mSecurityTokenHelper.isConnected()
|
||||
&& mSecurityTokenHelper.getTransport().equals(transport))) {
|
||||
mSecurityTokenHelper.setTransport(transport);
|
||||
mSecurityTokenHelper.connectToDevice();
|
||||
}
|
||||
doSecurityTokenInBackground();
|
||||
}
|
||||
|
||||
public boolean isSecurityTokenConnected() {
|
||||
return mSecurityTokenHelper.isConnected();
|
||||
}
|
||||
|
||||
public static class IsoDepNotSupportedException extends IOException {
|
||||
|
||||
public IsoDepNotSupportedException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if she wants to install PGP onto her Fidesmo token
|
||||
*/
|
||||
private void promptFidesmoPgpInstall() {
|
||||
FidesmoPgpInstallDialog fidesmoPgpInstallDialog = new FidesmoPgpInstallDialog();
|
||||
fidesmoPgpInstallDialog.show(getSupportFragmentManager(), "fidesmoPgpInstallDialog");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a Dialog to the user informing that Fidesmo App must be installed and with option
|
||||
* to launch the Google Play store.
|
||||
*/
|
||||
private void promptFidesmoAppInstall() {
|
||||
FidesmoInstallDialog fidesmoInstallDialog = new FidesmoInstallDialog();
|
||||
fidesmoInstallDialog.show(getSupportFragmentManager(), "fidesmoInstallDialog");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the package manager to detect if an application is installed on the phone
|
||||
*
|
||||
* @param uri an URI identifying the application's package
|
||||
* @return 'true' if the app is installed
|
||||
*/
|
||||
private boolean isAndroidAppInstalled(String uri) {
|
||||
PackageManager mPackageManager = getPackageManager();
|
||||
boolean mAppInstalled;
|
||||
try {
|
||||
mPackageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
|
||||
mAppInstalled = true;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(Constants.TAG, "App not installed on Android device");
|
||||
mAppInstalled = false;
|
||||
}
|
||||
return mAppInstalled;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
mUsbDispatcher.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mUsbDispatcher.onStart();
|
||||
}
|
||||
|
||||
public SecurityTokenHelper getSecurityTokenHelper() {
|
||||
return mSecurityTokenHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Security Token routines if last used token is connected and supports
|
||||
* persistent connections
|
||||
*/
|
||||
public void checkDeviceConnection() {
|
||||
mUsbDispatcher.rescanDevices();
|
||||
}
|
||||
}
|
||||
-1057
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -130,9 +130,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
|
||||
|
||||
switch (requiredInput.mType) {
|
||||
// always use CryptoOperationHelper.startActivityForResult!
|
||||
case NFC_MOVE_KEY_TO_CARD:
|
||||
case NFC_DECRYPT:
|
||||
case NFC_SIGN: {
|
||||
case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
|
||||
case SECURITY_TOKEN_DECRYPT:
|
||||
case SECURITY_TOKEN_SIGN: {
|
||||
Intent intent = new Intent(activity, SecurityTokenOperationActivity.class);
|
||||
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
|
||||
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
|
||||
|
||||
+6
-11
@@ -50,14 +50,13 @@ import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.util.TlsHelper;
|
||||
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
|
||||
@@ -354,19 +353,15 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
|
||||
Log.d("Converted URL", newKeyserver.toString());
|
||||
|
||||
OkHttpClient client = HkpKeyserver.getClient(newKeyserver.toURL(), proxy);
|
||||
|
||||
// don't follow any redirects
|
||||
client.setFollowRedirects(false);
|
||||
client.setFollowSslRedirects(false);
|
||||
|
||||
if (onlyTrustedKeyserver
|
||||
&& !TlsHelper.usePinnedCertificateIfAvailable(client, newKeyserver.toURL())) {
|
||||
&& TlsHelper.getPinnedSslSocketFactory(newKeyserver.toURL()) == null) {
|
||||
Log.w(Constants.TAG, "No pinned certificate for this host in OpenKeychain's assets.");
|
||||
reason = FailureReason.NO_PINNED_CERTIFICATE;
|
||||
return reason;
|
||||
}
|
||||
|
||||
OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(newKeyserver.toURL(), proxy);
|
||||
|
||||
client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute();
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
reason = FailureReason.CONNECTION_FAILED;
|
||||
|
||||
+146
-333
@@ -17,25 +17,26 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.dialog;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.os.Build;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
@@ -49,14 +50,18 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
|
||||
import org.sufficientlysecure.keychain.util.Choice;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
|
||||
public interface OnAlgorithmSelectedListener {
|
||||
public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey);
|
||||
void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey);
|
||||
}
|
||||
|
||||
public enum SupportedKeyType {
|
||||
RSA_2048, RSA_3072, RSA_4096, ECC_P256, ECC_P521
|
||||
}
|
||||
|
||||
private static final String ARG_WILL_BE_MASTER_KEY = "will_be_master_key";
|
||||
@@ -66,18 +71,12 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
private CheckBox mNoExpiryCheckBox;
|
||||
private TableRow mExpiryRow;
|
||||
private DatePicker mExpiryDatePicker;
|
||||
private Spinner mAlgorithmSpinner;
|
||||
private View mKeySizeRow;
|
||||
private Spinner mKeySizeSpinner;
|
||||
private View mCurveRow;
|
||||
private Spinner mCurveSpinner;
|
||||
private TextView mCustomKeyTextView;
|
||||
private EditText mCustomKeyEditText;
|
||||
private TextView mCustomKeyInfoTextView;
|
||||
private CheckBox mFlagCertify;
|
||||
private CheckBox mFlagSign;
|
||||
private CheckBox mFlagEncrypt;
|
||||
private CheckBox mFlagAuthenticate;
|
||||
private Spinner mKeyTypeSpinner;
|
||||
private RadioGroup mUsageRadioGroup;
|
||||
private RadioButton mUsageNone;
|
||||
private RadioButton mUsageSign;
|
||||
private RadioButton mUsageEncrypt;
|
||||
private RadioButton mUsageSignAndEncrypt;
|
||||
|
||||
private boolean mWillBeMasterKey;
|
||||
|
||||
@@ -96,6 +95,8 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
return frag;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity context = getActivity();
|
||||
@@ -106,25 +107,27 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
|
||||
CustomAlertDialogBuilder dialog = new CustomAlertDialogBuilder(context);
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
View view = mInflater.inflate(R.layout.add_subkey_dialog, null);
|
||||
dialog.setView(view);
|
||||
dialog.setTitle(R.string.title_add_subkey);
|
||||
|
||||
mNoExpiryCheckBox = (CheckBox) view.findViewById(R.id.add_subkey_no_expiry);
|
||||
mExpiryRow = (TableRow) view.findViewById(R.id.add_subkey_expiry_row);
|
||||
mExpiryDatePicker = (DatePicker) view.findViewById(R.id.add_subkey_expiry_date_picker);
|
||||
mAlgorithmSpinner = (Spinner) view.findViewById(R.id.add_subkey_algorithm);
|
||||
mKeySizeSpinner = (Spinner) view.findViewById(R.id.add_subkey_size);
|
||||
mCurveSpinner = (Spinner) view.findViewById(R.id.add_subkey_curve);
|
||||
mKeySizeRow = view.findViewById(R.id.add_subkey_row_size);
|
||||
mCurveRow = view.findViewById(R.id.add_subkey_row_curve);
|
||||
mCustomKeyTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_label);
|
||||
mCustomKeyEditText = (EditText) view.findViewById(R.id.add_subkey_custom_key_size_input);
|
||||
mCustomKeyInfoTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_info);
|
||||
mFlagCertify = (CheckBox) view.findViewById(R.id.add_subkey_flag_certify);
|
||||
mFlagSign = (CheckBox) view.findViewById(R.id.add_subkey_flag_sign);
|
||||
mFlagEncrypt = (CheckBox) view.findViewById(R.id.add_subkey_flag_encrypt);
|
||||
mFlagAuthenticate = (CheckBox) view.findViewById(R.id.add_subkey_flag_authenticate);
|
||||
mKeyTypeSpinner = (Spinner) view.findViewById(R.id.add_subkey_type);
|
||||
mUsageRadioGroup = (RadioGroup) view.findViewById(R.id.add_subkey_usage_group);
|
||||
mUsageNone = (RadioButton) view.findViewById(R.id.add_subkey_usage_none);
|
||||
mUsageSign = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign);
|
||||
mUsageEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_encrypt);
|
||||
mUsageSignAndEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign_and_encrypt);
|
||||
|
||||
if(mWillBeMasterKey) {
|
||||
dialog.setTitle(R.string.title_change_master_key);
|
||||
mUsageNone.setVisibility(View.VISIBLE);
|
||||
mUsageNone.setChecked(true);
|
||||
} else {
|
||||
dialog.setTitle(R.string.title_add_subkey);
|
||||
}
|
||||
|
||||
mNoExpiryCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
@@ -143,65 +146,24 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
mExpiryDatePicker.setMinDate(minDateCal.getTime().getTime());
|
||||
|
||||
{
|
||||
ArrayList<Choice<Algorithm>> choices = new ArrayList<>();
|
||||
choices.add(new Choice<>(Algorithm.DSA, getResources().getString(
|
||||
R.string.dsa)));
|
||||
if (!mWillBeMasterKey) {
|
||||
choices.add(new Choice<>(Algorithm.ELGAMAL, getResources().getString(
|
||||
R.string.elgamal)));
|
||||
}
|
||||
choices.add(new Choice<>(Algorithm.RSA, getResources().getString(
|
||||
R.string.rsa)));
|
||||
choices.add(new Choice<>(Algorithm.ECDSA, getResources().getString(
|
||||
R.string.ecdsa)));
|
||||
choices.add(new Choice<>(Algorithm.ECDH, getResources().getString(
|
||||
R.string.ecdh)));
|
||||
ArrayAdapter<Choice<Algorithm>> adapter = new ArrayAdapter<>(context,
|
||||
ArrayList<Choice<SupportedKeyType>> choices = new ArrayList<>();
|
||||
choices.add(new Choice<>(SupportedKeyType.RSA_2048, getResources().getString(
|
||||
R.string.rsa_2048), getResources().getString(R.string.rsa_2048_description_html)));
|
||||
choices.add(new Choice<>(SupportedKeyType.RSA_3072, getResources().getString(
|
||||
R.string.rsa_3072), getResources().getString(R.string.rsa_3072_description_html)));
|
||||
choices.add(new Choice<>(SupportedKeyType.RSA_4096, getResources().getString(
|
||||
R.string.rsa_4096), getResources().getString(R.string.rsa_4096_description_html)));
|
||||
choices.add(new Choice<>(SupportedKeyType.ECC_P256, getResources().getString(
|
||||
R.string.ecc_p256), getResources().getString(R.string.ecc_p256_description_html)));
|
||||
choices.add(new Choice<>(SupportedKeyType.ECC_P521, getResources().getString(
|
||||
R.string.ecc_p521), getResources().getString(R.string.ecc_p521_description_html)));
|
||||
TwoLineArrayAdapter adapter = new TwoLineArrayAdapter(context,
|
||||
android.R.layout.simple_spinner_item, choices);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mAlgorithmSpinner.setAdapter(adapter);
|
||||
// make RSA the default
|
||||
mKeyTypeSpinner.setAdapter(adapter);
|
||||
// make RSA 3072 the default
|
||||
for (int i = 0; i < choices.size(); ++i) {
|
||||
if (choices.get(i).getId() == Algorithm.RSA) {
|
||||
mAlgorithmSpinner.setSelection(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic ArrayAdapter must be created (instead of ArrayAdapter.getFromResource), because it's content may change
|
||||
ArrayAdapter<CharSequence> keySizeAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item,
|
||||
new ArrayList<CharSequence>(Arrays.asList(getResources().getStringArray(R.array.rsa_key_size_spinner_values))));
|
||||
keySizeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mKeySizeSpinner.setAdapter(keySizeAdapter);
|
||||
mKeySizeSpinner.setSelection(1); // Default to 4096 for the key length
|
||||
|
||||
{
|
||||
ArrayList<Choice<Curve>> choices = new ArrayList<>();
|
||||
|
||||
choices.add(new Choice<>(Curve.NIST_P256, getResources().getString(
|
||||
R.string.key_curve_nist_p256)));
|
||||
choices.add(new Choice<>(Curve.NIST_P384, getResources().getString(
|
||||
R.string.key_curve_nist_p384)));
|
||||
choices.add(new Choice<>(Curve.NIST_P521, getResources().getString(
|
||||
R.string.key_curve_nist_p521)));
|
||||
|
||||
/* @see SaveKeyringParcel
|
||||
choices.add(new Choice<Curve>(Curve.BRAINPOOL_P256, getResources().getString(
|
||||
R.string.key_curve_bp_p256)));
|
||||
choices.add(new Choice<Curve>(Curve.BRAINPOOL_P384, getResources().getString(
|
||||
R.string.key_curve_bp_p384)));
|
||||
choices.add(new Choice<Curve>(Curve.BRAINPOOL_P512, getResources().getString(
|
||||
R.string.key_curve_bp_p512)));
|
||||
*/
|
||||
|
||||
ArrayAdapter<Choice<Curve>> adapter = new ArrayAdapter<>(context,
|
||||
android.R.layout.simple_spinner_item, choices);
|
||||
mCurveSpinner.setAdapter(adapter);
|
||||
// make NIST P-256 the default
|
||||
for (int i = 0; i < choices.size(); ++i) {
|
||||
if (choices.get(i).getId() == Curve.NIST_P256) {
|
||||
mCurveSpinner.setSelection(i);
|
||||
if (choices.get(i).getId() == SupportedKeyType.RSA_3072) {
|
||||
mKeyTypeSpinner.setSelection(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -215,45 +177,35 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
|
||||
final AlertDialog alertDialog = dialog.show();
|
||||
|
||||
mCustomKeyEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
mKeyTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
setOkButtonAvailability(alertDialog);
|
||||
}
|
||||
});
|
||||
|
||||
mKeySizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
setCustomKeyVisibility();
|
||||
setOkButtonAvailability(alertDialog);
|
||||
// noinspection unchecked
|
||||
SupportedKeyType keyType = ((Choice<SupportedKeyType>) parent.getSelectedItem()).getId();
|
||||
|
||||
// RadioGroup.getCheckedRadioButtonId() gives the wrong RadioButton checked
|
||||
// when programmatically unchecking children radio buttons. Clearing all is the only option.
|
||||
mUsageRadioGroup.clearCheck();
|
||||
|
||||
if(mWillBeMasterKey) {
|
||||
mUsageNone.setChecked(true);
|
||||
}
|
||||
|
||||
if (keyType == SupportedKeyType.ECC_P521 || keyType == SupportedKeyType.ECC_P256) {
|
||||
mUsageSignAndEncrypt.setEnabled(false);
|
||||
if (mWillBeMasterKey) {
|
||||
mUsageEncrypt.setEnabled(false);
|
||||
}
|
||||
} else {
|
||||
// need to enable if previously disabled for ECC masterkey
|
||||
mUsageEncrypt.setEnabled(true);
|
||||
mUsageSignAndEncrypt.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
mAlgorithmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
updateUiForAlgorithm(((Choice<Algorithm>) parent.getSelectedItem()).getId());
|
||||
|
||||
setCustomKeyVisibility();
|
||||
setOkButtonAvailability(alertDialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
|
||||
return alertDialog;
|
||||
@@ -269,36 +221,74 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
positiveButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (!mFlagCertify.isChecked() && !mFlagSign.isChecked()
|
||||
&& !mFlagEncrypt.isChecked() && !mFlagAuthenticate.isChecked()) {
|
||||
Toast.makeText(getActivity(), R.string.edit_key_select_flag, Toast.LENGTH_LONG).show();
|
||||
if (mUsageRadioGroup.getCheckedRadioButtonId() == -1) {
|
||||
Toast.makeText(getActivity(), R.string.edit_key_select_usage, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId();
|
||||
// noinspection unchecked
|
||||
SupportedKeyType keyType = ((Choice<SupportedKeyType>) mKeyTypeSpinner.getSelectedItem()).getId();
|
||||
Curve curve = null;
|
||||
Integer keySize = null;
|
||||
// For EC keys, add a curve
|
||||
if (algorithm == Algorithm.ECDH || algorithm == Algorithm.ECDSA) {
|
||||
curve = ((Choice<Curve>) mCurveSpinner.getSelectedItem()).getId();
|
||||
// Otherwise, get a keysize
|
||||
} else {
|
||||
keySize = getProperKeyLength(algorithm, getSelectedKeyLength());
|
||||
Algorithm algorithm = null;
|
||||
|
||||
// set keysize & curve, for RSA & ECC respectively
|
||||
switch (keyType) {
|
||||
case RSA_2048: {
|
||||
keySize = 2048;
|
||||
break;
|
||||
}
|
||||
case RSA_3072: {
|
||||
keySize = 3072;
|
||||
break;
|
||||
}
|
||||
case RSA_4096: {
|
||||
keySize = 4096;
|
||||
break;
|
||||
}
|
||||
case ECC_P256: {
|
||||
curve = Curve.NIST_P256;
|
||||
break;
|
||||
}
|
||||
case ECC_P521: {
|
||||
curve = Curve.NIST_P521;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// set algorithm
|
||||
switch (keyType) {
|
||||
case RSA_2048:
|
||||
case RSA_3072:
|
||||
case RSA_4096: {
|
||||
algorithm = Algorithm.RSA;
|
||||
break;
|
||||
}
|
||||
|
||||
case ECC_P256:
|
||||
case ECC_P521: {
|
||||
if(mUsageEncrypt.isChecked()) {
|
||||
algorithm = Algorithm.ECDH;
|
||||
} else {
|
||||
algorithm = Algorithm.ECDSA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// set flags
|
||||
int flags = 0;
|
||||
if (mFlagCertify.isChecked()) {
|
||||
if (mWillBeMasterKey) {
|
||||
flags |= KeyFlags.CERTIFY_OTHER;
|
||||
}
|
||||
if (mFlagSign.isChecked()) {
|
||||
if (mUsageSign.isChecked()) {
|
||||
flags |= KeyFlags.SIGN_DATA;
|
||||
}
|
||||
if (mFlagEncrypt.isChecked()) {
|
||||
} else if (mUsageEncrypt.isChecked()) {
|
||||
flags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
|
||||
} else if (mUsageSignAndEncrypt.isChecked()) {
|
||||
flags |= KeyFlags.SIGN_DATA | KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
|
||||
}
|
||||
if (mFlagAuthenticate.isChecked()) {
|
||||
flags |= KeyFlags.AUTHENTICATION;
|
||||
}
|
||||
|
||||
|
||||
long expiry;
|
||||
if (mNoExpiryCheckBox.isChecked()) {
|
||||
@@ -332,206 +322,29 @@ public class AddSubkeyDialogFragment extends DialogFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private int getSelectedKeyLength() {
|
||||
final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem();
|
||||
final String customLengthString = getResources().getString(R.string.key_size_custom);
|
||||
final boolean customSelected = customLengthString.equals(selectedItemString);
|
||||
String keyLengthString = customSelected ? mCustomKeyEditText.getText().toString() : selectedItemString;
|
||||
int keySize;
|
||||
try {
|
||||
keySize = Integer.parseInt(keyLengthString);
|
||||
} catch (NumberFormatException e) {
|
||||
keySize = 0;
|
||||
private class TwoLineArrayAdapter extends ArrayAdapter<Choice<SupportedKeyType>> {
|
||||
public TwoLineArrayAdapter(Context context, int resource, List<Choice<SupportedKeyType>> objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
return keySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h3>RSA</h3>
|
||||
* <p>for RSA algorithm, key length must be greater than 2048. Possibility to generate keys bigger
|
||||
* than 8192 bits is currently disabled, because it's almost impossible to generate them on a mobile device (check
|
||||
* <a href="http://www.javamex.com/tutorials/cryptography/rsa_key_length.shtml">RSA key length plot</a> and
|
||||
* <a href="http://www.keylength.com/">Cryptographic Key Length Recommendation</a>). Also, key length must be a
|
||||
* multiplicity of 8.</p>
|
||||
* <h3>ElGamal</h3>
|
||||
* <p>For ElGamal algorithm, supported key lengths are 2048, 3072, 4096 or 8192 bits.</p>
|
||||
* <h3>DSA</h3>
|
||||
* <p>For DSA algorithm key length must be between 2048 and 3072. Also, it must me dividable by 64.</p>
|
||||
*
|
||||
* @return correct key length, according to BouncyCastle specification. Returns <code>-1</code>, if key length is
|
||||
* inappropriate.
|
||||
*/
|
||||
private int getProperKeyLength(Algorithm algorithm, int currentKeyLength) {
|
||||
final int[] elGamalSupportedLengths = {2048, 3072, 4096, 8192};
|
||||
int properKeyLength = -1;
|
||||
switch (algorithm) {
|
||||
case RSA: {
|
||||
if (currentKeyLength >= 2048 && currentKeyLength <= 16384) {
|
||||
properKeyLength = currentKeyLength + ((8 - (currentKeyLength % 8)) % 8);
|
||||
}
|
||||
break;
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
// inflate view if not given one
|
||||
if (convertView == null) {
|
||||
convertView = getActivity().getLayoutInflater()
|
||||
.inflate(R.layout.two_line_spinner_dropdown_item, parent, false);
|
||||
}
|
||||
case ELGAMAL: {
|
||||
int[] elGammalKeyDiff = new int[elGamalSupportedLengths.length];
|
||||
for (int i = 0; i < elGamalSupportedLengths.length; i++) {
|
||||
elGammalKeyDiff[i] = Math.abs(elGamalSupportedLengths[i] - currentKeyLength);
|
||||
}
|
||||
int minimalValue = Integer.MAX_VALUE;
|
||||
int minimalIndex = -1;
|
||||
for (int i = 0; i < elGammalKeyDiff.length; i++) {
|
||||
if (elGammalKeyDiff[i] <= minimalValue) {
|
||||
minimalValue = elGammalKeyDiff[i];
|
||||
minimalIndex = i;
|
||||
}
|
||||
}
|
||||
properKeyLength = elGamalSupportedLengths[minimalIndex];
|
||||
break;
|
||||
}
|
||||
case DSA: {
|
||||
// Bouncy Castle supports 4096 maximum
|
||||
if (currentKeyLength >= 2048 && currentKeyLength <= 4096) {
|
||||
properKeyLength = currentKeyLength + ((64 - (currentKeyLength % 64)) % 64);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return properKeyLength;
|
||||
}
|
||||
|
||||
private void setOkButtonAvailability(AlertDialog alertDialog) {
|
||||
Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId();
|
||||
boolean enabled = algorithm == Algorithm.ECDSA || algorithm == Algorithm.ECDH
|
||||
|| getProperKeyLength(algorithm, getSelectedKeyLength()) > 0;
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
|
||||
}
|
||||
Choice c = this.getItem(position);
|
||||
|
||||
private void setCustomKeyVisibility() {
|
||||
final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem();
|
||||
final String customLengthString = getResources().getString(R.string.key_size_custom);
|
||||
final boolean customSelected = customLengthString.equals(selectedItemString);
|
||||
final int visibility = customSelected ? View.VISIBLE : View.GONE;
|
||||
TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
|
||||
TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
|
||||
|
||||
mCustomKeyEditText.setVisibility(visibility);
|
||||
mCustomKeyTextView.setVisibility(visibility);
|
||||
mCustomKeyInfoTextView.setVisibility(visibility);
|
||||
text1.setText(c.getName());
|
||||
text2.setText(Html.fromHtml(c.getDescription()));
|
||||
|
||||
// hide keyboard after setting visibility to gone
|
||||
if (visibility == View.GONE) {
|
||||
InputMethodManager imm = (InputMethodManager)
|
||||
getActivity().getSystemService(FragmentActivity.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(mCustomKeyEditText.getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUiForAlgorithm(Algorithm algorithm) {
|
||||
final ArrayAdapter<CharSequence> keySizeAdapter = (ArrayAdapter<CharSequence>) mKeySizeSpinner.getAdapter();
|
||||
keySizeAdapter.clear();
|
||||
switch (algorithm) {
|
||||
case RSA: {
|
||||
replaceArrayAdapterContent(keySizeAdapter, R.array.rsa_key_size_spinner_values);
|
||||
mKeySizeSpinner.setSelection(1);
|
||||
mKeySizeRow.setVisibility(View.VISIBLE);
|
||||
mCurveRow.setVisibility(View.GONE);
|
||||
mCustomKeyInfoTextView.setText(getResources().getString(R.string.key_size_custom_info_rsa));
|
||||
// allowed flags:
|
||||
mFlagSign.setEnabled(true);
|
||||
mFlagEncrypt.setEnabled(true);
|
||||
mFlagAuthenticate.setEnabled(true);
|
||||
|
||||
if (mWillBeMasterKey) {
|
||||
mFlagCertify.setEnabled(true);
|
||||
|
||||
mFlagCertify.setChecked(true);
|
||||
mFlagSign.setChecked(false);
|
||||
mFlagEncrypt.setChecked(false);
|
||||
} else {
|
||||
mFlagCertify.setEnabled(false);
|
||||
|
||||
mFlagCertify.setChecked(false);
|
||||
mFlagSign.setChecked(true);
|
||||
mFlagEncrypt.setChecked(true);
|
||||
}
|
||||
mFlagAuthenticate.setChecked(false);
|
||||
break;
|
||||
}
|
||||
case ELGAMAL: {
|
||||
replaceArrayAdapterContent(keySizeAdapter, R.array.elgamal_key_size_spinner_values);
|
||||
mKeySizeSpinner.setSelection(3);
|
||||
mKeySizeRow.setVisibility(View.VISIBLE);
|
||||
mCurveRow.setVisibility(View.GONE);
|
||||
mCustomKeyInfoTextView.setText(""); // ElGamal does not support custom key length
|
||||
// allowed flags:
|
||||
mFlagCertify.setChecked(false);
|
||||
mFlagCertify.setEnabled(false);
|
||||
mFlagSign.setChecked(false);
|
||||
mFlagSign.setEnabled(false);
|
||||
mFlagEncrypt.setChecked(true);
|
||||
mFlagEncrypt.setEnabled(true);
|
||||
mFlagAuthenticate.setChecked(false);
|
||||
mFlagAuthenticate.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
case DSA: {
|
||||
replaceArrayAdapterContent(keySizeAdapter, R.array.dsa_key_size_spinner_values);
|
||||
mKeySizeSpinner.setSelection(2);
|
||||
mKeySizeRow.setVisibility(View.VISIBLE);
|
||||
mCurveRow.setVisibility(View.GONE);
|
||||
mCustomKeyInfoTextView.setText(getResources().getString(R.string.key_size_custom_info_dsa));
|
||||
// allowed flags:
|
||||
mFlagCertify.setChecked(false);
|
||||
mFlagCertify.setEnabled(false);
|
||||
mFlagSign.setChecked(true);
|
||||
mFlagSign.setEnabled(true);
|
||||
mFlagEncrypt.setChecked(false);
|
||||
mFlagEncrypt.setEnabled(false);
|
||||
mFlagAuthenticate.setChecked(false);
|
||||
mFlagAuthenticate.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
case ECDSA: {
|
||||
mKeySizeRow.setVisibility(View.GONE);
|
||||
mCurveRow.setVisibility(View.VISIBLE);
|
||||
mCustomKeyInfoTextView.setText("");
|
||||
// allowed flags:
|
||||
mFlagCertify.setEnabled(mWillBeMasterKey);
|
||||
mFlagCertify.setChecked(mWillBeMasterKey);
|
||||
mFlagSign.setEnabled(true);
|
||||
mFlagSign.setChecked(!mWillBeMasterKey);
|
||||
mFlagEncrypt.setEnabled(false);
|
||||
mFlagEncrypt.setChecked(false);
|
||||
mFlagAuthenticate.setEnabled(true);
|
||||
mFlagAuthenticate.setChecked(false);
|
||||
break;
|
||||
}
|
||||
case ECDH: {
|
||||
mKeySizeRow.setVisibility(View.GONE);
|
||||
mCurveRow.setVisibility(View.VISIBLE);
|
||||
mCustomKeyInfoTextView.setText("");
|
||||
// allowed flags:
|
||||
mFlagCertify.setChecked(false);
|
||||
mFlagCertify.setEnabled(false);
|
||||
mFlagSign.setChecked(false);
|
||||
mFlagSign.setEnabled(false);
|
||||
mFlagEncrypt.setChecked(true);
|
||||
mFlagEncrypt.setEnabled(true);
|
||||
mFlagAuthenticate.setChecked(false);
|
||||
mFlagAuthenticate.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
keySizeAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private void replaceArrayAdapterContent(ArrayAdapter<CharSequence> arrayAdapter, int stringArrayResourceId) {
|
||||
final String[] spinnerValuesStringArray = getResources().getStringArray(stringArrayResourceId);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
arrayAdapter.addAll(spinnerValuesStringArray);
|
||||
} else {
|
||||
for (final String value : spinnerValuesStringArray) {
|
||||
arrayAdapter.add(value);
|
||||
}
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-7
@@ -35,7 +35,11 @@ public class EditSubkeyDialogFragment extends DialogFragment {
|
||||
public static final int MESSAGE_CHANGE_EXPIRY = 1;
|
||||
public static final int MESSAGE_REVOKE = 2;
|
||||
public static final int MESSAGE_STRIP = 3;
|
||||
public static final int MESSAGE_MOVE_KEY_TO_CARD = 4;
|
||||
public static final int MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN = 4;
|
||||
public static final int SUBKEY_MENU_CHANGE_EXPIRY = 0;
|
||||
public static final int SUBKEY_MENU_REVOKE_SUBKEY = 1;
|
||||
public static final int SUBKEY_MENU_STRIP_SUBKEY = 2;
|
||||
public static final int SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN = 3;
|
||||
|
||||
private Messenger mMessenger;
|
||||
|
||||
@@ -68,17 +72,17 @@ public class EditSubkeyDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0:
|
||||
case SUBKEY_MENU_CHANGE_EXPIRY:
|
||||
sendMessageToHandler(MESSAGE_CHANGE_EXPIRY, null);
|
||||
break;
|
||||
case 1:
|
||||
case SUBKEY_MENU_REVOKE_SUBKEY:
|
||||
sendMessageToHandler(MESSAGE_REVOKE, null);
|
||||
break;
|
||||
case 2:
|
||||
sendMessageToHandler(MESSAGE_STRIP, null);
|
||||
case SUBKEY_MENU_STRIP_SUBKEY:
|
||||
showAlertDialog();
|
||||
break;
|
||||
case 3:
|
||||
sendMessageToHandler(MESSAGE_MOVE_KEY_TO_CARD, null);
|
||||
case SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN:
|
||||
sendMessageToHandler(MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN, null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -95,6 +99,25 @@ public class EditSubkeyDialogFragment extends DialogFragment {
|
||||
return builder.show();
|
||||
}
|
||||
|
||||
private void showAlertDialog() {
|
||||
CustomAlertDialogBuilder stripAlertDialog = new CustomAlertDialogBuilder(getActivity());
|
||||
stripAlertDialog.setTitle(R.string.title_alert_strip).
|
||||
setMessage(R.string.alert_strip).setCancelable(true);
|
||||
stripAlertDialog.setPositiveButton(R.string.strip, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
sendMessageToHandler(MESSAGE_STRIP, null);
|
||||
}
|
||||
});
|
||||
stripAlertDialog.setNegativeButton(R.string.btn_do_not_save, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
stripAlertDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
|
||||
private static Boolean checkHandle(String handle) {
|
||||
try {
|
||||
HttpURLConnection nection =
|
||||
(HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
|
||||
(HttpURLConnection) new URL("https://twitter.com/" + handle).getUrlResponse();
|
||||
nection.setRequestMethod("HEAD");
|
||||
nection.setRequestProperty("User-Agent", "OpenKeychain");
|
||||
return nection.getResponseCode() == 200;
|
||||
|
||||
+1
-1
@@ -64,8 +64,8 @@ public class KeyFormattingUtils {
|
||||
String algorithmStr;
|
||||
|
||||
switch (algorithm) {
|
||||
case PublicKeyAlgorithmTags.RSA_ENCRYPT:
|
||||
case PublicKeyAlgorithmTags.RSA_GENERAL:
|
||||
case PublicKeyAlgorithmTags.RSA_ENCRYPT:
|
||||
case PublicKeyAlgorithmTags.RSA_SIGN: {
|
||||
algorithmStr = "RSA";
|
||||
break;
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Alex Fong Jie Wen <alexfongg@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.util.spinner;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Spinner;
|
||||
|
||||
/**
|
||||
* Custom spinner which uses a hack to
|
||||
* always set focus on first item in list
|
||||
*
|
||||
*/
|
||||
public class FocusFirstItemSpinner extends Spinner {
|
||||
/**
|
||||
* Spinner is originally designed to set focus on the currently selected item.
|
||||
* When Spinner is selected to show dropdown, 'performClick()' is called internally.
|
||||
* 'getSelectedItemPosition()' is then called to obtain the item to focus on.
|
||||
* We use a toggle to have 'getSelectedItemPosition()' return the 0th index
|
||||
* for this particular case.
|
||||
*/
|
||||
|
||||
private boolean mToggleFlag = true;
|
||||
|
||||
public FocusFirstItemSpinner(Context context, AttributeSet attrs,
|
||||
int defStyle, int mode) {
|
||||
super(context, attrs, defStyle, mode);
|
||||
}
|
||||
|
||||
public FocusFirstItemSpinner(Context context, AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public FocusFirstItemSpinner(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public FocusFirstItemSpinner(Context context, int mode) {
|
||||
super(context, mode);
|
||||
}
|
||||
|
||||
public FocusFirstItemSpinner(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedItemPosition() {
|
||||
if (!mToggleFlag) {
|
||||
return 0;
|
||||
}
|
||||
return super.getSelectedItemPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performClick() {
|
||||
mToggleFlag = false;
|
||||
boolean result = super.performClick();
|
||||
mToggleFlag = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-5
@@ -113,11 +113,6 @@ public class CertifyKeySpinner extends KeySpinner {
|
||||
|
||||
@Override
|
||||
boolean isItemEnabled(Cursor cursor) {
|
||||
// "none" entry is always enabled!
|
||||
if (cursor.getPosition() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+1
-18
@@ -23,15 +23,11 @@ import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Patterns;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.ContactHelper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
public class EmailEditText extends AppCompatAutoCompleteTextView {
|
||||
|
||||
public EmailEditText(Context context) {
|
||||
@@ -70,20 +66,7 @@ public class EmailEditText extends AppCompatAutoCompleteTextView {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
String email = editable.toString();
|
||||
if (email.length() > 0) {
|
||||
Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email);
|
||||
if (emailMatcher.matches()) {
|
||||
EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0,
|
||||
R.drawable.ic_stat_retyped_ok, 0);
|
||||
} else {
|
||||
EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0,
|
||||
R.drawable.ic_stat_retyped_bad, 0);
|
||||
}
|
||||
} else {
|
||||
// remove drawable if email is empty
|
||||
EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user