New Gradle project structure
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package org.openintents.openpgp;
|
||||
|
||||
public class OpenPgpConstants {
|
||||
|
||||
public static final String TAG = "OpenPgp API";
|
||||
|
||||
public static final int REQUIRED_API_VERSION = 1;
|
||||
public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openintents.openpgp;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class OpenPgpData implements Parcelable {
|
||||
public static final int TYPE_STRING = 0;
|
||||
public static final int TYPE_BYTE_ARRAY = 1;
|
||||
public static final int TYPE_FILE_DESCRIPTOR = 2;
|
||||
public static final int TYPE_URI = 3;
|
||||
|
||||
int type;
|
||||
|
||||
String string;
|
||||
byte[] bytes = new byte[0];
|
||||
ParcelFileDescriptor fileDescriptor;
|
||||
Uri uri;
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getString() {
|
||||
return string;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor getFileDescriptor() {
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public OpenPgpData() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Not a real constructor. This can be used to define requested output type.
|
||||
*
|
||||
* @param type
|
||||
*/
|
||||
public OpenPgpData(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public OpenPgpData(String string) {
|
||||
this.string = string;
|
||||
this.type = TYPE_STRING;
|
||||
}
|
||||
|
||||
public OpenPgpData(byte[] bytes) {
|
||||
this.bytes = bytes;
|
||||
this.type = TYPE_BYTE_ARRAY;
|
||||
}
|
||||
|
||||
public OpenPgpData(ParcelFileDescriptor fileDescriptor) {
|
||||
this.fileDescriptor = fileDescriptor;
|
||||
this.type = TYPE_FILE_DESCRIPTOR;
|
||||
}
|
||||
|
||||
public OpenPgpData(Uri uri) {
|
||||
this.uri = uri;
|
||||
this.type = TYPE_URI;
|
||||
}
|
||||
|
||||
public OpenPgpData(OpenPgpData b) {
|
||||
this.string = b.string;
|
||||
this.bytes = b.bytes;
|
||||
this.fileDescriptor = b.fileDescriptor;
|
||||
this.uri = b.uri;
|
||||
}
|
||||
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(type);
|
||||
dest.writeString(string);
|
||||
dest.writeInt(bytes.length);
|
||||
dest.writeByteArray(bytes);
|
||||
dest.writeParcelable(fileDescriptor, 0);
|
||||
dest.writeParcelable(uri, 0);
|
||||
}
|
||||
|
||||
public static final Creator<OpenPgpData> CREATOR = new Creator<OpenPgpData>() {
|
||||
public OpenPgpData createFromParcel(final Parcel source) {
|
||||
OpenPgpData vr = new OpenPgpData();
|
||||
vr.type = source.readInt();
|
||||
vr.string = source.readString();
|
||||
vr.bytes = new byte[source.readInt()];
|
||||
source.readByteArray(vr.bytes);
|
||||
vr.fileDescriptor = source.readParcelable(ParcelFileDescriptor.class.getClassLoader());
|
||||
vr.fileDescriptor = source.readParcelable(Uri.class.getClassLoader());
|
||||
return vr;
|
||||
}
|
||||
|
||||
public OpenPgpData[] newArray(final int size) {
|
||||
return new OpenPgpData[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openintents.openpgp;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class OpenPgpError implements Parcelable {
|
||||
public static final int GENERIC_ERROR = 0;
|
||||
public static final int NO_OR_WRONG_PASSPHRASE = 1;
|
||||
public static final int NO_USER_IDS = 2;
|
||||
public static final int USER_INTERACTION_REQUIRED = 3;
|
||||
|
||||
int errorId;
|
||||
String message;
|
||||
|
||||
public OpenPgpError() {
|
||||
}
|
||||
|
||||
public OpenPgpError(int errorId, String message) {
|
||||
this.errorId = errorId;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public OpenPgpError(OpenPgpError b) {
|
||||
this.errorId = b.errorId;
|
||||
this.message = b.message;
|
||||
}
|
||||
|
||||
public int getErrorId() {
|
||||
return errorId;
|
||||
}
|
||||
|
||||
public void setErrorId(int errorId) {
|
||||
this.errorId = errorId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(errorId);
|
||||
dest.writeString(message);
|
||||
}
|
||||
|
||||
public static final Creator<OpenPgpError> CREATOR = new Creator<OpenPgpError>() {
|
||||
public OpenPgpError createFromParcel(final Parcel source) {
|
||||
OpenPgpError error = new OpenPgpError();
|
||||
error.errorId = source.readInt();
|
||||
error.message = source.readString();
|
||||
return error;
|
||||
}
|
||||
|
||||
public OpenPgpError[] newArray(final int size) {
|
||||
return new OpenPgpError[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openintents.openpgp;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ResolveInfo;
|
||||
|
||||
public class OpenPgpHelper {
|
||||
private Context context;
|
||||
|
||||
public static Pattern PGP_MESSAGE = Pattern.compile(
|
||||
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
|
||||
|
||||
public static Pattern PGP_SIGNED_MESSAGE = Pattern
|
||||
.compile(
|
||||
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
|
||||
Pattern.DOTALL);
|
||||
|
||||
public OpenPgpHelper(Context context) {
|
||||
super();
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
|
||||
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(intent, 0);
|
||||
if (!resInfo.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openintents.openpgp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.AlertDialog.Builder;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class OpenPgpListPreference extends DialogPreference {
|
||||
ArrayList<OpenPgpProviderEntry> mProviderList = new ArrayList<OpenPgpProviderEntry>();
|
||||
private String mSelectedPackage;
|
||||
|
||||
public OpenPgpListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(
|
||||
new Intent(OpenPgpConstants.SERVICE_INTENT), PackageManager.GET_META_DATA);
|
||||
if (!resInfo.isEmpty()) {
|
||||
for (ResolveInfo resolveInfo : resInfo) {
|
||||
if (resolveInfo.serviceInfo == null)
|
||||
continue;
|
||||
|
||||
String packageName = resolveInfo.serviceInfo.packageName;
|
||||
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(context
|
||||
.getPackageManager()));
|
||||
Drawable icon = resolveInfo.serviceInfo.loadIcon(context.getPackageManager());
|
||||
|
||||
// get api version
|
||||
ServiceInfo si = resolveInfo.serviceInfo;
|
||||
int apiVersion = si.metaData.getInt("api_version");
|
||||
|
||||
mProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon,
|
||||
apiVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OpenPgpListPreference(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to add "no selection"
|
||||
*
|
||||
* @param packageName
|
||||
* @param simpleName
|
||||
* @param icon
|
||||
*/
|
||||
public void addProvider(int position, String packageName, String simpleName, Drawable icon,
|
||||
int apiVersion) {
|
||||
mProviderList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon,
|
||||
apiVersion));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(Builder builder) {
|
||||
// Init ArrayAdapter with OpenPGP Providers
|
||||
ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getContext(),
|
||||
android.R.layout.select_dialog_singlechoice, android.R.id.text1, mProviderList) {
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
// User super class to create the View
|
||||
View v = super.getView(position, convertView, parent);
|
||||
TextView tv = (TextView) v.findViewById(android.R.id.text1);
|
||||
|
||||
// Put the image on the TextView
|
||||
tv.setCompoundDrawablesWithIntrinsicBounds(mProviderList.get(position).icon, null,
|
||||
null, null);
|
||||
|
||||
// Add margin between image and text (support various screen densities)
|
||||
int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f);
|
||||
tv.setCompoundDrawablePadding(dp10);
|
||||
|
||||
// disable if it has the wrong api_version
|
||||
if (mProviderList.get(position).apiVersion == OpenPgpConstants.REQUIRED_API_VERSION) {
|
||||
tv.setEnabled(true);
|
||||
} else {
|
||||
tv.setEnabled(false);
|
||||
tv.setText(tv.getText() + " (API v" + mProviderList.get(position).apiVersion
|
||||
+ ", needs v" + OpenPgpConstants.REQUIRED_API_VERSION + ")");
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
builder.setSingleChoiceItems(adapter, getIndexOfProviderList(getValue()),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mSelectedPackage = mProviderList.get(which).packageName;
|
||||
|
||||
/*
|
||||
* Clicking on an item simulates the positive button click, and dismisses
|
||||
* the dialog.
|
||||
*/
|
||||
OpenPgpListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* The typical interaction for list-based dialogs is to have click-on-an-item dismiss the
|
||||
* dialog instead of the user having to press 'Ok'.
|
||||
*/
|
||||
builder.setPositiveButton(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
super.onDialogClosed(positiveResult);
|
||||
|
||||
if (positiveResult && (mSelectedPackage != null)) {
|
||||
if (callChangeListener(mSelectedPackage)) {
|
||||
setValue(mSelectedPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getIndexOfProviderList(String packageName) {
|
||||
for (OpenPgpProviderEntry app : mProviderList) {
|
||||
if (app.packageName.equals(packageName)) {
|
||||
return mProviderList.indexOf(app);
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void setValue(String packageName) {
|
||||
mSelectedPackage = packageName;
|
||||
persistString(packageName);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return mSelectedPackage;
|
||||
}
|
||||
|
||||
public String getEntry() {
|
||||
return getEntryByValue(mSelectedPackage);
|
||||
}
|
||||
|
||||
public String getEntryByValue(String packageName) {
|
||||
for (OpenPgpProviderEntry app : mProviderList) {
|
||||
if (app.packageName.equals(packageName)) {
|
||||
return app.simpleName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class OpenPgpProviderEntry {
|
||||
private String packageName;
|
||||
private String simpleName;
|
||||
private Drawable icon;
|
||||
private int apiVersion;
|
||||
|
||||
public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon,
|
||||
int apiVersion) {
|
||||
this.packageName = packageName;
|
||||
this.simpleName = simpleName;
|
||||
this.icon = icon;
|
||||
this.apiVersion = apiVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return simpleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openintents.openpgp;
|
||||
|
||||
import org.openintents.openpgp.IOpenPgpService;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
public class OpenPgpServiceConnection {
|
||||
private Context mApplicationContext;
|
||||
|
||||
private IOpenPgpService mService;
|
||||
private boolean mBound;
|
||||
private String mCryptoProviderPackageName;
|
||||
|
||||
public OpenPgpServiceConnection(Context context, String cryptoProviderPackageName) {
|
||||
this.mApplicationContext = context.getApplicationContext();
|
||||
this.mCryptoProviderPackageName = cryptoProviderPackageName;
|
||||
}
|
||||
|
||||
public IOpenPgpService getService() {
|
||||
return mService;
|
||||
}
|
||||
|
||||
public boolean isBound() {
|
||||
return mBound;
|
||||
}
|
||||
|
||||
private ServiceConnection mCryptoServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mService = IOpenPgpService.Stub.asInterface(service);
|
||||
Log.d(OpenPgpConstants.TAG, "connected to service");
|
||||
mBound = true;
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mService = null;
|
||||
Log.d(OpenPgpConstants.TAG, "disconnected from service");
|
||||
mBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If not already bound, bind!
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean bindToService() {
|
||||
if (mService == null && !mBound) { // if not already connected
|
||||
try {
|
||||
Log.d(OpenPgpConstants.TAG, "not bound yet");
|
||||
|
||||
Intent serviceIntent = new Intent();
|
||||
serviceIntent.setAction(IOpenPgpService.class.getName());
|
||||
serviceIntent.setPackage(mCryptoProviderPackageName);
|
||||
mApplicationContext.bindService(serviceIntent, mCryptoServiceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.d(OpenPgpConstants.TAG, "Exception on binding", e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Log.d(OpenPgpConstants.TAG, "already bound");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void unbindFromService() {
|
||||
mApplicationContext.unbindService(mCryptoServiceConnection);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.openintents.openpgp;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class OpenPgpSignatureResult implements Parcelable {
|
||||
// generic error on signature verification
|
||||
public static final int SIGNATURE_ERROR = 0;
|
||||
// successfully verified signature, with trusted public key
|
||||
public static final int SIGNATURE_SUCCESS_TRUSTED = 1;
|
||||
// no public key was found for this signature verification
|
||||
// you can retrieve the key with
|
||||
// getKeys(new String[] {String.valueOf(signatureResult.getKeyId)}, true, callback)
|
||||
public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2;
|
||||
// successfully verified signature, but with untrusted public key
|
||||
public static final int SIGNATURE_SUCCESS_UNTRUSTED = 3;
|
||||
|
||||
int status;
|
||||
boolean signatureOnly;
|
||||
String userId;
|
||||
long keyId;
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean isSignatureOnly() {
|
||||
return signatureOnly;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public long getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public OpenPgpSignatureResult() {
|
||||
|
||||
}
|
||||
|
||||
public OpenPgpSignatureResult(int signatureStatus, String signatureUserId,
|
||||
boolean signatureOnly, long keyId) {
|
||||
this.status = signatureStatus;
|
||||
this.signatureOnly = signatureOnly;
|
||||
this.userId = signatureUserId;
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
public OpenPgpSignatureResult(OpenPgpSignatureResult b) {
|
||||
this.status = b.status;
|
||||
this.userId = b.userId;
|
||||
this.signatureOnly = b.signatureOnly;
|
||||
this.keyId = b.keyId;
|
||||
}
|
||||
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(status);
|
||||
dest.writeByte((byte) (signatureOnly ? 1 : 0));
|
||||
dest.writeString(userId);
|
||||
dest.writeLong(keyId);
|
||||
}
|
||||
|
||||
public static final Creator<OpenPgpSignatureResult> CREATOR = new Creator<OpenPgpSignatureResult>() {
|
||||
public OpenPgpSignatureResult createFromParcel(final Parcel source) {
|
||||
OpenPgpSignatureResult vr = new OpenPgpSignatureResult();
|
||||
vr.status = source.readInt();
|
||||
vr.signatureOnly = source.readByte() == 1;
|
||||
vr.userId = source.readString();
|
||||
vr.keyId = source.readLong();
|
||||
return vr;
|
||||
}
|
||||
|
||||
public OpenPgpSignatureResult[] newArray(final int size) {
|
||||
return new OpenPgpSignatureResult[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String out = new String();
|
||||
out += "\nstatus: " + status;
|
||||
out += "\nuserId: " + userId;
|
||||
out += "\nsignatureOnly: " + signatureOnly;
|
||||
out += "\nkeyId: " + keyId;
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain;
|
||||
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import android.os.Environment;
|
||||
|
||||
public final class Constants {
|
||||
|
||||
public static final boolean DEBUG = BuildConfig.DEBUG;
|
||||
|
||||
public static final String TAG = "Keychain";
|
||||
|
||||
public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain";
|
||||
|
||||
// as defined in http://tools.ietf.org/html/rfc3156, section 7
|
||||
public static final String NFC_MIME = "application/pgp-keys";
|
||||
|
||||
// Not BC due to the use of Spongy Castle for Android
|
||||
public static final String SC = BouncyCastleProvider.PROVIDER_NAME;
|
||||
public static final String BOUNCY_CASTLE_PROVIDER_NAME = SC;
|
||||
|
||||
public static final String INTENT_PREFIX = PACKAGE_NAME + ".action.";
|
||||
|
||||
public static final class path {
|
||||
public static final String APP_DIR = Environment.getExternalStorageDirectory()
|
||||
+ "/OpenPGP-Keychain";
|
||||
}
|
||||
|
||||
public static final class pref {
|
||||
public static final String DEFAULT_ENCRYPTION_ALGORITHM = "defaultEncryptionAlgorithm";
|
||||
public static final String DEFAULT_HASH_ALGORITHM = "defaultHashAlgorithm";
|
||||
public static final String DEFAULT_ASCII_ARMOUR = "defaultAsciiArmour";
|
||||
public static final String DEFAULT_MESSAGE_COMPRESSION = "defaultMessageCompression";
|
||||
public static final String DEFAULT_FILE_COMPRESSION = "defaultFileCompression";
|
||||
public static final String PASS_PHRASE_CACHE_TTL = "passPhraseCacheTtl";
|
||||
public static final String LANGUAGE = "language";
|
||||
public static final String FORCE_V3_SIGNATURES = "forceV3Signatures";
|
||||
public static final String KEY_SERVERS = "keyServers";
|
||||
}
|
||||
|
||||
public static final class defaults {
|
||||
public static final String KEY_SERVERS = "pool.sks-keyservers.net, subkeys.pgp.net, pgp.mit.edu";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain;
|
||||
|
||||
import org.spongycastle.bcpg.CompressionAlgorithmTags;
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO:
|
||||
*
|
||||
* - refactor ids, some are not needed and can be done with xml
|
||||
*
|
||||
*/
|
||||
public final class Id {
|
||||
|
||||
public static final class menu {
|
||||
|
||||
public static final class option {
|
||||
public static final int new_pass_phrase = 0x21070001;
|
||||
public static final int create = 0x21070002;
|
||||
public static final int about = 0x21070003;
|
||||
public static final int manage_public_keys = 0x21070004;
|
||||
public static final int manage_secret_keys = 0x21070005;
|
||||
public static final int export_keys = 0x21070007;
|
||||
public static final int preferences = 0x21070008;
|
||||
public static final int search = 0x21070009;
|
||||
public static final int help = 0x21070010;
|
||||
public static final int key_server = 0x21070011;
|
||||
public static final int scanQRCode = 0x21070012;
|
||||
public static final int encrypt = 0x21070013;
|
||||
public static final int encrypt_to_clipboard = 0x21070014;
|
||||
public static final int decrypt = 0x21070015;
|
||||
public static final int reply = 0x21070016;
|
||||
public static final int cancel = 0x21070017;
|
||||
public static final int save = 0x21070018;
|
||||
public static final int okay = 0x21070019;
|
||||
public static final int import_from_file = 0x21070020;
|
||||
public static final int import_from_qr_code = 0x21070021;
|
||||
public static final int import_from_nfc = 0x21070022;
|
||||
public static final int crypto_consumers = 0x21070023;
|
||||
public static final int createExpert = 0x21070024;
|
||||
}
|
||||
}
|
||||
|
||||
// use only lower 16 bits due to compatibility lib
|
||||
public static final class message {
|
||||
public static final int progress_update = 0x00006001;
|
||||
public static final int done = 0x00006002;
|
||||
public static final int import_keys = 0x00006003;
|
||||
public static final int export_keys = 0x00006004;
|
||||
public static final int import_done = 0x00006005;
|
||||
public static final int export_done = 0x00006006;
|
||||
public static final int create_key = 0x00006007;
|
||||
public static final int edit_key = 0x00006008;
|
||||
public static final int delete_done = 0x00006009;
|
||||
public static final int query_done = 0x00006010;
|
||||
public static final int unknown_signature_key = 0x00006011;
|
||||
}
|
||||
|
||||
// use only lower 16 bits due to compatibility lib
|
||||
public static final class request {
|
||||
public static final int public_keys = 0x00007001;
|
||||
public static final int secret_keys = 0x00007002;
|
||||
public static final int filename = 0x00007003;
|
||||
// public static final int output_filename = 0x00007004;
|
||||
public static final int key_server_preference = 0x00007005;
|
||||
public static final int look_up_key_id = 0x00007006;
|
||||
public static final int export_to_server = 0x00007007;
|
||||
public static final int import_from_qr_code = 0x00007008;
|
||||
public static final int sign_key = 0x00007009;
|
||||
}
|
||||
|
||||
public static final class dialog {
|
||||
public static final int pass_phrase = 0x21070001;
|
||||
public static final int encrypting = 0x21070002;
|
||||
public static final int decrypting = 0x21070003;
|
||||
public static final int new_pass_phrase = 0x21070004;
|
||||
public static final int pass_phrases_do_not_match = 0x21070005;
|
||||
public static final int no_pass_phrase = 0x21070006;
|
||||
public static final int saving = 0x21070007;
|
||||
public static final int delete_key = 0x21070008;
|
||||
public static final int import_keys = 0x21070009;
|
||||
public static final int importing = 0x2107000a;
|
||||
public static final int export_key = 0x2107000b;
|
||||
public static final int export_keys = 0x2107000c;
|
||||
public static final int exporting = 0x2107000d;
|
||||
public static final int new_account = 0x2107000e;
|
||||
public static final int change_log = 0x21070010;
|
||||
public static final int output_filename = 0x21070011;
|
||||
public static final int delete_file = 0x21070012;
|
||||
public static final int deleting = 0x21070013;
|
||||
public static final int help = 0x21070014;
|
||||
public static final int querying = 0x21070015;
|
||||
public static final int lookup_unknown_key = 0x21070016;
|
||||
public static final int signing = 0x21070017;
|
||||
}
|
||||
|
||||
public static final class task {
|
||||
public static final int import_keys = 0x21070001;
|
||||
public static final int export_keys = 0x21070002;
|
||||
}
|
||||
|
||||
public static final class type {
|
||||
public static final int public_key = 0x21070001;
|
||||
public static final int secret_key = 0x21070002;
|
||||
public static final int user_id = 0x21070003;
|
||||
public static final int key = 0x21070004;
|
||||
}
|
||||
|
||||
public static final class choice {
|
||||
public static final class algorithm {
|
||||
public static final int dsa = 0x21070001;
|
||||
public static final int elgamal = 0x21070002;
|
||||
public static final int rsa = 0x21070003;
|
||||
}
|
||||
|
||||
public static final class compression {
|
||||
public static final int none = 0x21070001;
|
||||
public static final int zlib = CompressionAlgorithmTags.ZLIB;
|
||||
public static final int bzip2 = CompressionAlgorithmTags.BZIP2;
|
||||
public static final int zip = CompressionAlgorithmTags.ZIP;
|
||||
}
|
||||
|
||||
public static final class usage {
|
||||
public static final int sign_only = 0x21070001;
|
||||
public static final int encrypt_only = 0x21070002;
|
||||
public static final int sign_and_encrypt = 0x21070003;
|
||||
}
|
||||
|
||||
public static final class action {
|
||||
public static final int encrypt = 0x21070001;
|
||||
public static final int decrypt = 0x21070002;
|
||||
public static final int import_public = 0x21070003;
|
||||
public static final int import_secret = 0x21070004;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class return_value {
|
||||
public static final int ok = 0;
|
||||
public static final int error = -1;
|
||||
public static final int no_master_key = -2;
|
||||
public static final int updated = 1;
|
||||
public static final int bad = -3;
|
||||
}
|
||||
|
||||
public static final class target {
|
||||
public static final int clipboard = 0x21070001;
|
||||
public static final int email = 0x21070002;
|
||||
public static final int file = 0x21070003;
|
||||
public static final int message = 0x21070004;
|
||||
}
|
||||
|
||||
public static final class mode {
|
||||
public static final int undefined = 0x21070001;
|
||||
public static final int byte_array = 0x21070002;
|
||||
public static final int file = 0x21070003;
|
||||
public static final int stream = 0x21070004;
|
||||
}
|
||||
|
||||
public static final class key {
|
||||
public static final int none = 0;
|
||||
public static final int symmetric = -1;
|
||||
}
|
||||
|
||||
public static final class content {
|
||||
public static final int unknown = 0;
|
||||
public static final int encrypted_data = 1;
|
||||
public static final int keys = 2;
|
||||
}
|
||||
|
||||
public static final class keyserver {
|
||||
public static final int search = 0x21070001;
|
||||
public static final int get = 0x21070002;
|
||||
public static final int add = 0x21070003;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.PRNGFixes;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Environment;
|
||||
|
||||
public class KeychainApplication extends Application {
|
||||
|
||||
/**
|
||||
* Called when the application is starting, before any activity, service, or receiver objects
|
||||
* (excluding content providers) have been created.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
/*
|
||||
* Sets Bouncy (Spongy) Castle as preferred security provider
|
||||
*
|
||||
* insertProviderAt() position starts from 1
|
||||
*/
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
||||
|
||||
/*
|
||||
* apply RNG fixes
|
||||
*
|
||||
* among other things, executes Security.insertProviderAt(new
|
||||
* LinuxPRNGSecureRandomProvider(), 1) for Android <= SDK 17
|
||||
*/
|
||||
PRNGFixes.apply();
|
||||
Log.d(Constants.TAG, "Bouncy Castle set and PRNG Fixes applied!");
|
||||
|
||||
if (Constants.DEBUG) {
|
||||
Provider[] providers = Security.getProviders();
|
||||
Log.d(Constants.TAG, "Installed Security Providers:");
|
||||
for (Provider p : providers) {
|
||||
Log.d(Constants.TAG, "provider class: " + p.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Create APG directory on sdcard if not existing
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
File dir = new File(Constants.path.APP_DIR);
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
// ignore this for now, it's not crucial
|
||||
// that the directory doesn't exist at this point
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.compatibility;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class ClipboardReflection {
|
||||
|
||||
private static final String clipboardLabel = "Keychain";
|
||||
|
||||
/**
|
||||
* Wrapper around ClipboardManager based on Android version using Reflection API
|
||||
*
|
||||
* @param context
|
||||
* @param text
|
||||
*/
|
||||
public static void copyToClipboard(Context context, String text) {
|
||||
Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
try {
|
||||
if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
|
||||
Method methodSetText = clipboard.getClass()
|
||||
.getMethod("setText", CharSequence.class);
|
||||
methodSetText.invoke(clipboard, text);
|
||||
} else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
|
||||
Class<?> classClipData = Class.forName("android.content.ClipData");
|
||||
Method methodNewPlainText = classClipData.getMethod("newPlainText",
|
||||
CharSequence.class, CharSequence.class);
|
||||
Object clip = methodNewPlainText.invoke(null, clipboardLabel, text);
|
||||
methodNewPlainText = clipboard.getClass()
|
||||
.getMethod("setPrimaryClip", classClipData);
|
||||
methodNewPlainText.invoke(clipboard, clip);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("ProjectsException", "There was an error copying the text to the clipboard: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around ClipboardManager based on Android version using Reflection API
|
||||
*
|
||||
* @param context
|
||||
* @param text
|
||||
*/
|
||||
public static CharSequence getClipboardText(Context context) {
|
||||
Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
try {
|
||||
if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
|
||||
// CharSequence text = clipboard.getText();
|
||||
Method methodGetText = clipboard.getClass().getMethod("getText");
|
||||
Object text = methodGetText.invoke(clipboard);
|
||||
|
||||
return (CharSequence) text;
|
||||
} else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
|
||||
// ClipData clipData = clipboard.getPrimaryClip();
|
||||
Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip");
|
||||
Object clipData = methodGetPrimaryClip.invoke(clipboard);
|
||||
|
||||
// ClipData.Item clipDataItem = clipData.getItemAt(0);
|
||||
Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class);
|
||||
Object clipDataItem = methodGetItemAt.invoke(clipData, 0);
|
||||
|
||||
// CharSequence text = clipDataItem.coerceToText(context);
|
||||
Method methodGetString = clipDataItem.getClass().getMethod("coerceToText",
|
||||
Context.class);
|
||||
Object text = methodGetString.invoke(clipDataItem, context);
|
||||
|
||||
return (CharSequence) text;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("ProjectsException", "There was an error getting the text from the clipboard: "
|
||||
+ e.getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.compatibility;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
|
||||
/**
|
||||
* Bug on Android >= 4.2
|
||||
*
|
||||
* http://code.google.com/p/android/issues/detail?id=41901
|
||||
*
|
||||
* DialogFragment disappears on pressing home and comming back. This also happens especially in
|
||||
* FileDialogFragment after launching a file manager and coming back.
|
||||
*
|
||||
* Usage: <code>
|
||||
* DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
|
||||
* public void run() {
|
||||
* // show dialog...
|
||||
* }
|
||||
* });
|
||||
* </code>
|
||||
*/
|
||||
public class DialogFragmentWorkaround {
|
||||
public static final SDKLevel17Interface INTERFACE = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) ? new SDKLevel17Impl()
|
||||
: new SDKLevelPriorLevel17Impl());
|
||||
|
||||
private static final int RUNNABLE_DELAY = 300;
|
||||
|
||||
public interface SDKLevel17Interface {
|
||||
// Workaround for http://code.google.com/p/android/issues/detail?id=41901
|
||||
void runnableRunDelayed(Runnable runnable);
|
||||
}
|
||||
|
||||
private static class SDKLevelPriorLevel17Impl implements SDKLevel17Interface {
|
||||
@Override
|
||||
public void runnableRunDelayed(Runnable runnable) {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SDKLevel17Impl implements SDKLevel17Interface {
|
||||
@Override
|
||||
public void runnableRunDelayed(Runnable runnable) {
|
||||
new Handler().postDelayed(runnable, RUNNABLE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
// Can't instantiate this class
|
||||
private DialogFragmentWorkaround() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.compatibility;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockListFragment;
|
||||
|
||||
/**
|
||||
* Bug on Android >= 4.1
|
||||
*
|
||||
* http://code.google.com/p/android/issues/detail?id=35885
|
||||
*
|
||||
* Items are not checked in layout
|
||||
*/
|
||||
public class ListFragmentWorkaround extends SherlockListFragment {
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
l.setItemChecked(position, l.isItemChecked(position));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.helper;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.actionbarsherlock.app.ActionBar;
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
|
||||
public class ActionBarHelper {
|
||||
|
||||
/**
|
||||
* Set actionbar without home button if called from another app
|
||||
*
|
||||
* @param activity
|
||||
*/
|
||||
public static void setBackButton(SherlockFragmentActivity activity) {
|
||||
// set actionbar without home button if called from another app
|
||||
final ActionBar actionBar = activity.getSupportActionBar();
|
||||
Log.d(Constants.TAG, "calling package (only set when using startActivityForResult)="
|
||||
+ activity.getCallingPackage());
|
||||
if (activity.getCallingPackage() != null
|
||||
&& activity.getCallingPackage().equals(Constants.PACKAGE_NAME)) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setHomeButtonEnabled(true);
|
||||
} else {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setHomeButtonEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets custom view on ActionBar for Done/Cancel activities
|
||||
*
|
||||
* @param actionBar
|
||||
* @param doneText
|
||||
* @param doneOnClickListener
|
||||
* @param cancelText
|
||||
* @param cancelOnClickListener
|
||||
*/
|
||||
public static void setDoneCancelView(ActionBar actionBar, int doneText,
|
||||
OnClickListener doneOnClickListener, int cancelText,
|
||||
OnClickListener cancelOnClickListener) {
|
||||
|
||||
// Inflate a "Done"/"Cancel" custom action bar view
|
||||
final LayoutInflater inflater = (LayoutInflater) actionBar.getThemedContext()
|
||||
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
|
||||
final View customActionBarView = inflater.inflate(
|
||||
R.layout.actionbar_custom_view_done_cancel, null);
|
||||
|
||||
((TextView) customActionBarView.findViewById(R.id.actionbar_done_text)).setText(doneText);
|
||||
customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener(
|
||||
doneOnClickListener);
|
||||
((TextView) customActionBarView.findViewById(R.id.actionbar_cancel_text))
|
||||
.setText(cancelText);
|
||||
customActionBarView.findViewById(R.id.actionbar_cancel).setOnClickListener(
|
||||
cancelOnClickListener);
|
||||
|
||||
// Show the custom action bar view and hide the normal Home icon and title.
|
||||
actionBar.setDisplayShowTitleEnabled(false);
|
||||
actionBar.setDisplayShowHomeEnabled(false);
|
||||
actionBar.setDisplayShowCustomEnabled(true);
|
||||
actionBar.setCustomView(customActionBarView, new ActionBar.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets custom view on ActionBar for Done activities
|
||||
*
|
||||
* @param actionBar
|
||||
* @param doneText
|
||||
* @param doneOnClickListener
|
||||
*/
|
||||
public static void setDoneView(ActionBar actionBar, int doneText,
|
||||
OnClickListener doneOnClickListener) {
|
||||
// Inflate a "Done" custom action bar view to serve as the "Up" affordance.
|
||||
final LayoutInflater inflater = (LayoutInflater) actionBar.getThemedContext()
|
||||
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
|
||||
final View customActionBarView = inflater
|
||||
.inflate(R.layout.actionbar_custom_view_done, null);
|
||||
|
||||
((TextView) customActionBarView.findViewById(R.id.actionbar_done_text)).setText(doneText);
|
||||
customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener(
|
||||
doneOnClickListener);
|
||||
|
||||
// Show the custom action bar view and hide the normal Home icon and title.
|
||||
actionBar.setDisplayShowTitleEnabled(false);
|
||||
actionBar.setDisplayShowHomeEnabled(false);
|
||||
actionBar.setDisplayShowCustomEnabled(true);
|
||||
actionBar.setCustomView(customActionBarView);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.helper;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
|
||||
public class ExportHelper {
|
||||
protected FileDialogFragment mFileDialog;
|
||||
protected String mExportFilename;
|
||||
|
||||
SherlockFragmentActivity activity;
|
||||
|
||||
public ExportHelper(SherlockFragmentActivity activity) {
|
||||
super();
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public void deleteKey(Uri dataUri, final int keyType, Handler deleteHandler) {
|
||||
long keyRingRowId = Long.valueOf(dataUri.getLastPathSegment());
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(deleteHandler);
|
||||
|
||||
DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger,
|
||||
new long[] { keyRingRowId }, keyType);
|
||||
|
||||
deleteKeyDialog.show(activity.getSupportFragmentManager(), "deleteKeyDialog");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dialog where to export keys
|
||||
*
|
||||
* @param keyRingMasterKeyId
|
||||
* if -1 export all keys
|
||||
*/
|
||||
public void showExportKeysDialog(final Uri dataUri, final int keyType,
|
||||
final String exportFilename) {
|
||||
mExportFilename = exportFilename;
|
||||
|
||||
// Message is received after file is selected
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == FileDialogFragment.MESSAGE_OKAY) {
|
||||
Bundle data = message.getData();
|
||||
mExportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
|
||||
|
||||
exportKeys(dataUri, keyType);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
final Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
|
||||
public void run() {
|
||||
String title = null;
|
||||
if (dataUri == null) {
|
||||
// export all keys
|
||||
title = activity.getString(R.string.title_export_keys);
|
||||
} else {
|
||||
// export only key specified at data uri
|
||||
title = activity.getString(R.string.title_export_key);
|
||||
}
|
||||
|
||||
String message = null;
|
||||
if (keyType == Id.type.public_key) {
|
||||
message = activity.getString(R.string.specify_file_to_export_to);
|
||||
} else {
|
||||
message = activity.getString(R.string.specify_file_to_export_secret_keys_to);
|
||||
}
|
||||
|
||||
mFileDialog = FileDialogFragment.newInstance(messenger, title, message,
|
||||
exportFilename, null);
|
||||
|
||||
mFileDialog.show(activity.getSupportFragmentManager(), "fileDialog");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export keys
|
||||
*
|
||||
* @param keyRingMasterKeyId
|
||||
* if -1 export all keys
|
||||
*/
|
||||
public void exportKeys(Uri dataUri, int keyType) {
|
||||
Log.d(Constants.TAG, "exportKeys started");
|
||||
|
||||
// Send all information needed to service to export key in other thread
|
||||
Intent intent = new Intent(activity, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_EXPORT_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFilename);
|
||||
data.putInt(KeychainIntentService.EXPORT_KEY_TYPE, keyType);
|
||||
|
||||
if (dataUri == null) {
|
||||
data.putBoolean(KeychainIntentService.EXPORT_ALL, true);
|
||||
} else {
|
||||
// TODO: put data uri into service???
|
||||
long keyRingMasterKeyId = ProviderHelper.getMasterKeyId(activity, dataUri);
|
||||
|
||||
data.putLong(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, keyRingMasterKeyId);
|
||||
}
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after exporting is done in ApgService
|
||||
KeychainIntentServiceHandler exportHandler = new KeychainIntentServiceHandler(activity,
|
||||
R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
// get returned data bundle
|
||||
Bundle returnData = message.getData();
|
||||
|
||||
int exported = returnData.getInt(KeychainIntentService.RESULT_EXPORT);
|
||||
String toastMessage;
|
||||
if (exported == 1) {
|
||||
toastMessage = activity.getString(R.string.key_exported);
|
||||
} else if (exported > 0) {
|
||||
toastMessage = activity.getString(R.string.keys_exported, exported);
|
||||
} else {
|
||||
toastMessage = activity.getString(R.string.no_keys_exported);
|
||||
}
|
||||
Toast.makeText(activity, toastMessage, Toast.LENGTH_SHORT).show();
|
||||
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(exportHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
exportHandler.showProgressDialog(activity);
|
||||
|
||||
// start service with intent
|
||||
activity.startService(intent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.helper;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class FileHelper {
|
||||
|
||||
/**
|
||||
* Checks if external storage is mounted if file is located on external storage
|
||||
*
|
||||
* @param file
|
||||
* @return true if storage is mounted
|
||||
*/
|
||||
public static boolean isStorageMounted(String file) {
|
||||
if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
|
||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the preferred installed file manager on Android and shows a toast if no manager is
|
||||
* installed.
|
||||
*
|
||||
* @param activity
|
||||
* @param filename
|
||||
* default selected file, not supported by all file managers
|
||||
* @param mimeType
|
||||
* can be text/plain for example
|
||||
* @param requestCode
|
||||
* requestCode used to identify the result coming back from file manager to
|
||||
* onActivityResult() in your activity
|
||||
*/
|
||||
public static void openFile(Activity activity, String filename, String mimeType, int requestCode) {
|
||||
Intent intent = buildFileIntent(filename, mimeType);
|
||||
|
||||
try {
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// No compatible file manager was found.
|
||||
Toast.makeText(activity, R.string.no_filemanager_installed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFile(Fragment fragment, String filename, String mimeType, int requestCode) {
|
||||
Intent intent = buildFileIntent(filename, mimeType);
|
||||
|
||||
try {
|
||||
fragment.startActivityForResult(intent, requestCode);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// No compatible file manager was found.
|
||||
Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent buildFileIntent(String filename, String mimeType) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
|
||||
intent.setData(Uri.parse("file://" + filename));
|
||||
intent.setType(mimeType);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file path from a Uri.
|
||||
*
|
||||
* from https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/
|
||||
* afilechooser/utils/FileUtils.java
|
||||
*
|
||||
* @param context
|
||||
* @param uri
|
||||
* @return
|
||||
*
|
||||
* @author paulburke
|
||||
*/
|
||||
public static String getPath(Context context, Uri uri) {
|
||||
Log.d(Constants.TAG + " File -",
|
||||
"Authority: " + uri.getAuthority() + ", Fragment: " + uri.getFragment()
|
||||
+ ", Port: " + uri.getPort() + ", Query: " + uri.getQuery() + ", Scheme: "
|
||||
+ uri.getScheme() + ", Host: " + uri.getHost() + ", Segments: "
|
||||
+ uri.getPathSegments().toString());
|
||||
|
||||
if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
String[] projection = { "_data" };
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, null, null, null);
|
||||
int column_index = cursor.getColumnIndexOrThrow("_data");
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Eat it
|
||||
}
|
||||
}
|
||||
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.helper;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
public class OtherHelper {
|
||||
|
||||
/**
|
||||
* Return the number if days between two dates
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return number of days
|
||||
*/
|
||||
public static long getNumDaysBetween(GregorianCalendar first, GregorianCalendar second) {
|
||||
GregorianCalendar tmp = new GregorianCalendar();
|
||||
tmp.setTime(first.getTime());
|
||||
long numDays = (second.getTimeInMillis() - first.getTimeInMillis()) / 1000 / 86400;
|
||||
tmp.add(Calendar.DAY_OF_MONTH, (int) numDays);
|
||||
while (tmp.before(second)) {
|
||||
tmp.add(Calendar.DAY_OF_MONTH, 1);
|
||||
++numDays;
|
||||
}
|
||||
return numDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs bundle content to debug for inspecting the content
|
||||
*
|
||||
* @param bundle
|
||||
* @param bundleName
|
||||
*/
|
||||
public static void logDebugBundle(Bundle bundle, String bundleName) {
|
||||
if (Constants.DEBUG) {
|
||||
if (bundle != null) {
|
||||
Set<String> ks = bundle.keySet();
|
||||
Iterator<String> iterator = ks.iterator();
|
||||
|
||||
Log.d(Constants.TAG, "Bundle " + bundleName + ":");
|
||||
Log.d(Constants.TAG, "------------------------------");
|
||||
while (iterator.hasNext()) {
|
||||
String key = iterator.next();
|
||||
Object value = bundle.get(key);
|
||||
|
||||
if (value != null) {
|
||||
Log.d(Constants.TAG, key + " : " + value.toString());
|
||||
} else {
|
||||
Log.d(Constants.TAG, key + " : null");
|
||||
}
|
||||
}
|
||||
Log.d(Constants.TAG, "------------------------------");
|
||||
} else {
|
||||
Log.d(Constants.TAG, "Bundle " + bundleName + ": null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.helper;
|
||||
|
||||
import org.spongycastle.bcpg.HashAlgorithmTags;
|
||||
import org.spongycastle.openpgp.PGPEncryptedData;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Singleton Implementation of a Preference Helper
|
||||
*/
|
||||
public class Preferences {
|
||||
private static Preferences mPreferences;
|
||||
private SharedPreferences mSharedPreferences;
|
||||
|
||||
public static synchronized Preferences getPreferences(Context context) {
|
||||
return getPreferences(context, false);
|
||||
}
|
||||
|
||||
public static synchronized Preferences getPreferences(Context context, boolean force_new) {
|
||||
if (mPreferences == null || force_new) {
|
||||
mPreferences = new Preferences(context);
|
||||
}
|
||||
return mPreferences;
|
||||
}
|
||||
|
||||
private Preferences(Context context) {
|
||||
mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return mSharedPreferences.getString(Constants.pref.LANGUAGE, "");
|
||||
}
|
||||
|
||||
public void setLanguage(String value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putString(Constants.pref.LANGUAGE, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public long getPassPhraseCacheTtl() {
|
||||
int ttl = mSharedPreferences.getInt(Constants.pref.PASS_PHRASE_CACHE_TTL, 180);
|
||||
// fix the value if it was set to "never" in previous versions, which currently is not
|
||||
// supported
|
||||
if (ttl == 0) {
|
||||
ttl = 180;
|
||||
}
|
||||
return (long) ttl;
|
||||
}
|
||||
|
||||
public void setPassPhraseCacheTtl(int value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putInt(Constants.pref.PASS_PHRASE_CACHE_TTL, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public int getDefaultEncryptionAlgorithm() {
|
||||
return mSharedPreferences.getInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
PGPEncryptedData.AES_256);
|
||||
}
|
||||
|
||||
public void setDefaultEncryptionAlgorithm(int value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public int getDefaultHashAlgorithm() {
|
||||
return mSharedPreferences.getInt(Constants.pref.DEFAULT_HASH_ALGORITHM,
|
||||
HashAlgorithmTags.SHA512);
|
||||
}
|
||||
|
||||
public void setDefaultHashAlgorithm(int value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putInt(Constants.pref.DEFAULT_HASH_ALGORITHM, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public int getDefaultMessageCompression() {
|
||||
return mSharedPreferences.getInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION,
|
||||
Id.choice.compression.zlib);
|
||||
}
|
||||
|
||||
public void setDefaultMessageCompression(int value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public int getDefaultFileCompression() {
|
||||
return mSharedPreferences.getInt(Constants.pref.DEFAULT_FILE_COMPRESSION,
|
||||
Id.choice.compression.none);
|
||||
}
|
||||
|
||||
public void setDefaultFileCompression(int value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putInt(Constants.pref.DEFAULT_FILE_COMPRESSION, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public boolean getDefaultAsciiArmour() {
|
||||
return mSharedPreferences.getBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, false);
|
||||
}
|
||||
|
||||
public void setDefaultAsciiArmour(boolean value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public boolean getForceV3Signatures() {
|
||||
return mSharedPreferences.getBoolean(Constants.pref.FORCE_V3_SIGNATURES, false);
|
||||
}
|
||||
|
||||
public void setForceV3Signatures(boolean value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
editor.putBoolean(Constants.pref.FORCE_V3_SIGNATURES, value);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public String[] getKeyServers() {
|
||||
String rawData = mSharedPreferences.getString(Constants.pref.KEY_SERVERS,
|
||||
Constants.defaults.KEY_SERVERS);
|
||||
Vector<String> servers = new Vector<String>();
|
||||
String chunks[] = rawData.split(",");
|
||||
for (int i = 0; i < chunks.length; ++i) {
|
||||
String tmp = chunks[i].trim();
|
||||
if (tmp.length() > 0) {
|
||||
servers.add(tmp);
|
||||
}
|
||||
}
|
||||
return servers.toArray(chunks);
|
||||
}
|
||||
|
||||
public void setKeyServers(String[] value) {
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit();
|
||||
String rawData = "";
|
||||
for (int i = 0; i < value.length; ++i) {
|
||||
String tmp = value[i].trim();
|
||||
if (tmp.length() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!"".equals(rawData)) {
|
||||
rawData += ",";
|
||||
}
|
||||
rawData += tmp;
|
||||
}
|
||||
editor.putString(Constants.pref.KEY_SERVERS, rawData);
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.pgp;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPObjectFactory;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
|
||||
public class PgpConversionHelper {
|
||||
|
||||
/**
|
||||
* Convert from byte[] to PGPKeyRing
|
||||
*
|
||||
* @param keysBytes
|
||||
* @return
|
||||
*/
|
||||
public static PGPKeyRing BytesToPGPKeyRing(byte[] keysBytes) {
|
||||
PGPObjectFactory factory = new PGPObjectFactory(keysBytes);
|
||||
PGPKeyRing keyRing = null;
|
||||
try {
|
||||
if ((keyRing = (PGPKeyRing) factory.nextObject()) == null) {
|
||||
Log.e(Constants.TAG, "No keys given!");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Error while converting to PGPKeyRing!", e);
|
||||
}
|
||||
|
||||
return keyRing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from byte[] to ArrayList<PGPSecretKey>
|
||||
*
|
||||
* @param keysBytes
|
||||
* @return
|
||||
*/
|
||||
public static ArrayList<PGPSecretKey> BytesToPGPSecretKeyList(byte[] keysBytes) {
|
||||
PGPSecretKeyRing keyRing = (PGPSecretKeyRing) BytesToPGPKeyRing(keysBytes);
|
||||
ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSecretKey> itr = keyRing.getSecretKeys();
|
||||
while (itr.hasNext()) {
|
||||
keys.add(itr.next());
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from byte[] to PGPSecretKey
|
||||
*
|
||||
* Singles keys are encoded as keyRings with one single key in it by Bouncy Castle
|
||||
*
|
||||
* @param keysBytes
|
||||
* @return
|
||||
*/
|
||||
public static PGPSecretKey BytesToPGPSecretKey(byte[] keyBytes) {
|
||||
PGPSecretKey key = BytesToPGPSecretKeyList(keyBytes).get(0);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from ArrayList<PGPSecretKey> to byte[]
|
||||
*
|
||||
* @param keys
|
||||
* @return
|
||||
*/
|
||||
public static byte[] PGPSecretKeyArrayListToBytes(ArrayList<PGPSecretKey> keys) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
for (PGPSecretKey key : keys) {
|
||||
try {
|
||||
key.encode(os);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Error while converting ArrayList<PGPSecretKey> to byte[]!", e);
|
||||
}
|
||||
}
|
||||
|
||||
return os.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from PGPSecretKey to byte[]
|
||||
*
|
||||
* @param keysBytes
|
||||
* @return
|
||||
*/
|
||||
public static byte[] PGPSecretKeyToBytes(PGPSecretKey key) {
|
||||
try {
|
||||
return key.getEncoded();
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Encoding failed", e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from PGPSecretKeyRing to byte[]
|
||||
*
|
||||
* @param keysBytes
|
||||
* @return
|
||||
*/
|
||||
public static byte[] PGPSecretKeyRingToBytes(PGPSecretKeyRing keyRing) {
|
||||
try {
|
||||
return keyRing.getEncoded();
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Encoding failed", e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Iterator;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.spongycastle.openpgp.PGPEncryptedDataList;
|
||||
import org.spongycastle.openpgp.PGPObjectFactory;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.spongycastle.openpgp.PGPUtil;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
|
||||
public class PgpHelper {
|
||||
|
||||
public static Pattern PGP_MESSAGE = Pattern.compile(
|
||||
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
|
||||
|
||||
public static Pattern PGP_SIGNED_MESSAGE = Pattern
|
||||
.compile(
|
||||
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
|
||||
Pattern.DOTALL);
|
||||
|
||||
public static Pattern PGP_PUBLIC_KEY = Pattern.compile(
|
||||
".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*",
|
||||
Pattern.DOTALL);
|
||||
|
||||
public static String getVersion(Context context) {
|
||||
String version = null;
|
||||
try {
|
||||
PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0);
|
||||
version = pi.versionName;
|
||||
return version;
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(Constants.TAG, "Version could not be retrieved!", e);
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFullVersion(Context context) {
|
||||
return "OpenPGP Keychain v" + getVersion(context);
|
||||
}
|
||||
|
||||
public static long getDecryptionKeyId(Context context, InputStream inputStream)
|
||||
throws PgpGeneralException, NoAsymmetricEncryptionException, IOException {
|
||||
InputStream in = PGPUtil.getDecoderStream(inputStream);
|
||||
PGPObjectFactory pgpF = new PGPObjectFactory(in);
|
||||
PGPEncryptedDataList enc;
|
||||
Object o = pgpF.nextObject();
|
||||
|
||||
// the first object might be a PGP marker packet.
|
||||
if (o instanceof PGPEncryptedDataList) {
|
||||
enc = (PGPEncryptedDataList) o;
|
||||
} else {
|
||||
enc = (PGPEncryptedDataList) pgpF.nextObject();
|
||||
}
|
||||
|
||||
if (enc == null) {
|
||||
throw new PgpGeneralException(context.getString(R.string.error_invalid_data));
|
||||
}
|
||||
|
||||
// TODO: currently we always only look at the first known key
|
||||
// find the secret key
|
||||
PGPSecretKey secretKey = null;
|
||||
Iterator<?> it = enc.getEncryptedDataObjects();
|
||||
boolean gotAsymmetricEncryption = false;
|
||||
while (it.hasNext()) {
|
||||
Object obj = it.next();
|
||||
if (obj instanceof PGPPublicKeyEncryptedData) {
|
||||
gotAsymmetricEncryption = true;
|
||||
PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) obj;
|
||||
secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, pbe.getKeyID());
|
||||
if (secretKey != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!gotAsymmetricEncryption) {
|
||||
throw new NoAsymmetricEncryptionException();
|
||||
}
|
||||
|
||||
if (secretKey == null) {
|
||||
return Id.key.none;
|
||||
}
|
||||
|
||||
return secretKey.getKeyID();
|
||||
}
|
||||
|
||||
public static int getStreamContent(Context context, InputStream inStream) throws IOException {
|
||||
InputStream in = PGPUtil.getDecoderStream(inStream);
|
||||
PGPObjectFactory pgpF = new PGPObjectFactory(in);
|
||||
Object object = pgpF.nextObject();
|
||||
while (object != null) {
|
||||
if (object instanceof PGPPublicKeyRing || object instanceof PGPSecretKeyRing) {
|
||||
return Id.content.keys;
|
||||
} else if (object instanceof PGPEncryptedDataList) {
|
||||
return Id.content.encrypted_data;
|
||||
}
|
||||
object = pgpF.nextObject();
|
||||
}
|
||||
|
||||
return Id.content.unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random filename
|
||||
*
|
||||
* @param length
|
||||
* @return
|
||||
*/
|
||||
public static String generateRandomFilename(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
|
||||
byte bytes[] = new byte[length];
|
||||
random.nextBytes(bytes);
|
||||
String result = "";
|
||||
for (int i = 0; i < length; ++i) {
|
||||
int v = (bytes[i] + 256) % 64;
|
||||
if (v < 10) {
|
||||
result += (char) ('0' + v);
|
||||
} else if (v < 36) {
|
||||
result += (char) ('A' + v - 10);
|
||||
} else if (v < 62) {
|
||||
result += (char) ('a' + v - 36);
|
||||
} else if (v == 62) {
|
||||
result += '_';
|
||||
} else if (v == 63) {
|
||||
result += '.';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go once through stream to get length of stream. The length is later used to display progress
|
||||
* when encrypting/decrypting
|
||||
*
|
||||
* @param in
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static long getLengthOfStream(InputStream in) throws IOException {
|
||||
long size = 0;
|
||||
long n = 0;
|
||||
byte dummy[] = new byte[0x10000];
|
||||
while ((n = in.read(dummy)) > 0) {
|
||||
size += n;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes file securely by overwriting it with random data before deleting it.
|
||||
*
|
||||
* TODO: Does this really help on flash storage?
|
||||
*
|
||||
* @param context
|
||||
* @param progress
|
||||
* @param file
|
||||
* @throws FileNotFoundException
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void deleteFileSecurely(Context context, ProgressDialogUpdater progress, File file)
|
||||
throws FileNotFoundException, IOException {
|
||||
long length = file.length();
|
||||
SecureRandom random = new SecureRandom();
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "rws");
|
||||
raf.seek(0);
|
||||
raf.getFilePointer();
|
||||
byte[] data = new byte[1 << 16];
|
||||
int pos = 0;
|
||||
String msg = context.getString(R.string.progress_deleting_securely, file.getName());
|
||||
while (pos < length) {
|
||||
if (progress != null)
|
||||
progress.setProgress(msg, (int) (100 * pos / length), 100);
|
||||
random.nextBytes(data);
|
||||
raf.write(data);
|
||||
pos += data.length;
|
||||
}
|
||||
raf.close();
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.spongycastle.bcpg.ArmoredOutputStream;
|
||||
import org.spongycastle.openpgp.PGPException;
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPObjectFactory;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.spongycastle.openpgp.PGPUtil;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.util.HkpKeyServer;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.KeyServer.AddKeyException;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
|
||||
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
|
||||
public class PgpImportExport {
|
||||
private Context mContext;
|
||||
private ProgressDialogUpdater mProgress;
|
||||
|
||||
public PgpImportExport(Context context, ProgressDialogUpdater progress) {
|
||||
super();
|
||||
this.mContext = context;
|
||||
this.mProgress = progress;
|
||||
}
|
||||
|
||||
public void updateProgress(int message, int current, int total) {
|
||||
if (mProgress != null) {
|
||||
mProgress.setProgress(message, current, total);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProgress(String message, int current, int total) {
|
||||
if (mProgress != null) {
|
||||
mProgress.setProgress(message, current, total);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProgress(int current, int total) {
|
||||
if (mProgress != null) {
|
||||
mProgress.setProgress(current, total);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
|
||||
try {
|
||||
aos.write(keyring.getEncoded());
|
||||
aos.close();
|
||||
|
||||
String armouredKey = bos.toString("UTF-8");
|
||||
server.add(armouredKey);
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} catch (AddKeyException e) {
|
||||
// TODO: tell the user?
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
bos.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports keys from given data. If keyIds is given only those are imported
|
||||
*
|
||||
* @param data
|
||||
* @param keyIds
|
||||
* @return
|
||||
* @throws PgpGeneralException
|
||||
* @throws FileNotFoundException
|
||||
* @throws PGPException
|
||||
* @throws IOException
|
||||
*/
|
||||
public Bundle importKeyRings(InputData data, ArrayList<Long> keyIds)
|
||||
throws PgpGeneralException, FileNotFoundException, PGPException, IOException {
|
||||
Bundle returnData = new Bundle();
|
||||
|
||||
updateProgress(R.string.progress_importing_secret_keys, 0, 100);
|
||||
|
||||
PositionAwareInputStream progressIn = new PositionAwareInputStream(data.getInputStream());
|
||||
|
||||
// need to have access to the bufferedInput, so we can reuse it for the possible
|
||||
// PGPObject chunks after the first one, e.g. files with several consecutive ASCII
|
||||
// armour blocks
|
||||
BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
|
||||
int newKeys = 0;
|
||||
int oldKeys = 0;
|
||||
int badKeys = 0;
|
||||
try {
|
||||
|
||||
// read all available blocks... (asc files can contain many blocks with BEGIN END)
|
||||
while (bufferedInput.available() > 0) {
|
||||
InputStream in = PGPUtil.getDecoderStream(bufferedInput);
|
||||
PGPObjectFactory objectFactory = new PGPObjectFactory(in);
|
||||
|
||||
// go through all objects in this block
|
||||
Object obj;
|
||||
while ((obj = objectFactory.nextObject()) != null) {
|
||||
Log.d(Constants.TAG, "Found class: " + obj.getClass());
|
||||
|
||||
if (obj instanceof PGPKeyRing) {
|
||||
PGPKeyRing keyring = (PGPKeyRing) obj;
|
||||
|
||||
int status = Integer.MIN_VALUE; // out of bounds value
|
||||
|
||||
if (keyIds != null) {
|
||||
if (keyIds.contains(keyring.getPublicKey().getKeyID())) {
|
||||
status = storeKeyRingInCache(keyring);
|
||||
} else {
|
||||
Log.d(Constants.TAG, "not selected! key id: "
|
||||
+ keyring.getPublicKey().getKeyID());
|
||||
}
|
||||
} else {
|
||||
status = storeKeyRingInCache(keyring);
|
||||
}
|
||||
|
||||
if (status == Id.return_value.error) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_saving_keys));
|
||||
}
|
||||
|
||||
// update the counts to display to the user at the end
|
||||
if (status == Id.return_value.updated) {
|
||||
++oldKeys;
|
||||
} else if (status == Id.return_value.ok) {
|
||||
++newKeys;
|
||||
} else if (status == Id.return_value.bad) {
|
||||
++badKeys;
|
||||
}
|
||||
|
||||
updateProgress((int) (100 * progressIn.position() / data.getSize()), 100);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "Exception on parsing key file!", e);
|
||||
}
|
||||
|
||||
returnData.putInt(KeychainIntentService.RESULT_IMPORT_ADDED, newKeys);
|
||||
returnData.putInt(KeychainIntentService.RESULT_IMPORT_UPDATED, oldKeys);
|
||||
returnData.putInt(KeychainIntentService.RESULT_IMPORT_BAD, badKeys);
|
||||
|
||||
updateProgress(R.string.progress_done, 100, 100);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
public Bundle exportKeyRings(ArrayList<Long> keyRingMasterKeyIds, int keyType,
|
||||
OutputStream outStream) throws PgpGeneralException, FileNotFoundException,
|
||||
PGPException, IOException {
|
||||
Bundle returnData = new Bundle();
|
||||
|
||||
updateProgress(
|
||||
mContext.getResources().getQuantityString(R.plurals.progress_exporting_key,
|
||||
keyRingMasterKeyIds.size()), 0, 100);
|
||||
|
||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_external_storage_not_ready));
|
||||
}
|
||||
|
||||
// export public keyrings...
|
||||
ArmoredOutputStream outPub = new ArmoredOutputStream(outStream);
|
||||
outPub.setHeader("Version", PgpHelper.getFullVersion(mContext));
|
||||
|
||||
int numKeys = 0;
|
||||
for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) {
|
||||
// double the needed time if exporting both public and secret parts
|
||||
if (keyType == Id.type.secret_key) {
|
||||
updateProgress(i * 100 / keyRingMasterKeyIds.size() / 2, 100);
|
||||
} else {
|
||||
updateProgress(i * 100 / keyRingMasterKeyIds.size(), 100);
|
||||
}
|
||||
|
||||
PGPPublicKeyRing publicKeyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(
|
||||
mContext, keyRingMasterKeyIds.get(i));
|
||||
|
||||
if (publicKeyRing != null) {
|
||||
publicKeyRing.encode(outPub);
|
||||
}
|
||||
++numKeys;
|
||||
}
|
||||
outPub.close();
|
||||
|
||||
// if we export secret keyrings, append all secret parts after the public parts
|
||||
if (keyType == Id.type.secret_key) {
|
||||
ArmoredOutputStream outSec = new ArmoredOutputStream(outStream);
|
||||
outSec.setHeader("Version", PgpHelper.getFullVersion(mContext));
|
||||
|
||||
for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) {
|
||||
updateProgress(i * 100 / keyRingMasterKeyIds.size() / 2, 100);
|
||||
|
||||
PGPSecretKeyRing secretKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
|
||||
mContext, keyRingMasterKeyIds.get(i));
|
||||
|
||||
if (secretKeyRing != null) {
|
||||
secretKeyRing.encode(outSec);
|
||||
}
|
||||
}
|
||||
outSec.close();
|
||||
}
|
||||
|
||||
returnData.putInt(KeychainIntentService.RESULT_EXPORT, numKeys);
|
||||
|
||||
updateProgress(R.string.progress_done, 100, 100);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: implement Id.return_value.updated as status when key already existed
|
||||
*
|
||||
* @param context
|
||||
* @param keyring
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public int storeKeyRingInCache(PGPKeyRing keyring) {
|
||||
int status = Integer.MIN_VALUE; // out of bounds value (Id.return_value.*)
|
||||
try {
|
||||
if (keyring instanceof PGPSecretKeyRing) {
|
||||
PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring;
|
||||
boolean save = true;
|
||||
|
||||
for (PGPSecretKey testSecretKey : new IterableIterator<PGPSecretKey>(
|
||||
secretKeyRing.getSecretKeys())) {
|
||||
if (!testSecretKey.isMasterKey()) {
|
||||
if (PgpKeyHelper.isSecretKeyPrivateEmpty(testSecretKey)) {
|
||||
// this is bad, something is very wrong...
|
||||
save = false;
|
||||
status = Id.return_value.bad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (save) {
|
||||
ProviderHelper.saveKeyRing(mContext, secretKeyRing);
|
||||
// TODO: preserve certifications
|
||||
// (http://osdir.com/ml/encryption.bouncy-castle.devel/2007-01/msg00054.html ?)
|
||||
PGPPublicKeyRing newPubRing = null;
|
||||
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(
|
||||
secretKeyRing.getPublicKeys())) {
|
||||
if (newPubRing == null) {
|
||||
newPubRing = new PGPPublicKeyRing(key.getEncoded(),
|
||||
new JcaKeyFingerprintCalculator());
|
||||
}
|
||||
newPubRing = PGPPublicKeyRing.insertPublicKey(newPubRing, key);
|
||||
}
|
||||
if (newPubRing != null)
|
||||
ProviderHelper.saveKeyRing(mContext, newPubRing);
|
||||
// TODO: remove status returns, use exceptions!
|
||||
status = Id.return_value.ok;
|
||||
}
|
||||
} else if (keyring instanceof PGPPublicKeyRing) {
|
||||
PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
|
||||
ProviderHelper.saveKeyRing(mContext, publicKeyRing);
|
||||
// TODO: remove status returns, use exceptions!
|
||||
status = Id.return_value.ok;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
status = Id.return_value.error;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSignature;
|
||||
import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class PgpKeyHelper {
|
||||
|
||||
/**
|
||||
* Returns the last 9 chars of a fingerprint
|
||||
*
|
||||
* @param fingerprint
|
||||
* String containing short or long fingerprint
|
||||
* @return
|
||||
*/
|
||||
public static String shortifyFingerprint(String fingerprint) {
|
||||
return fingerprint.substring(41);
|
||||
}
|
||||
|
||||
public static Date getCreationDate(PGPPublicKey key) {
|
||||
return key.getCreationTime();
|
||||
}
|
||||
|
||||
public static Date getCreationDate(PGPSecretKey key) {
|
||||
return key.getPublicKey().getCreationTime();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static PGPPublicKey getMasterKey(PGPPublicKeyRing keyRing) {
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
|
||||
if (key.isMasterKey()) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static PGPSecretKey getMasterKey(PGPSecretKeyRing keyRing) {
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
|
||||
if (key.isMasterKey()) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static PGPSecretKey getKeyNum(PGPSecretKeyRing keyRing, long num) {
|
||||
long cnt = 0;
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
|
||||
if (cnt == num) {
|
||||
return key;
|
||||
}
|
||||
cnt++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Vector<PGPPublicKey> getEncryptKeys(PGPPublicKeyRing keyRing) {
|
||||
Vector<PGPPublicKey> encryptKeys = new Vector<PGPPublicKey>();
|
||||
|
||||
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
|
||||
if (isEncryptionKey(key)) {
|
||||
encryptKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return encryptKeys;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Vector<PGPSecretKey> getSigningKeys(PGPSecretKeyRing keyRing) {
|
||||
Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
|
||||
|
||||
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
|
||||
if (isSigningKey(key)) {
|
||||
signingKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return signingKeys;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Vector<PGPSecretKey> getCertificationKeys(PGPSecretKeyRing keyRing) {
|
||||
Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
|
||||
|
||||
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
|
||||
if (isCertificationKey(key)) {
|
||||
signingKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return signingKeys;
|
||||
}
|
||||
|
||||
public static Vector<PGPPublicKey> getUsableEncryptKeys(PGPPublicKeyRing keyRing) {
|
||||
Vector<PGPPublicKey> usableKeys = new Vector<PGPPublicKey>();
|
||||
Vector<PGPPublicKey> encryptKeys = getEncryptKeys(keyRing);
|
||||
PGPPublicKey masterKey = null;
|
||||
for (int i = 0; i < encryptKeys.size(); ++i) {
|
||||
PGPPublicKey key = encryptKeys.get(i);
|
||||
if (!isExpired(key) && !key.isRevoked()) {
|
||||
if (key.isMasterKey()) {
|
||||
masterKey = key;
|
||||
} else {
|
||||
usableKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (masterKey != null) {
|
||||
usableKeys.add(masterKey);
|
||||
}
|
||||
return usableKeys;
|
||||
}
|
||||
|
||||
public static boolean isExpired(PGPPublicKey key) {
|
||||
Date creationDate = getCreationDate(key);
|
||||
Date expiryDate = getExpiryDate(key);
|
||||
Date now = new Date();
|
||||
if (now.compareTo(creationDate) >= 0
|
||||
&& (expiryDate == null || now.compareTo(expiryDate) <= 0)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isExpired(PGPSecretKey key) {
|
||||
return isExpired(key.getPublicKey());
|
||||
}
|
||||
|
||||
public static Vector<PGPSecretKey> getUsableCertificationKeys(PGPSecretKeyRing keyRing) {
|
||||
Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
|
||||
Vector<PGPSecretKey> signingKeys = getCertificationKeys(keyRing);
|
||||
PGPSecretKey masterKey = null;
|
||||
for (int i = 0; i < signingKeys.size(); ++i) {
|
||||
PGPSecretKey key = signingKeys.get(i);
|
||||
if (key.isMasterKey()) {
|
||||
masterKey = key;
|
||||
} else {
|
||||
usableKeys.add(key);
|
||||
}
|
||||
}
|
||||
if (masterKey != null) {
|
||||
usableKeys.add(masterKey);
|
||||
}
|
||||
return usableKeys;
|
||||
}
|
||||
|
||||
public static Vector<PGPSecretKey> getUsableSigningKeys(PGPSecretKeyRing keyRing) {
|
||||
Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
|
||||
Vector<PGPSecretKey> signingKeys = getSigningKeys(keyRing);
|
||||
PGPSecretKey masterKey = null;
|
||||
for (int i = 0; i < signingKeys.size(); ++i) {
|
||||
PGPSecretKey key = signingKeys.get(i);
|
||||
if (key.isMasterKey()) {
|
||||
masterKey = key;
|
||||
} else {
|
||||
usableKeys.add(key);
|
||||
}
|
||||
}
|
||||
if (masterKey != null) {
|
||||
usableKeys.add(masterKey);
|
||||
}
|
||||
return usableKeys;
|
||||
}
|
||||
|
||||
public static Date getExpiryDate(PGPPublicKey key) {
|
||||
Date creationDate = getCreationDate(key);
|
||||
if (key.getValidDays() == 0) {
|
||||
// no expiry
|
||||
return null;
|
||||
}
|
||||
Calendar calendar = GregorianCalendar.getInstance();
|
||||
calendar.setTime(creationDate);
|
||||
calendar.add(Calendar.DATE, key.getValidDays());
|
||||
Date expiryDate = calendar.getTime();
|
||||
|
||||
return expiryDate;
|
||||
}
|
||||
|
||||
public static Date getExpiryDate(PGPSecretKey key) {
|
||||
return getExpiryDate(key.getPublicKey());
|
||||
}
|
||||
|
||||
public static PGPPublicKey getEncryptPublicKey(Context context, long masterKeyId) {
|
||||
PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(context,
|
||||
masterKeyId);
|
||||
if (keyRing == null) {
|
||||
Log.e(Constants.TAG, "keyRing is null!");
|
||||
return null;
|
||||
}
|
||||
Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing);
|
||||
if (encryptKeys.size() == 0) {
|
||||
Log.e(Constants.TAG, "encryptKeys is null!");
|
||||
return null;
|
||||
}
|
||||
return encryptKeys.get(0);
|
||||
}
|
||||
|
||||
public static PGPSecretKey getCertificationKey(Context context, long masterKeyId) {
|
||||
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(context,
|
||||
masterKeyId);
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
Vector<PGPSecretKey> signingKeys = getUsableCertificationKeys(keyRing);
|
||||
if (signingKeys.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return signingKeys.get(0);
|
||||
}
|
||||
|
||||
public static PGPSecretKey getSigningKey(Context context, long masterKeyId) {
|
||||
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(context,
|
||||
masterKeyId);
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing);
|
||||
if (signingKeys.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
return signingKeys.get(0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static String getMainUserId(PGPPublicKey key) {
|
||||
for (String userId : new IterableIterator<String>(key.getUserIDs())) {
|
||||
return userId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static String getMainUserId(PGPSecretKey key) {
|
||||
for (String userId : new IterableIterator<String>(key.getUserIDs())) {
|
||||
return userId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getMainUserIdSafe(Context context, PGPPublicKey key) {
|
||||
String userId = getMainUserId(key);
|
||||
if (userId == null || userId.equals("")) {
|
||||
userId = context.getString(R.string.unknown_user_id);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
public static String getMainUserIdSafe(Context context, PGPSecretKey key) {
|
||||
String userId = getMainUserId(key);
|
||||
if (userId == null || userId.equals("")) {
|
||||
userId = context.getString(R.string.unknown_user_id);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static boolean isEncryptionKey(PGPPublicKey key) {
|
||||
if (!key.isEncryptionKey()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key.getVersion() <= 3) {
|
||||
// this must be true now
|
||||
return key.isEncryptionKey();
|
||||
}
|
||||
|
||||
// special cases
|
||||
if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.getAlgorithm() == PGPPublicKey.RSA_ENCRYPT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
|
||||
if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
|
||||
continue;
|
||||
}
|
||||
PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
|
||||
|
||||
if (hashed != null
|
||||
&& (hashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
|
||||
|
||||
if (unhashed != null
|
||||
&& (unhashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEncryptionKey(PGPSecretKey key) {
|
||||
return isEncryptionKey(key.getPublicKey());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static boolean isSigningKey(PGPPublicKey key) {
|
||||
if (key.getVersion() <= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// special case
|
||||
if (key.getAlgorithm() == PGPPublicKey.RSA_SIGN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
|
||||
if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
|
||||
continue;
|
||||
}
|
||||
PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
|
||||
|
||||
if (hashed != null && (hashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
|
||||
|
||||
if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isSigningKey(PGPSecretKey key) {
|
||||
return isSigningKey(key.getPublicKey());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static boolean isCertificationKey(PGPPublicKey key) {
|
||||
if (key.getVersion() <= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
|
||||
if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
|
||||
continue;
|
||||
}
|
||||
PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
|
||||
|
||||
if (hashed != null && (hashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
|
||||
|
||||
if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isCertificationKey(PGPSecretKey key) {
|
||||
return isCertificationKey(key.getPublicKey());
|
||||
}
|
||||
|
||||
public static String getAlgorithmInfo(PGPPublicKey key) {
|
||||
return getAlgorithmInfo(key.getAlgorithm(), key.getBitStrength());
|
||||
}
|
||||
|
||||
public static String getAlgorithmInfo(PGPSecretKey key) {
|
||||
return getAlgorithmInfo(key.getPublicKey());
|
||||
}
|
||||
|
||||
public static String getAlgorithmInfo(int algorithm, int keySize) {
|
||||
String algorithmStr = null;
|
||||
|
||||
switch (algorithm) {
|
||||
case PGPPublicKey.RSA_ENCRYPT:
|
||||
case PGPPublicKey.RSA_GENERAL:
|
||||
case PGPPublicKey.RSA_SIGN: {
|
||||
algorithmStr = "RSA";
|
||||
break;
|
||||
}
|
||||
|
||||
case PGPPublicKey.DSA: {
|
||||
algorithmStr = "DSA";
|
||||
break;
|
||||
}
|
||||
|
||||
case PGPPublicKey.ELGAMAL_ENCRYPT:
|
||||
case PGPPublicKey.ELGAMAL_GENERAL: {
|
||||
algorithmStr = "ElGamal";
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
algorithmStr = "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return algorithmStr + ", " + keySize + " bit";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fingerprint to hex with whitespaces after 4 characters
|
||||
*
|
||||
* @param fp
|
||||
* @return
|
||||
*/
|
||||
public static String convertFingerprintToHex(byte[] fp) {
|
||||
String fingerPrint = "";
|
||||
for (int i = 0; i < fp.length; ++i) {
|
||||
if (i != 0 && i % 10 == 0) {
|
||||
fingerPrint += " ";
|
||||
} else if (i != 0 && i % 2 == 0) {
|
||||
fingerPrint += " ";
|
||||
}
|
||||
String chunk = Integer.toHexString((fp[i] + 256) % 256).toUpperCase(Locale.US);
|
||||
while (chunk.length() < 2) {
|
||||
chunk = "0" + chunk;
|
||||
}
|
||||
fingerPrint += chunk;
|
||||
}
|
||||
|
||||
return fingerPrint;
|
||||
|
||||
}
|
||||
|
||||
public static String getFingerPrint(Context context, long keyId) {
|
||||
PGPPublicKey key = ProviderHelper.getPGPPublicKeyByKeyId(context, keyId);
|
||||
// if it is no public key get it from your own keys...
|
||||
if (key == null) {
|
||||
PGPSecretKey secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, keyId);
|
||||
if (secretKey == null) {
|
||||
Log.e(Constants.TAG, "Key could not be found!");
|
||||
return null;
|
||||
}
|
||||
key = secretKey.getPublicKey();
|
||||
}
|
||||
|
||||
return convertFingerprintToHex(key.getFingerprint());
|
||||
}
|
||||
|
||||
public static boolean isSecretKeyPrivateEmpty(PGPSecretKey secretKey) {
|
||||
return secretKey.isPrivateKeyEmpty();
|
||||
}
|
||||
|
||||
public static boolean isSecretKeyPrivateEmpty(Context context, long keyId) {
|
||||
PGPSecretKey secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, keyId);
|
||||
if (secretKey == null) {
|
||||
Log.e(Constants.TAG, "Key could not be found!");
|
||||
return false; // could be a public key, assume it is not empty
|
||||
}
|
||||
return isSecretKeyPrivateEmpty(secretKey);
|
||||
}
|
||||
|
||||
public static String convertKeyIdToHex(long keyId) {
|
||||
String fingerPrint = Long.toHexString(keyId & 0xffffffffL).toUpperCase(Locale.US);
|
||||
while (fingerPrint.length() < 8) {
|
||||
fingerPrint = "0" + fingerPrint;
|
||||
}
|
||||
return fingerPrint;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: documentation
|
||||
*
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
public static String convertKeyToHex(long keyId) {
|
||||
return convertKeyIdToHex(keyId >> 32) + convertKeyIdToHex(keyId);
|
||||
}
|
||||
|
||||
public static long convertHexToKeyId(String data) {
|
||||
int len = data.length();
|
||||
String s2 = data.substring(len - 8);
|
||||
String s1 = data.substring(0, len - 8);
|
||||
return (Long.parseLong(s1, 16) << 32) | Long.parseLong(s2, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits userId string into naming part, email part, and comment part
|
||||
*
|
||||
* @param userId
|
||||
* @return array with naming (0), email (1), comment (2)
|
||||
*/
|
||||
public static String[] splitUserId(String userId) {
|
||||
String[] result = new String[] { "", "", "" };
|
||||
|
||||
Pattern withComment = Pattern.compile("^(.*) \\((.*)\\) <(.*)>$");
|
||||
Matcher matcher = withComment.matcher(userId);
|
||||
if (matcher.matches()) {
|
||||
result[0] = matcher.group(1);
|
||||
result[1] = matcher.group(3);
|
||||
result[2] = matcher.group(2);
|
||||
return result;
|
||||
}
|
||||
|
||||
Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$");
|
||||
matcher = withoutComment.matcher(userId);
|
||||
if (matcher.matches()) {
|
||||
result[0] = matcher.group(1);
|
||||
result[1] = matcher.group(2);
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
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.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import org.spongycastle.bcpg.CompressionAlgorithmTags;
|
||||
import org.spongycastle.bcpg.HashAlgorithmTags;
|
||||
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
|
||||
import org.spongycastle.bcpg.sig.KeyFlags;
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.spongycastle.jce.spec.ElGamalParameterSpec;
|
||||
import org.spongycastle.openpgp.PGPEncryptedData;
|
||||
import org.spongycastle.openpgp.PGPException;
|
||||
import org.spongycastle.openpgp.PGPKeyPair;
|
||||
import org.spongycastle.openpgp.PGPKeyRingGenerator;
|
||||
import org.spongycastle.openpgp.PGPPrivateKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSignature;
|
||||
import org.spongycastle.openpgp.PGPSignatureGenerator;
|
||||
import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
|
||||
import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
|
||||
import org.spongycastle.openpgp.PGPUtil;
|
||||
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||
import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
|
||||
import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
|
||||
import org.spongycastle.openpgp.operator.PGPDigestCalculator;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Primes;
|
||||
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class PgpKeyOperation {
|
||||
private Context mContext;
|
||||
private ProgressDialogUpdater mProgress;
|
||||
|
||||
private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[] {
|
||||
SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192,
|
||||
SymmetricKeyAlgorithmTags.AES_128, SymmetricKeyAlgorithmTags.CAST5,
|
||||
SymmetricKeyAlgorithmTags.TRIPLE_DES };
|
||||
private static final int[] PREFERRED_HASH_ALGORITHMS = new int[] { HashAlgorithmTags.SHA1,
|
||||
HashAlgorithmTags.SHA256, HashAlgorithmTags.RIPEMD160 };
|
||||
private static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[] {
|
||||
CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2,
|
||||
CompressionAlgorithmTags.ZIP };
|
||||
|
||||
public PgpKeyOperation(Context context, ProgressDialogUpdater progress) {
|
||||
super();
|
||||
this.mContext = context;
|
||||
this.mProgress = progress;
|
||||
}
|
||||
|
||||
public void updateProgress(int message, int current, int total) {
|
||||
if (mProgress != null) {
|
||||
mProgress.setProgress(message, current, total);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProgress(int current, int total) {
|
||||
if (mProgress != null) {
|
||||
mProgress.setProgress(current, total);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new secret key. The returned PGPSecretKeyRing contains only one newly generated key
|
||||
* when this key is the new masterkey. If a masterkey is supplied in the parameters
|
||||
* PGPSecretKeyRing contains the masterkey and the new key as a subkey (certified by the
|
||||
* masterkey).
|
||||
*
|
||||
* @param algorithmChoice
|
||||
* @param keySize
|
||||
* @param passPhrase
|
||||
* @param masterSecretKey
|
||||
* @return
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws PGPException
|
||||
* @throws NoSuchProviderException
|
||||
* @throws PgpGeneralException
|
||||
* @throws InvalidAlgorithmParameterException
|
||||
*/
|
||||
public PGPSecretKeyRing createKey(int algorithmChoice, int keySize, String passPhrase,
|
||||
PGPSecretKey masterSecretKey) throws NoSuchAlgorithmException, PGPException,
|
||||
NoSuchProviderException, PgpGeneralException, InvalidAlgorithmParameterException {
|
||||
|
||||
if (keySize < 512) {
|
||||
throw new PgpGeneralException(mContext.getString(R.string.error_key_size_minimum512bit));
|
||||
}
|
||||
|
||||
if (passPhrase == null) {
|
||||
passPhrase = "";
|
||||
}
|
||||
|
||||
int algorithm = 0;
|
||||
KeyPairGenerator keyGen = null;
|
||||
|
||||
switch (algorithmChoice) {
|
||||
case Id.choice.algorithm.dsa: {
|
||||
keyGen = KeyPairGenerator.getInstance("DSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
keyGen.initialize(keySize, new SecureRandom());
|
||||
algorithm = PGPPublicKey.DSA;
|
||||
break;
|
||||
}
|
||||
|
||||
case Id.choice.algorithm.elgamal: {
|
||||
if (masterSecretKey == null) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_master_key_must_not_be_el_gamal));
|
||||
}
|
||||
keyGen = KeyPairGenerator.getInstance("ElGamal", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
BigInteger p = Primes.getBestPrime(keySize);
|
||||
BigInteger g = new BigInteger("2");
|
||||
|
||||
ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g);
|
||||
|
||||
keyGen.initialize(elParams);
|
||||
algorithm = PGPPublicKey.ELGAMAL_ENCRYPT;
|
||||
break;
|
||||
}
|
||||
|
||||
case Id.choice.algorithm.rsa: {
|
||||
keyGen = KeyPairGenerator.getInstance("RSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
keyGen.initialize(keySize, new SecureRandom());
|
||||
|
||||
algorithm = PGPPublicKey.RSA_GENERAL;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_unknown_algorithm_choice));
|
||||
}
|
||||
}
|
||||
|
||||
// build new key pair
|
||||
PGPKeyPair keyPair = new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date());
|
||||
|
||||
// define hashing and signing algos
|
||||
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
|
||||
HashAlgorithmTags.SHA1);
|
||||
|
||||
// Build key encrypter and decrypter based on passphrase
|
||||
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
|
||||
PGPEncryptedData.CAST5, sha1Calc)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passPhrase.toCharArray());
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passPhrase.toCharArray());
|
||||
|
||||
PGPKeyRingGenerator ringGen = null;
|
||||
PGPContentSignerBuilder certificationSignerBuilder = null;
|
||||
if (masterSecretKey == null) {
|
||||
certificationSignerBuilder = new JcaPGPContentSignerBuilder(keyPair.getPublicKey()
|
||||
.getAlgorithm(), HashAlgorithmTags.SHA1);
|
||||
|
||||
// build keyRing with only this one master key in it!
|
||||
ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, keyPair, "",
|
||||
sha1Calc, null, null, certificationSignerBuilder, keyEncryptor);
|
||||
} else {
|
||||
PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
|
||||
PGPPrivateKey masterPrivateKey = masterSecretKey.extractPrivateKey(keyDecryptor);
|
||||
PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
|
||||
|
||||
certificationSignerBuilder = new JcaPGPContentSignerBuilder(masterKeyPair
|
||||
.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
|
||||
|
||||
// build keyRing with master key and new key as subkey (certified by masterkey)
|
||||
ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, masterKeyPair,
|
||||
"", sha1Calc, null, null, certificationSignerBuilder, keyEncryptor);
|
||||
|
||||
ringGen.addSubKey(keyPair);
|
||||
}
|
||||
|
||||
PGPSecretKeyRing secKeyRing = ringGen.generateSecretKeyRing();
|
||||
|
||||
return secKeyRing;
|
||||
}
|
||||
|
||||
public void changeSecretKeyPassphrase(PGPSecretKeyRing keyRing, String oldPassPhrase,
|
||||
String newPassPhrase) throws IOException, PGPException, PGPException,
|
||||
NoSuchProviderException {
|
||||
|
||||
updateProgress(R.string.progress_building_key, 0, 100);
|
||||
if (oldPassPhrase == null) {
|
||||
oldPassPhrase = "";
|
||||
}
|
||||
if (newPassPhrase == null) {
|
||||
newPassPhrase = "";
|
||||
}
|
||||
|
||||
PGPSecretKeyRing newKeyRing = PGPSecretKeyRing.copyWithNewPassword(
|
||||
keyRing,
|
||||
new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder()
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build()).setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray()),
|
||||
new JcePBESecretKeyEncryptorBuilder(keyRing.getSecretKey()
|
||||
.getKeyEncryptionAlgorithm()).build(newPassPhrase.toCharArray()));
|
||||
|
||||
updateProgress(R.string.progress_saving_key_ring, 50, 100);
|
||||
|
||||
ProviderHelper.saveKeyRing(mContext, newKeyRing);
|
||||
|
||||
updateProgress(R.string.progress_done, 100, 100);
|
||||
|
||||
}
|
||||
|
||||
public void buildSecretKey(ArrayList<String> userIds, ArrayList<PGPSecretKey> keys,
|
||||
ArrayList<Integer> keysUsages, long masterKeyId, String oldPassPhrase,
|
||||
String newPassPhrase) throws PgpGeneralException, NoSuchProviderException,
|
||||
PGPException, NoSuchAlgorithmException, SignatureException, IOException {
|
||||
|
||||
Log.d(Constants.TAG, "userIds: " + userIds.toString());
|
||||
|
||||
updateProgress(R.string.progress_building_key, 0, 100);
|
||||
|
||||
if (oldPassPhrase == null) {
|
||||
oldPassPhrase = "";
|
||||
}
|
||||
if (newPassPhrase == null) {
|
||||
newPassPhrase = "";
|
||||
}
|
||||
|
||||
updateProgress(R.string.progress_preparing_master_key, 10, 100);
|
||||
|
||||
int usageId = keysUsages.get(0);
|
||||
boolean canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
boolean canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
|
||||
String mainUserId = userIds.get(0);
|
||||
|
||||
PGPSecretKey masterKey = keys.get(0);
|
||||
|
||||
// this removes all userIds and certifications previously attached to the masterPublicKey
|
||||
PGPPublicKey tmpKey = masterKey.getPublicKey();
|
||||
PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
|
||||
tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
|
||||
|
||||
// already done by code above:
|
||||
// PGPPublicKey masterPublicKey = masterKey.getPublicKey();
|
||||
// // Somehow, the PGPPublicKey already has an empty certification attached to it when the
|
||||
// // keyRing is generated the first time, we remove that when it exists, before adding the
|
||||
// new
|
||||
// // ones
|
||||
// PGPPublicKey masterPublicKeyRmCert = PGPPublicKey.removeCertification(masterPublicKey,
|
||||
// "");
|
||||
// if (masterPublicKeyRmCert != null) {
|
||||
// masterPublicKey = masterPublicKeyRmCert;
|
||||
// }
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
|
||||
PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
|
||||
|
||||
updateProgress(R.string.progress_certifying_master_key, 20, 100);
|
||||
|
||||
// TODO: if we are editing a key, keep old certs, don't remake certs we don't have to.
|
||||
|
||||
for (String userId : userIds) {
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
|
||||
sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
|
||||
|
||||
PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
|
||||
|
||||
masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
|
||||
}
|
||||
|
||||
PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
|
||||
|
||||
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
|
||||
int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
|
||||
if (canEncrypt) {
|
||||
keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
|
||||
}
|
||||
hashedPacketsGen.setKeyFlags(true, keyFlags);
|
||||
|
||||
hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
|
||||
hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
|
||||
hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
|
||||
|
||||
// TODO: this doesn't work quite right yet (APG 1)
|
||||
// if (keyEditor.getExpiryDate() != null) {
|
||||
// GregorianCalendar creationDate = new GregorianCalendar();
|
||||
// creationDate.setTime(getCreationDate(masterKey));
|
||||
// GregorianCalendar expiryDate = keyEditor.getExpiryDate();
|
||||
// long numDays = Utils.getNumDaysBetween(creationDate, expiryDate);
|
||||
// if (numDays <= 0) {
|
||||
// throw new GeneralException(
|
||||
// context.getString(R.string.error_expiryMustComeAfterCreation));
|
||||
// }
|
||||
// hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
|
||||
// }
|
||||
|
||||
updateProgress(R.string.progress_building_master_key, 30, 100);
|
||||
|
||||
// define hashing and signing algos
|
||||
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
|
||||
HashAlgorithmTags.SHA1);
|
||||
PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
|
||||
masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
|
||||
|
||||
// Build key encrypter based on passphrase
|
||||
PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
|
||||
PGPEncryptedData.CAST5, sha1Calc)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||
newPassPhrase.toCharArray());
|
||||
|
||||
PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
|
||||
masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
|
||||
unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
|
||||
|
||||
updateProgress(R.string.progress_adding_sub_keys, 40, 100);
|
||||
|
||||
for (int i = 1; i < keys.size(); ++i) {
|
||||
updateProgress(40 + 50 * (i - 1) / (keys.size() - 1), 100);
|
||||
|
||||
PGPSecretKey subKey = keys.get(i);
|
||||
PGPPublicKey subPublicKey = subKey.getPublicKey();
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||
oldPassPhrase.toCharArray());
|
||||
PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2);
|
||||
|
||||
// TODO: now used without algorithm and creation time?! (APG 1)
|
||||
PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey);
|
||||
|
||||
hashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
|
||||
|
||||
keyFlags = 0;
|
||||
|
||||
usageId = keysUsages.get(i);
|
||||
canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
|
||||
if (canSign) { // TODO: ensure signing times are the same, like gpg
|
||||
keyFlags |= KeyFlags.SIGN_DATA;
|
||||
// cross-certify signing keys
|
||||
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
|
||||
subPublicKey.getAlgorithm(), PGPUtil.SHA1)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
|
||||
sGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey);
|
||||
PGPSignature certification = sGen.generateCertification(masterPublicKey,
|
||||
subPublicKey);
|
||||
unhashedPacketsGen.setEmbeddedSignature(false, certification);
|
||||
}
|
||||
if (canEncrypt) {
|
||||
keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
|
||||
}
|
||||
hashedPacketsGen.setKeyFlags(false, keyFlags);
|
||||
|
||||
// TODO: this doesn't work quite right yet (APG 1)
|
||||
// if (keyEditor.getExpiryDate() != null) {
|
||||
// GregorianCalendar creationDate = new GregorianCalendar();
|
||||
// creationDate.setTime(getCreationDate(masterKey));
|
||||
// GregorianCalendar expiryDate = keyEditor.getExpiryDate();
|
||||
// long numDays = Utils.getNumDaysBetween(creationDate, expiryDate);
|
||||
// if (numDays <= 0) {
|
||||
// throw new GeneralException(
|
||||
// context.getString(R.string.error_expiryMustComeAfterCreation));
|
||||
// }
|
||||
// hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
|
||||
// }
|
||||
|
||||
keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate());
|
||||
}
|
||||
|
||||
PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
|
||||
PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
|
||||
|
||||
updateProgress(R.string.progress_saving_key_ring, 90, 100);
|
||||
|
||||
ProviderHelper.saveKeyRing(mContext, secretKeyRing);
|
||||
ProviderHelper.saveKeyRing(mContext, publicKeyRing);
|
||||
|
||||
updateProgress(R.string.progress_done, 100, 100);
|
||||
}
|
||||
|
||||
public PGPPublicKeyRing signKey(long masterKeyId, long pubKeyId, String passphrase)
|
||||
throws PgpGeneralException, NoSuchAlgorithmException, NoSuchProviderException,
|
||||
PGPException, SignatureException {
|
||||
if (passphrase == null) {
|
||||
throw new PgpGeneralException("Unable to obtain passphrase");
|
||||
} else {
|
||||
PGPPublicKeyRing pubring = ProviderHelper
|
||||
.getPGPPublicKeyRingByKeyId(mContext, pubKeyId);
|
||||
|
||||
PGPSecretKey signingKey = PgpKeyHelper.getCertificationKey(mContext, masterKeyId);
|
||||
if (signingKey == null) {
|
||||
throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
|
||||
}
|
||||
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
|
||||
PGPPrivateKey signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
|
||||
if (signaturePrivateKey == null) {
|
||||
throw new PgpGeneralException(
|
||||
mContext.getString(R.string.error_could_not_extract_private_key));
|
||||
}
|
||||
|
||||
// TODO: SHA256 fixed?
|
||||
JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
|
||||
signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
|
||||
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
|
||||
contentSignerBuilder);
|
||||
|
||||
signatureGenerator.init(PGPSignature.DIRECT_KEY, signaturePrivateKey);
|
||||
|
||||
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
|
||||
|
||||
PGPSignatureSubpacketVector packetVector = spGen.generate();
|
||||
signatureGenerator.setHashedSubpackets(packetVector);
|
||||
|
||||
PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId),
|
||||
signatureGenerator.generate());
|
||||
pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
|
||||
|
||||
return pubring;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,307 @@
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.Vector;
|
||||
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.PasswordCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
|
||||
import org.spongycastle.asn1.DERObjectIdentifier;
|
||||
import org.spongycastle.asn1.x509.AuthorityKeyIdentifier;
|
||||
import org.spongycastle.asn1.x509.BasicConstraints;
|
||||
import org.spongycastle.asn1.x509.GeneralName;
|
||||
import org.spongycastle.asn1.x509.GeneralNames;
|
||||
import org.spongycastle.asn1.x509.SubjectKeyIdentifier;
|
||||
import org.spongycastle.asn1.x509.X509Extensions;
|
||||
import org.spongycastle.asn1.x509.X509Name;
|
||||
import org.spongycastle.openpgp.PGPException;
|
||||
import org.spongycastle.openpgp.PGPPrivateKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.x509.X509V3CertificateGenerator;
|
||||
import org.spongycastle.x509.extension.AuthorityKeyIdentifierStructure;
|
||||
import org.spongycastle.x509.extension.SubjectKeyIdentifierStructure;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class PgpToX509 {
|
||||
public final static String DN_COMMON_PART_O = "OpenPGP to X.509 Bridge";
|
||||
public final static String DN_COMMON_PART_OU = "OpenPGP Keychain cert";
|
||||
|
||||
/**
|
||||
* Creates a self-signed certificate from a public and private key. The (critical) key-usage
|
||||
* extension is set up with: digital signature, non-repudiation, key-encipherment, key-agreement
|
||||
* and certificate-signing. The (non-critical) Netscape extension is set up with: SSL client and
|
||||
* S/MIME. A URI subjectAltName may also be set up.
|
||||
*
|
||||
* @param pubKey
|
||||
* public key
|
||||
* @param privKey
|
||||
* private key
|
||||
* @param subject
|
||||
* subject (and issuer) DN for this certificate, RFC 2253 format preferred.
|
||||
* @param startDate
|
||||
* date from which the certificate will be valid (defaults to current date and time
|
||||
* if null)
|
||||
* @param endDate
|
||||
* date until which the certificate will be valid (defaults to current date and time
|
||||
* if null) *
|
||||
* @param subjAltNameURI
|
||||
* URI to be placed in subjectAltName
|
||||
* @return self-signed certificate
|
||||
* @throws InvalidKeyException
|
||||
* @throws SignatureException
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws IllegalStateException
|
||||
* @throws NoSuchProviderException
|
||||
* @throws CertificateException
|
||||
* @throws Exception
|
||||
*
|
||||
* @author Bruno Harbulot
|
||||
*/
|
||||
public static X509Certificate createSelfSignedCert(PublicKey pubKey, PrivateKey privKey,
|
||||
X509Name subject, Date startDate, Date endDate, String subjAltNameURI)
|
||||
throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException,
|
||||
SignatureException, CertificateException, NoSuchProviderException {
|
||||
|
||||
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
|
||||
|
||||
certGenerator.reset();
|
||||
/*
|
||||
* Sets up the subject distinguished name. Since it's a self-signed certificate, issuer and
|
||||
* subject are the same.
|
||||
*/
|
||||
certGenerator.setIssuerDN(subject);
|
||||
certGenerator.setSubjectDN(subject);
|
||||
|
||||
/*
|
||||
* Sets up the validity dates.
|
||||
*/
|
||||
if (startDate == null) {
|
||||
startDate = new Date(System.currentTimeMillis());
|
||||
}
|
||||
certGenerator.setNotBefore(startDate);
|
||||
if (endDate == null) {
|
||||
endDate = new Date(startDate.getTime() + (365L * 24L * 60L * 60L * 1000L));
|
||||
Log.d(Constants.TAG, "end date is=" + DateFormat.getDateInstance().format(endDate));
|
||||
}
|
||||
|
||||
certGenerator.setNotAfter(endDate);
|
||||
|
||||
/*
|
||||
* The serial-number of this certificate is 1. It makes sense because it's self-signed.
|
||||
*/
|
||||
certGenerator.setSerialNumber(BigInteger.ONE);
|
||||
/*
|
||||
* Sets the public-key to embed in this certificate.
|
||||
*/
|
||||
certGenerator.setPublicKey(pubKey);
|
||||
/*
|
||||
* Sets the signature algorithm.
|
||||
*/
|
||||
String pubKeyAlgorithm = pubKey.getAlgorithm();
|
||||
if (pubKeyAlgorithm.equals("DSA")) {
|
||||
certGenerator.setSignatureAlgorithm("SHA1WithDSA");
|
||||
} else if (pubKeyAlgorithm.equals("RSA")) {
|
||||
certGenerator.setSignatureAlgorithm("SHA1WithRSAEncryption");
|
||||
} else {
|
||||
RuntimeException re = new RuntimeException("Algorithm not recognised: "
|
||||
+ pubKeyAlgorithm);
|
||||
Log.e(Constants.TAG, re.getMessage(), re);
|
||||
throw re;
|
||||
}
|
||||
|
||||
/*
|
||||
* Adds the Basic Constraint (CA: true) extension.
|
||||
*/
|
||||
certGenerator.addExtension(X509Extensions.BasicConstraints, true,
|
||||
new BasicConstraints(true));
|
||||
|
||||
/*
|
||||
* Adds the subject key identifier extension.
|
||||
*/
|
||||
SubjectKeyIdentifier subjectKeyIdentifier = new SubjectKeyIdentifierStructure(pubKey);
|
||||
certGenerator
|
||||
.addExtension(X509Extensions.SubjectKeyIdentifier, false, subjectKeyIdentifier);
|
||||
|
||||
/*
|
||||
* Adds the authority key identifier extension.
|
||||
*/
|
||||
AuthorityKeyIdentifier authorityKeyIdentifier = new AuthorityKeyIdentifierStructure(pubKey);
|
||||
certGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
|
||||
authorityKeyIdentifier);
|
||||
|
||||
/*
|
||||
* Adds the subject alternative-name extension.
|
||||
*/
|
||||
if (subjAltNameURI != null) {
|
||||
GeneralNames subjectAltNames = new GeneralNames(new GeneralName(
|
||||
GeneralName.uniformResourceIdentifier, subjAltNameURI));
|
||||
certGenerator.addExtension(X509Extensions.SubjectAlternativeName, false,
|
||||
subjectAltNames);
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates and sign this certificate with the private key corresponding to the public key of
|
||||
* the certificate (hence the name "self-signed certificate").
|
||||
*/
|
||||
X509Certificate cert = certGenerator.generate(privKey);
|
||||
|
||||
/*
|
||||
* Checks that this certificate has indeed been correctly signed.
|
||||
*/
|
||||
cert.verify(pubKey);
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a self-signed certificate from a PGP Secret Key.
|
||||
*
|
||||
* @param pgpSecKey
|
||||
* PGP Secret Key (from which one can extract the public and private keys and other
|
||||
* attributes).
|
||||
* @param pgpPrivKey
|
||||
* PGP Private Key corresponding to the Secret Key (password callbacks should be done
|
||||
* before calling this method)
|
||||
* @param subjAltNameURI
|
||||
* optional URI to embed in the subject alternative-name
|
||||
* @return self-signed certificate
|
||||
* @throws PGPException
|
||||
* @throws NoSuchProviderException
|
||||
* @throws InvalidKeyException
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws SignatureException
|
||||
* @throws CertificateException
|
||||
*
|
||||
* @author Bruno Harbulot
|
||||
*/
|
||||
public static X509Certificate createSelfSignedCert(PGPSecretKey pgpSecKey,
|
||||
PGPPrivateKey pgpPrivKey, String subjAltNameURI) throws PGPException,
|
||||
NoSuchProviderException, InvalidKeyException, NoSuchAlgorithmException,
|
||||
SignatureException, CertificateException {
|
||||
// get public key from secret key
|
||||
PGPPublicKey pgpPubKey = pgpSecKey.getPublicKey();
|
||||
|
||||
// LOGGER.info("Key ID: " + Long.toHexString(pgpPubKey.getKeyID() & 0xffffffffL));
|
||||
|
||||
/*
|
||||
* The X.509 Name to be the subject DN is prepared. The CN is extracted from the Secret Key
|
||||
* user ID.
|
||||
*/
|
||||
Vector<DERObjectIdentifier> x509NameOids = new Vector<DERObjectIdentifier>();
|
||||
Vector<String> x509NameValues = new Vector<String>();
|
||||
|
||||
x509NameOids.add(X509Name.O);
|
||||
x509NameValues.add(DN_COMMON_PART_O);
|
||||
|
||||
x509NameOids.add(X509Name.OU);
|
||||
x509NameValues.add(DN_COMMON_PART_OU);
|
||||
|
||||
for (@SuppressWarnings("unchecked")
|
||||
Iterator<Object> it = (Iterator<Object>) pgpSecKey.getUserIDs(); it.hasNext();) {
|
||||
Object attrib = it.next();
|
||||
x509NameOids.add(X509Name.CN);
|
||||
x509NameValues.add("CryptoCall");
|
||||
// x509NameValues.add(attrib.toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* Currently unused.
|
||||
*/
|
||||
Log.d(Constants.TAG, "User attributes: ");
|
||||
for (@SuppressWarnings("unchecked")
|
||||
Iterator<Object> it = (Iterator<Object>) pgpSecKey.getUserAttributes(); it.hasNext();) {
|
||||
Object attrib = it.next();
|
||||
Log.d(Constants.TAG, " - " + attrib + " -- " + attrib.getClass());
|
||||
}
|
||||
|
||||
X509Name x509name = new X509Name(x509NameOids, x509NameValues);
|
||||
|
||||
Log.d(Constants.TAG, "Subject DN: " + x509name);
|
||||
|
||||
/*
|
||||
* To check the signature from the certificate on the recipient side, the creation time
|
||||
* needs to be embedded in the certificate. It seems natural to make this creation time be
|
||||
* the "not-before" date of the X.509 certificate. Unlimited PGP keys have a validity of 0
|
||||
* second. In this case, the "not-after" date will be the same as the not-before date. This
|
||||
* is something that needs to be checked by the service receiving this certificate.
|
||||
*/
|
||||
Date creationTime = pgpPubKey.getCreationTime();
|
||||
Log.d(Constants.TAG,
|
||||
"pgp pub key creation time=" + DateFormat.getDateInstance().format(creationTime));
|
||||
Log.d(Constants.TAG, "pgp valid seconds=" + pgpPubKey.getValidSeconds());
|
||||
Date validTo = null;
|
||||
if (pgpPubKey.getValidSeconds() > 0) {
|
||||
validTo = new Date(creationTime.getTime() + 1000L * pgpPubKey.getValidSeconds());
|
||||
}
|
||||
|
||||
X509Certificate selfSignedCert = createSelfSignedCert(
|
||||
pgpPubKey.getKey(Constants.BOUNCY_CASTLE_PROVIDER_NAME), pgpPrivKey.getKey(),
|
||||
x509name, creationTime, validTo, subjAltNameURI);
|
||||
|
||||
return selfSignedCert;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a password callback handler that will fill in a password automatically. Useful to
|
||||
* configure passwords in advance, but should be used with caution depending on how much you
|
||||
* allow passwords to be stored within your application.
|
||||
*
|
||||
* @author Bruno Harbulot.
|
||||
*
|
||||
*/
|
||||
public final static class PredefinedPasswordCallbackHandler implements CallbackHandler {
|
||||
|
||||
private char[] password;
|
||||
private String prompt;
|
||||
|
||||
public PredefinedPasswordCallbackHandler(String password) {
|
||||
this(password == null ? null : password.toCharArray(), null);
|
||||
}
|
||||
|
||||
public PredefinedPasswordCallbackHandler(char[] password) {
|
||||
this(password, null);
|
||||
}
|
||||
|
||||
public PredefinedPasswordCallbackHandler(String password, String prompt) {
|
||||
this(password == null ? null : password.toCharArray(), prompt);
|
||||
}
|
||||
|
||||
public PredefinedPasswordCallbackHandler(char[] password, String prompt) {
|
||||
this.password = password;
|
||||
this.prompt = prompt;
|
||||
}
|
||||
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
for (Callback callback : callbacks) {
|
||||
if (callback instanceof PasswordCallback) {
|
||||
PasswordCallback pwCallback = (PasswordCallback) callback;
|
||||
if ((this.prompt == null) || (this.prompt.equals(pwCallback.getPrompt()))) {
|
||||
pwCallback.setPassword(this.password);
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(callback, "Unrecognised callback.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected final Object clone() throws CloneNotSupportedException {
|
||||
throw new CloneNotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.sufficientlysecure.keychain.pgp.exception;
|
||||
|
||||
public class NoAsymmetricEncryptionException extends Exception {
|
||||
static final long serialVersionUID = 0xf812773343L;
|
||||
|
||||
public NoAsymmetricEncryptionException() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.sufficientlysecure.keychain.pgp.exception;
|
||||
|
||||
public class PgpGeneralException extends Exception {
|
||||
static final long serialVersionUID = 0xf812773342L;
|
||||
|
||||
public PgpGeneralException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.provider;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
public class KeychainContract {
|
||||
|
||||
interface KeyRingsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String TYPE = "type"; // see KeyTypes
|
||||
String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob
|
||||
}
|
||||
|
||||
interface KeysColumns {
|
||||
String KEY_ID = "key_id"; // not a database id
|
||||
String TYPE = "type"; // see KeyTypes
|
||||
String IS_MASTER_KEY = "is_master_key";
|
||||
String ALGORITHM = "algorithm";
|
||||
String KEY_SIZE = "key_size";
|
||||
String CAN_CERTIFY = "can_certify";
|
||||
String CAN_SIGN = "can_sign";
|
||||
String CAN_ENCRYPT = "can_encrypt";
|
||||
String IS_REVOKED = "is_revoked";
|
||||
String CREATION = "creation";
|
||||
String EXPIRY = "expiry";
|
||||
String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID
|
||||
String KEY_DATA = "key_data"; // PGPPublicKey/PGPSecretKey blob
|
||||
String RANK = "rank";
|
||||
}
|
||||
|
||||
interface UserIdsColumns {
|
||||
String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID
|
||||
String USER_ID = "user_id"; // not a database id
|
||||
String RANK = "rank";
|
||||
}
|
||||
|
||||
interface ApiAppsColumns {
|
||||
String PACKAGE_NAME = "package_name";
|
||||
String PACKAGE_SIGNATURE = "package_signature";
|
||||
String KEY_ID = "key_id"; // not a database id
|
||||
String ENCRYPTION_ALGORITHM = "encryption_algorithm";
|
||||
String HASH_ALORITHM = "hash_algorithm";
|
||||
String COMPRESSION = "compression";
|
||||
}
|
||||
|
||||
public static final class KeyTypes {
|
||||
public static final int PUBLIC = 0;
|
||||
public static final int SECRET = 1;
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider";
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_INTERNAL = Uri
|
||||
.parse("content://" + CONTENT_AUTHORITY);
|
||||
|
||||
public static final String BASE_KEY_RINGS = "key_rings";
|
||||
public static final String BASE_DATA = "data";
|
||||
|
||||
public static final String PATH_PUBLIC = "public";
|
||||
public static final String PATH_SECRET = "secret";
|
||||
|
||||
public static final String PATH_BY_MASTER_KEY_ID = "master_key_id";
|
||||
public static final String PATH_BY_KEY_ID = "key_id";
|
||||
public static final String PATH_BY_EMAILS = "emails";
|
||||
public static final String PATH_BY_LIKE_EMAIL = "like_email";
|
||||
|
||||
public static final String PATH_USER_IDS = "user_ids";
|
||||
public static final String PATH_KEYS = "keys";
|
||||
|
||||
public static final String BASE_API_APPS = "api_apps";
|
||||
public static final String PATH_BY_PACKAGE_NAME = "package_name";
|
||||
|
||||
public static class KeyRings implements KeyRingsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
/** Use if multiple items get returned */
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key_ring";
|
||||
|
||||
/** Use if a single item is returned */
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key_ring";
|
||||
|
||||
public static Uri buildPublicKeyRingsUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingsUri(String keyRingRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingsByMasterKeyIdUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC)
|
||||
.appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingsByKeyIdUri(String keyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_KEY_ID)
|
||||
.appendPath(keyId).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingsByEmailsUri(String emails) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_EMAILS)
|
||||
.appendPath(emails).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeyRingsByLikeEmailUri(String emails) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_LIKE_EMAIL)
|
||||
.appendPath(emails).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingsUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingsUri(String keyRingRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingsByMasterKeyIdUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET)
|
||||
.appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingsByKeyIdUri(String keyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_KEY_ID)
|
||||
.appendPath(keyId).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingsByEmailsUri(String emails) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_EMAILS)
|
||||
.appendPath(emails).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingsByLikeEmails(String emails) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_LIKE_EMAIL)
|
||||
.appendPath(emails).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Keys implements KeysColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
/** Use if multiple items get returned */
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key";
|
||||
|
||||
/** Use if a single item is returned */
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key";
|
||||
|
||||
public static Uri buildPublicKeysUri(String keyRingRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_KEYS).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicKeysUri(String keyRingRowId, String keyRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_KEYS).appendPath(keyRowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeysUri(String keyRingRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_KEYS).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeysUri(String keyRingRowId, String keyRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_KEYS).appendPath(keyRowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildKeysUri(Uri keyRingUri) {
|
||||
return keyRingUri.buildUpon().appendPath(PATH_KEYS).build();
|
||||
}
|
||||
|
||||
public static Uri buildKeysUri(Uri keyRingUri, String keyRowId) {
|
||||
return keyRingUri.buildUpon().appendPath(PATH_KEYS).appendPath(keyRowId).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class UserIds implements UserIdsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
/** Use if multiple items get returned */
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.user_id";
|
||||
|
||||
/** Use if a single item is returned */
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.user_id";
|
||||
|
||||
public static Uri buildPublicUserIdsUri(String keyRingRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildPublicUserIdsUri(String keyRingRowId, String userIdRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretUserIdsUri(String keyRingRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretUserIdsUri(String keyRingRowId, String userIdRowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
|
||||
.appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildUserIdsUri(Uri keyRingUri) {
|
||||
return keyRingUri.buildUpon().appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
|
||||
public static Uri buildUserIdsUri(Uri keyRingUri, String userIdRowId) {
|
||||
return keyRingUri.buildUpon().appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiApps implements ApiAppsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_API_APPS).build();
|
||||
|
||||
/** Use if multiple items get returned */
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.api_apps";
|
||||
|
||||
/** Use if a single item is returned */
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.api_apps";
|
||||
|
||||
public static Uri buildIdUri(String rowId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(rowId).build();
|
||||
}
|
||||
|
||||
public static Uri buildByPackageNameUri(String packageName) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DataStream {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_DATA).build();
|
||||
|
||||
public static Uri buildDataStreamUri(String streamFilename) {
|
||||
return CONTENT_URI.buildUpon().appendPath(streamFilename).build();
|
||||
}
|
||||
}
|
||||
|
||||
private KeychainContract() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.provider;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "apg.db";
|
||||
private static final int DATABASE_VERSION = 6;
|
||||
|
||||
public interface Tables {
|
||||
String KEY_RINGS = "key_rings";
|
||||
String KEYS = "keys";
|
||||
String USER_IDS = "user_ids";
|
||||
String API_APPS = "api_apps";
|
||||
}
|
||||
|
||||
private static final String CREATE_KEY_RINGS = "CREATE TABLE IF NOT EXISTS " + Tables.KEY_RINGS
|
||||
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ KeyRingsColumns.MASTER_KEY_ID + " INT64, " + KeyRingsColumns.TYPE + " INTEGER, "
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB)";
|
||||
|
||||
private static final String CREATE_KEYS = "CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " ("
|
||||
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + KeysColumns.KEY_ID
|
||||
+ " INT64, " + KeysColumns.TYPE + " INTEGER, " + KeysColumns.IS_MASTER_KEY
|
||||
+ " INTEGER, " + KeysColumns.ALGORITHM + " INTEGER, " + KeysColumns.KEY_SIZE
|
||||
+ " INTEGER, " + KeysColumns.CAN_CERTIFY + " INTEGER, " + KeysColumns.CAN_SIGN
|
||||
+ " INTEGER, " + KeysColumns.CAN_ENCRYPT + " INTEGER, " + KeysColumns.IS_REVOKED
|
||||
+ " INTEGER, " + KeysColumns.CREATION + " INTEGER, " + KeysColumns.EXPIRY
|
||||
+ " INTEGER, " + KeysColumns.KEY_DATA + " BLOB," + KeysColumns.RANK + " INTEGER, "
|
||||
+ KeysColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL, FOREIGN KEY("
|
||||
+ KeysColumns.KEY_RING_ROW_ID + ") REFERENCES " + Tables.KEY_RINGS + "("
|
||||
+ BaseColumns._ID + ") ON DELETE CASCADE)";
|
||||
|
||||
private static final String CREATE_USER_IDS = "CREATE TABLE IF NOT EXISTS " + Tables.USER_IDS
|
||||
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ UserIdsColumns.USER_ID + " TEXT, " + UserIdsColumns.RANK + " INTEGER, "
|
||||
+ UserIdsColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL, FOREIGN KEY("
|
||||
+ UserIdsColumns.KEY_RING_ROW_ID + ") REFERENCES " + Tables.KEY_RINGS + "("
|
||||
+ BaseColumns._ID + ") ON DELETE CASCADE)";
|
||||
|
||||
private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS
|
||||
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ ApiAppsColumns.PACKAGE_NAME + " TEXT UNIQUE, " + ApiAppsColumns.PACKAGE_SIGNATURE
|
||||
+ " BLOB, " + ApiAppsColumns.KEY_ID + " INT64, " + ApiAppsColumns.ENCRYPTION_ALGORITHM
|
||||
+ " INTEGER, " + ApiAppsColumns.HASH_ALORITHM + " INTEGER, "
|
||||
+ ApiAppsColumns.COMPRESSION + " INTEGER)";
|
||||
|
||||
KeychainDatabase(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
Log.w(Constants.TAG, "Creating database...");
|
||||
|
||||
db.execSQL(CREATE_KEY_RINGS);
|
||||
db.execSQL(CREATE_KEYS);
|
||||
db.execSQL(CREATE_USER_IDS);
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(SQLiteDatabase db) {
|
||||
super.onOpen(db);
|
||||
if (!db.isReadOnly()) {
|
||||
// Enable foreign key constraints
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log.w(Constants.TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
|
||||
|
||||
// Upgrade from oldVersion through all methods to newest one
|
||||
for (int version = oldVersion; version < newVersion; ++version) {
|
||||
Log.w(Constants.TAG, "Upgrading database to version " + version);
|
||||
|
||||
switch (version) {
|
||||
case 3:
|
||||
db.execSQL("ALTER TABLE " + Tables.KEYS + " ADD COLUMN " + KeysColumns.CAN_CERTIFY
|
||||
+ " INTEGER DEFAULT 0;");
|
||||
db.execSQL("UPDATE " + Tables.KEYS + " SET " + KeysColumns.CAN_CERTIFY
|
||||
+ " = 1 WHERE " + KeysColumns.IS_MASTER_KEY + "= 1;");
|
||||
break;
|
||||
case 4:
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
case 5:
|
||||
// new column: package_signature
|
||||
db.execSQL("DROP TABLE IF EXISTS " + Tables.API_APPS);
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,971 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyTypes;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.TextUtils;
|
||||
|
||||
public class KeychainProvider extends ContentProvider {
|
||||
// public static final String ACTION_BROADCAST_DATABASE_CHANGE = Constants.PACKAGE_NAME
|
||||
// + ".action.DATABASE_CHANGE";
|
||||
//
|
||||
// public static final String EXTRA_BROADCAST_KEY_TYPE = "key_type";
|
||||
// public static final String EXTRA_BROADCAST_CONTENT_ITEM_TYPE = "contentItemType";
|
||||
|
||||
private static final int PUBLIC_KEY_RING = 101;
|
||||
private static final int PUBLIC_KEY_RING_BY_ROW_ID = 102;
|
||||
private static final int PUBLIC_KEY_RING_BY_MASTER_KEY_ID = 103;
|
||||
private static final int PUBLIC_KEY_RING_BY_KEY_ID = 104;
|
||||
private static final int PUBLIC_KEY_RING_BY_EMAILS = 105;
|
||||
private static final int PUBLIC_KEY_RING_BY_LIKE_EMAIL = 106;
|
||||
|
||||
private static final int PUBLIC_KEY_RING_KEY = 111;
|
||||
private static final int PUBLIC_KEY_RING_KEY_BY_ROW_ID = 112;
|
||||
|
||||
private static final int PUBLIC_KEY_RING_USER_ID = 121;
|
||||
private static final int PUBLIC_KEY_RING_USER_ID_BY_ROW_ID = 122;
|
||||
|
||||
private static final int SECRET_KEY_RING = 201;
|
||||
private static final int SECRET_KEY_RING_BY_ROW_ID = 202;
|
||||
private static final int SECRET_KEY_RING_BY_MASTER_KEY_ID = 203;
|
||||
private static final int SECRET_KEY_RING_BY_KEY_ID = 204;
|
||||
private static final int SECRET_KEY_RING_BY_EMAILS = 205;
|
||||
private static final int SECRET_KEY_RING_BY_LIKE_EMAIL = 206;
|
||||
|
||||
private static final int SECRET_KEY_RING_KEY = 211;
|
||||
private static final int SECRET_KEY_RING_KEY_BY_ROW_ID = 212;
|
||||
|
||||
private static final int SECRET_KEY_RING_USER_ID = 221;
|
||||
private static final int SECRET_KEY_RING_USER_ID_BY_ROW_ID = 222;
|
||||
|
||||
private static final int API_APPS = 301;
|
||||
private static final int API_APPS_BY_ROW_ID = 302;
|
||||
private static final int API_APPS_BY_PACKAGE_NAME = 303;
|
||||
|
||||
// private static final int DATA_STREAM = 401;
|
||||
|
||||
protected UriMatcher mUriMatcher;
|
||||
|
||||
/**
|
||||
* 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 = KeychainContract.CONTENT_AUTHORITY;
|
||||
|
||||
/**
|
||||
* public key rings
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/public
|
||||
* key_rings/public/#
|
||||
* key_rings/public/master_key_id/_
|
||||
* key_rings/public/key_id/_
|
||||
* key_rings/public/emails/_
|
||||
* key_rings/public/like_email/_
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC, PUBLIC_KEY_RING);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/#", PUBLIC_KEY_RING_BY_ROW_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_MASTER_KEY_ID
|
||||
+ "/*", PUBLIC_KEY_RING_BY_MASTER_KEY_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_KEY_ID + "/*",
|
||||
PUBLIC_KEY_RING_BY_KEY_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_EMAILS + "/*",
|
||||
PUBLIC_KEY_RING_BY_EMAILS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_EMAILS,
|
||||
PUBLIC_KEY_RING_BY_EMAILS); // without emails specified
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/" + KeychainContract.PATH_BY_LIKE_EMAIL + "/*",
|
||||
PUBLIC_KEY_RING_BY_LIKE_EMAIL);
|
||||
|
||||
/**
|
||||
* public keys
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/public/#/keys
|
||||
* key_rings/public/#/keys/#
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_KEYS,
|
||||
PUBLIC_KEY_RING_KEY);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_KEYS + "/#",
|
||||
PUBLIC_KEY_RING_KEY_BY_ROW_ID);
|
||||
|
||||
/**
|
||||
* public user ids
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/public/#/user_ids
|
||||
* key_rings/public/#/user_ids/#
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_USER_IDS,
|
||||
PUBLIC_KEY_RING_USER_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_PUBLIC + "/#/" + KeychainContract.PATH_USER_IDS + "/#",
|
||||
PUBLIC_KEY_RING_USER_ID_BY_ROW_ID);
|
||||
|
||||
/**
|
||||
* secret key rings
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/secret
|
||||
* key_rings/secret/#
|
||||
* key_rings/secret/master_key_id/_
|
||||
* key_rings/secret/key_id/_
|
||||
* key_rings/secret/emails/_
|
||||
* key_rings/secret/like_email/_
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET, SECRET_KEY_RING);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/#", SECRET_KEY_RING_BY_ROW_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_MASTER_KEY_ID
|
||||
+ "/*", SECRET_KEY_RING_BY_MASTER_KEY_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_KEY_ID + "/*",
|
||||
SECRET_KEY_RING_BY_KEY_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_EMAILS + "/*",
|
||||
SECRET_KEY_RING_BY_EMAILS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_EMAILS,
|
||||
SECRET_KEY_RING_BY_EMAILS); // without emails specified
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/" + KeychainContract.PATH_BY_LIKE_EMAIL + "/*",
|
||||
SECRET_KEY_RING_BY_LIKE_EMAIL);
|
||||
|
||||
/**
|
||||
* secret keys
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/secret/#/keys
|
||||
* key_rings/secret/#/keys/#
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_KEYS,
|
||||
SECRET_KEY_RING_KEY);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_KEYS + "/#",
|
||||
SECRET_KEY_RING_KEY_BY_ROW_ID);
|
||||
|
||||
/**
|
||||
* secret user ids
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/secret/#/user_ids
|
||||
* key_rings/secret/#/user_ids/#
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_USER_IDS,
|
||||
SECRET_KEY_RING_USER_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_SECRET + "/#/" + KeychainContract.PATH_USER_IDS + "/#",
|
||||
SECRET_KEY_RING_USER_ID_BY_ROW_ID);
|
||||
|
||||
/**
|
||||
* API apps
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS, API_APPS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/#", API_APPS_BY_ROW_ID);
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/"
|
||||
+ KeychainContract.PATH_BY_PACKAGE_NAME + "/*", API_APPS_BY_PACKAGE_NAME);
|
||||
|
||||
/**
|
||||
* data stream
|
||||
*
|
||||
* <pre>
|
||||
* data / _
|
||||
* </pre>
|
||||
*/
|
||||
// matcher.addURI(authority, KeychainContract.BASE_DATA + "/*", DATA_STREAM);
|
||||
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private KeychainDatabase mApgDatabase;
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mUriMatcher = buildUriMatcher();
|
||||
mApgDatabase = new KeychainDatabase(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
switch (match) {
|
||||
case PUBLIC_KEY_RING:
|
||||
case PUBLIC_KEY_RING_BY_EMAILS:
|
||||
case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
|
||||
case SECRET_KEY_RING:
|
||||
case SECRET_KEY_RING_BY_EMAILS:
|
||||
case SECRET_KEY_RING_BY_LIKE_EMAIL:
|
||||
return KeyRings.CONTENT_TYPE;
|
||||
|
||||
case PUBLIC_KEY_RING_BY_ROW_ID:
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case PUBLIC_KEY_RING_BY_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_KEY_ID:
|
||||
return KeyRings.CONTENT_ITEM_TYPE;
|
||||
|
||||
case PUBLIC_KEY_RING_KEY:
|
||||
case SECRET_KEY_RING_KEY:
|
||||
return Keys.CONTENT_TYPE;
|
||||
|
||||
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_KEY_BY_ROW_ID:
|
||||
return Keys.CONTENT_ITEM_TYPE;
|
||||
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
case SECRET_KEY_RING_USER_ID:
|
||||
return UserIds.CONTENT_TYPE;
|
||||
|
||||
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
return UserIds.CONTENT_ITEM_TYPE;
|
||||
|
||||
case API_APPS:
|
||||
return ApiApps.CONTENT_TYPE;
|
||||
|
||||
case API_APPS_BY_ROW_ID:
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
return ApiApps.CONTENT_ITEM_TYPE;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns type of the query (secret/public)
|
||||
*
|
||||
* @param uri
|
||||
* @return
|
||||
*/
|
||||
private int getKeyType(int match) {
|
||||
int type;
|
||||
switch (match) {
|
||||
case PUBLIC_KEY_RING:
|
||||
case PUBLIC_KEY_RING_BY_ROW_ID:
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case PUBLIC_KEY_RING_BY_KEY_ID:
|
||||
case PUBLIC_KEY_RING_BY_EMAILS:
|
||||
case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
|
||||
case PUBLIC_KEY_RING_KEY:
|
||||
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
type = KeyTypes.PUBLIC;
|
||||
break;
|
||||
|
||||
case SECRET_KEY_RING:
|
||||
case SECRET_KEY_RING_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_EMAILS:
|
||||
case SECRET_KEY_RING_BY_LIKE_EMAIL:
|
||||
case SECRET_KEY_RING_KEY:
|
||||
case SECRET_KEY_RING_KEY_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_USER_ID:
|
||||
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
type = KeyTypes.SECRET;
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(Constants.TAG, "Unknown match " + match);
|
||||
type = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set result of query to specific columns, don't show blob column for external content provider
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private HashMap<String, String> getProjectionMapForKeyRings() {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
|
||||
projectionMap.put(BaseColumns._ID, Tables.KEY_RINGS + "." + BaseColumns._ID);
|
||||
projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "."
|
||||
+ KeyRingsColumns.MASTER_KEY_ID);
|
||||
projectionMap.put(KeyRingsColumns.KEY_RING_DATA, Tables.KEY_RINGS + "."
|
||||
+ KeyRingsColumns.KEY_RING_DATA);
|
||||
projectionMap.put(UserIdsColumns.USER_ID, Tables.USER_IDS + "." + UserIdsColumns.USER_ID);
|
||||
|
||||
return projectionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set result of query to specific columns, don't show blob column for external content provider
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private HashMap<String, String> getProjectionMapForKeys() {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
|
||||
projectionMap.put(BaseColumns._ID, BaseColumns._ID);
|
||||
projectionMap.put(KeysColumns.KEY_ID, KeysColumns.KEY_ID);
|
||||
projectionMap.put(KeysColumns.IS_MASTER_KEY, KeysColumns.IS_MASTER_KEY);
|
||||
projectionMap.put(KeysColumns.ALGORITHM, KeysColumns.ALGORITHM);
|
||||
projectionMap.put(KeysColumns.KEY_SIZE, KeysColumns.KEY_SIZE);
|
||||
projectionMap.put(KeysColumns.CAN_CERTIFY, KeysColumns.CAN_CERTIFY);
|
||||
projectionMap.put(KeysColumns.CAN_SIGN, KeysColumns.CAN_SIGN);
|
||||
projectionMap.put(KeysColumns.CAN_ENCRYPT, KeysColumns.CAN_ENCRYPT);
|
||||
projectionMap.put(KeysColumns.IS_REVOKED, KeysColumns.IS_REVOKED);
|
||||
projectionMap.put(KeysColumns.CREATION, KeysColumns.CREATION);
|
||||
projectionMap.put(KeysColumns.EXPIRY, KeysColumns.EXPIRY);
|
||||
projectionMap.put(KeysColumns.KEY_RING_ROW_ID, KeysColumns.KEY_RING_ROW_ID);
|
||||
projectionMap.put(KeysColumns.KEY_DATA, KeysColumns.KEY_DATA);
|
||||
projectionMap.put(KeysColumns.RANK, KeysColumns.RANK);
|
||||
|
||||
return projectionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds default query for keyRings: KeyRings table is joined with UserIds
|
||||
*
|
||||
* @param qb
|
||||
* @param match
|
||||
* @param isMasterKey
|
||||
* @param sortOrder
|
||||
* @return
|
||||
*/
|
||||
private SQLiteQueryBuilder buildKeyRingQuery(SQLiteQueryBuilder qb, int match, String sortOrder) {
|
||||
// public or secret keyring
|
||||
qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = ");
|
||||
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
|
||||
|
||||
// join keyrings with userIds to every keyring
|
||||
qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.USER_IDS + " ON " + "("
|
||||
+ Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "."
|
||||
+ UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "."
|
||||
+ UserIdsColumns.RANK + " = '0')");
|
||||
|
||||
qb.setProjectionMap(getProjectionMapForKeyRings());
|
||||
|
||||
return qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds default query for keyRings: KeyRings table is joined with Keys and UserIds
|
||||
*
|
||||
* @param qb
|
||||
* @param match
|
||||
* @param isMasterKey
|
||||
* @param sortOrder
|
||||
* @return
|
||||
*/
|
||||
private SQLiteQueryBuilder buildKeyRingQueryWithKeys(SQLiteQueryBuilder qb, int match,
|
||||
String sortOrder) {
|
||||
// public or secret keyring
|
||||
qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = ");
|
||||
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
|
||||
|
||||
// join keyrings with keys and userIds to every keyring
|
||||
qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.KEYS + " ON " + "("
|
||||
+ Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.KEYS + "."
|
||||
+ KeysColumns.KEY_RING_ROW_ID + ") " + " INNER JOIN " + Tables.USER_IDS + " ON "
|
||||
+ "(" + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "."
|
||||
+ UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "."
|
||||
+ UserIdsColumns.RANK + " = '0')");
|
||||
|
||||
qb.setProjectionMap(getProjectionMapForKeyRings());
|
||||
|
||||
return qb;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public Cursor query(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();
|
||||
SQLiteDatabase db = mApgDatabase.getReadableDatabase();
|
||||
|
||||
int match = mUriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case PUBLIC_KEY_RING:
|
||||
case SECRET_KEY_RING:
|
||||
qb = buildKeyRingQuery(qb, match, sortOrder);
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_BY_ROW_ID:
|
||||
qb = buildKeyRingQuery(qb, match, sortOrder);
|
||||
|
||||
qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + BaseColumns._ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
|
||||
qb = buildKeyRingQuery(qb, match, sortOrder);
|
||||
|
||||
qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SECRET_KEY_RING_BY_KEY_ID:
|
||||
case PUBLIC_KEY_RING_BY_KEY_ID:
|
||||
qb = buildKeyRingQueryWithKeys(qb, match, sortOrder);
|
||||
|
||||
qb.appendWhere(" AND " + Tables.KEYS + "." + KeysColumns.KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SECRET_KEY_RING_BY_EMAILS:
|
||||
case PUBLIC_KEY_RING_BY_EMAILS:
|
||||
qb = buildKeyRingQuery(qb, match, sortOrder);
|
||||
|
||||
String emails = uri.getLastPathSegment();
|
||||
String chunks[] = emails.split(" *, *");
|
||||
boolean gotCondition = false;
|
||||
String emailWhere = "";
|
||||
for (int i = 0; i < chunks.length; ++i) {
|
||||
if (chunks[i].length() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (i != 0) {
|
||||
emailWhere += " OR ";
|
||||
}
|
||||
emailWhere += "tmp." + UserIdsColumns.USER_ID + " LIKE ";
|
||||
// match '*<email>', so it has to be at the *end* of the user id
|
||||
emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
|
||||
gotCondition = true;
|
||||
}
|
||||
|
||||
if (gotCondition) {
|
||||
qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM "
|
||||
+ Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID
|
||||
+ " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + emailWhere
|
||||
+ "))");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SECRET_KEY_RING_BY_LIKE_EMAIL:
|
||||
case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
|
||||
qb = buildKeyRingQuery(qb, match, sortOrder);
|
||||
|
||||
String likeEmail = uri.getLastPathSegment();
|
||||
|
||||
String likeEmailWhere = "tmp." + UserIdsColumns.USER_ID + " LIKE "
|
||||
+ DatabaseUtils.sqlEscapeString("%<%" + likeEmail + "%>");
|
||||
|
||||
qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM "
|
||||
+ Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID
|
||||
+ " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + likeEmailWhere
|
||||
+ "))");
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_KEY:
|
||||
case SECRET_KEY_RING_KEY:
|
||||
qb.setTables(Tables.KEYS);
|
||||
qb.appendWhere(KeysColumns.TYPE + " = ");
|
||||
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
|
||||
|
||||
qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
|
||||
|
||||
qb.setProjectionMap(getProjectionMapForKeys());
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_KEY_BY_ROW_ID:
|
||||
qb.setTables(Tables.KEYS);
|
||||
qb.appendWhere(KeysColumns.TYPE + " = ");
|
||||
qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
|
||||
|
||||
qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
|
||||
|
||||
qb.appendWhere(" AND " + BaseColumns._ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
qb.setProjectionMap(getProjectionMapForKeys());
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
case SECRET_KEY_RING_USER_ID:
|
||||
qb.setTables(Tables.USER_IDS);
|
||||
qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
|
||||
|
||||
break;
|
||||
|
||||
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
qb.setTables(Tables.USER_IDS);
|
||||
qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
|
||||
|
||||
qb.appendWhere(" AND " + BaseColumns._ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
break;
|
||||
|
||||
case API_APPS:
|
||||
qb.setTables(Tables.API_APPS);
|
||||
|
||||
break;
|
||||
case API_APPS_BY_ROW_ID:
|
||||
qb.setTables(Tables.API_APPS);
|
||||
|
||||
qb.appendWhere(BaseColumns._ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
break;
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
qb.setTables(Tables.API_APPS);
|
||||
qb.appendWhere(ApiApps.PACKAGE_NAME + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(2));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI " + uri);
|
||||
|
||||
}
|
||||
|
||||
// If no sort order is specified use the default
|
||||
String orderBy;
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
orderBy = null;
|
||||
} else {
|
||||
orderBy = sortOrder;
|
||||
}
|
||||
|
||||
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
|
||||
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
c.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
if (Constants.DEBUG) {
|
||||
Log.d(Constants.TAG,
|
||||
"Query: "
|
||||
+ qb.buildQuery(projection, selection, selectionArgs, null, null,
|
||||
orderBy, null));
|
||||
Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(c));
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
Log.d(Constants.TAG, "insert(uri=" + uri + ", values=" + values.toString() + ")");
|
||||
|
||||
final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
|
||||
|
||||
Uri rowUri = null;
|
||||
long rowId = -1;
|
||||
try {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case PUBLIC_KEY_RING:
|
||||
values.put(KeyRings.TYPE, KeyTypes.PUBLIC);
|
||||
|
||||
rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values);
|
||||
rowUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId));
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case PUBLIC_KEY_RING_KEY:
|
||||
values.put(Keys.TYPE, KeyTypes.PUBLIC);
|
||||
|
||||
rowId = db.insertOrThrow(Tables.KEYS, null, values);
|
||||
rowUri = Keys.buildPublicKeysUri(Long.toString(rowId));
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case PUBLIC_KEY_RING_USER_ID:
|
||||
rowId = db.insertOrThrow(Tables.USER_IDS, null, values);
|
||||
rowUri = UserIds.buildPublicUserIdsUri(Long.toString(rowId));
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case SECRET_KEY_RING:
|
||||
values.put(KeyRings.TYPE, KeyTypes.SECRET);
|
||||
|
||||
rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values);
|
||||
rowUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId));
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case SECRET_KEY_RING_KEY:
|
||||
values.put(Keys.TYPE, KeyTypes.SECRET);
|
||||
|
||||
rowId = db.insertOrThrow(Tables.KEYS, null, values);
|
||||
rowUri = Keys.buildSecretKeysUri(Long.toString(rowId));
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case SECRET_KEY_RING_USER_ID:
|
||||
rowId = db.insertOrThrow(Tables.USER_IDS, null, values);
|
||||
rowUri = UserIds.buildSecretUserIdsUri(Long.toString(rowId));
|
||||
|
||||
break;
|
||||
case API_APPS:
|
||||
rowId = db.insertOrThrow(Tables.API_APPS, null, values);
|
||||
rowUri = ApiApps.buildIdUri(Long.toString(rowId));
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.e(Constants.TAG, "Constraint exception on insert! Entry already existing?");
|
||||
}
|
||||
|
||||
return rowUri;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
Log.v(Constants.TAG, "delete(uri=" + uri + ")");
|
||||
|
||||
final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
|
||||
|
||||
int count;
|
||||
final int match = mUriMatcher.match(uri);
|
||||
|
||||
String defaultSelection = null;
|
||||
switch (match) {
|
||||
case PUBLIC_KEY_RING_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_BY_ROW_ID:
|
||||
defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment();
|
||||
// corresponding keys and userIds are deleted by ON DELETE CASCADE
|
||||
count = db.delete(Tables.KEY_RINGS,
|
||||
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection),
|
||||
selectionArgs);
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
break;
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
|
||||
defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment();
|
||||
// corresponding keys and userIds are deleted by ON DELETE CASCADE
|
||||
count = db.delete(Tables.KEY_RINGS,
|
||||
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection),
|
||||
selectionArgs);
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
break;
|
||||
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_KEY_BY_ROW_ID:
|
||||
count = db.delete(Tables.KEYS,
|
||||
buildDefaultKeysSelection(uri, getKeyType(match), selection), selectionArgs);
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
break;
|
||||
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
count = db.delete(Tables.KEYS, buildDefaultUserIdsSelection(uri, selection),
|
||||
selectionArgs);
|
||||
break;
|
||||
case API_APPS_BY_ROW_ID:
|
||||
count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, false, selection),
|
||||
selectionArgs);
|
||||
break;
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, true, selection),
|
||||
selectionArgs);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")");
|
||||
|
||||
final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
|
||||
|
||||
String defaultSelection = null;
|
||||
int count = 0;
|
||||
try {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
switch (match) {
|
||||
case PUBLIC_KEY_RING_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_BY_ROW_ID:
|
||||
defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment();
|
||||
|
||||
count = db.update(
|
||||
Tables.KEY_RINGS,
|
||||
values,
|
||||
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match),
|
||||
selection), selectionArgs);
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
|
||||
case SECRET_KEY_RING_BY_MASTER_KEY_ID:
|
||||
defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment();
|
||||
|
||||
count = db.update(
|
||||
Tables.KEY_RINGS,
|
||||
values,
|
||||
buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match),
|
||||
selection), selectionArgs);
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_KEY_BY_ROW_ID:
|
||||
count = db
|
||||
.update(Tables.KEYS, values,
|
||||
buildDefaultKeysSelection(uri, getKeyType(match), selection),
|
||||
selectionArgs);
|
||||
sendBroadcastDatabaseChange(getKeyType(match), getType(uri));
|
||||
|
||||
break;
|
||||
case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
|
||||
count = db.update(Tables.USER_IDS, values,
|
||||
buildDefaultUserIdsSelection(uri, selection), selectionArgs);
|
||||
break;
|
||||
case API_APPS_BY_ROW_ID:
|
||||
count = db.update(Tables.API_APPS, values,
|
||||
buildDefaultApiAppsSelection(uri, false, selection), selectionArgs);
|
||||
break;
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
count = db.update(Tables.API_APPS, values,
|
||||
buildDefaultApiAppsSelection(uri, true, selection), selectionArgs);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.e(Constants.TAG, "Constraint exception on update! Entry already existing?");
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default selection statement for KeyRings. If no extra selection is specified only build
|
||||
* where clause with rowId
|
||||
*
|
||||
* @param uri
|
||||
* @param selection
|
||||
* @return
|
||||
*/
|
||||
private String buildDefaultKeyRingsSelection(String defaultSelection, Integer keyType,
|
||||
String selection) {
|
||||
String andType = "";
|
||||
if (keyType != null) {
|
||||
andType = " AND " + KeyRingsColumns.TYPE + "=" + keyType;
|
||||
}
|
||||
|
||||
String andSelection = "";
|
||||
if (!TextUtils.isEmpty(selection)) {
|
||||
andSelection = " AND (" + selection + ")";
|
||||
}
|
||||
|
||||
return defaultSelection + andType + andSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default selection statement for Keys. If no extra selection is specified only build
|
||||
* where clause with rowId
|
||||
*
|
||||
* @param uri
|
||||
* @param selection
|
||||
* @return
|
||||
*/
|
||||
private String buildDefaultKeysSelection(Uri uri, Integer keyType, String selection) {
|
||||
String rowId = uri.getLastPathSegment();
|
||||
|
||||
String foreignKeyRingRowId = uri.getPathSegments().get(2);
|
||||
String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = "
|
||||
+ foreignKeyRingRowId;
|
||||
|
||||
String andType = "";
|
||||
if (keyType != null) {
|
||||
andType = " AND " + KeysColumns.TYPE + "=" + keyType;
|
||||
}
|
||||
|
||||
String andSelection = "";
|
||||
if (!TextUtils.isEmpty(selection)) {
|
||||
andSelection = " AND (" + selection + ")";
|
||||
}
|
||||
|
||||
return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andType + andSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default selection statement for UserIds. If no extra selection is specified only build
|
||||
* where clause with rowId
|
||||
*
|
||||
* @param uri
|
||||
* @param selection
|
||||
* @return
|
||||
*/
|
||||
private String buildDefaultUserIdsSelection(Uri uri, String selection) {
|
||||
String rowId = uri.getLastPathSegment();
|
||||
|
||||
String foreignKeyRingRowId = uri.getPathSegments().get(2);
|
||||
String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = "
|
||||
+ foreignKeyRingRowId;
|
||||
|
||||
String andSelection = "";
|
||||
if (!TextUtils.isEmpty(selection)) {
|
||||
andSelection = " AND (" + selection + ")";
|
||||
}
|
||||
|
||||
return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default selection statement for API apps. If no extra selection is specified only build
|
||||
* where clause with rowId
|
||||
*
|
||||
* @param uri
|
||||
* @param selection
|
||||
* @return
|
||||
*/
|
||||
private String buildDefaultApiAppsSelection(Uri uri, boolean packageSelection, String selection) {
|
||||
String lastPathSegment = uri.getLastPathSegment();
|
||||
|
||||
String andSelection = "";
|
||||
if (!TextUtils.isEmpty(selection)) {
|
||||
andSelection = " AND (" + selection + ")";
|
||||
}
|
||||
|
||||
if (packageSelection) {
|
||||
return ApiApps.PACKAGE_NAME + "=" + lastPathSegment + andSelection;
|
||||
} else {
|
||||
return BaseColumns._ID + "=" + lastPathSegment + andSelection;
|
||||
}
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
// int match = mUriMatcher.match(uri);
|
||||
// if (match != DATA_STREAM) {
|
||||
// throw new FileNotFoundException();
|
||||
// }
|
||||
// String fileName = uri.getLastPathSegment();
|
||||
// File file = new File(getContext().getFilesDir().getAbsolutePath(), fileName);
|
||||
// return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
// }
|
||||
|
||||
/**
|
||||
* This broadcast is send system wide to inform other application that a keyring was inserted,
|
||||
* updated, or deleted
|
||||
*/
|
||||
private void sendBroadcastDatabaseChange(int keyType, String contentItemType) {
|
||||
// TODO: Disabled, old API
|
||||
// Intent intent = new Intent();
|
||||
// intent.setAction(ACTION_BROADCAST_DATABASE_CHANGE);
|
||||
// intent.putExtra(EXTRA_BROADCAST_KEY_TYPE, keyType);
|
||||
// intent.putExtra(EXTRA_BROADCAST_CONTENT_ITEM_TYPE, contentItemType);
|
||||
//
|
||||
// getContext().sendBroadcast(intent, Constants.PERMISSION_ACCESS_API);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.provider;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
public class KeychainServiceBlobContract {
|
||||
|
||||
interface BlobsColumns {
|
||||
String KEY = "key";
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".blobs";
|
||||
|
||||
private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
|
||||
|
||||
public static class Blobs implements BlobsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI;
|
||||
}
|
||||
|
||||
private KeychainServiceBlobContract() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Markus Doits <markus.doits@googlemail.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.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
|
||||
|
||||
public class KeychainServiceBlobDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "apg_blob.db";
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
|
||||
public static final String TABLE = "data";
|
||||
|
||||
public KeychainServiceBlobDatabase(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TABLE + " ( " + BaseColumns._ID
|
||||
+ " INTEGER PRIMARY KEY AUTOINCREMENT, " + BlobsColumns.KEY + " TEXT NOT NULL)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// no upgrade necessary yet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Markus Doits <markus.doits@googlemail.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 org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.Blobs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class KeychainServiceBlobProvider extends ContentProvider {
|
||||
private static final String STORE_PATH = Constants.path.APP_DIR + "/ApgBlobs";
|
||||
|
||||
private KeychainServiceBlobDatabase mBlobDatabase = null;
|
||||
|
||||
public KeychainServiceBlobProvider() {
|
||||
File dir = new File(STORE_PATH);
|
||||
dir.mkdirs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mBlobDatabase = new KeychainServiceBlobDatabase(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues ignored) {
|
||||
// ContentValues are actually ignored, because we want to store a blob with no more
|
||||
// information but have to create an record with the password generated here first
|
||||
ContentValues vals = new ContentValues();
|
||||
|
||||
// Insert a random key in the database. This has to provided by the caller when updating or
|
||||
// getting the blob
|
||||
String password = UUID.randomUUID().toString();
|
||||
vals.put(BlobsColumns.KEY, password);
|
||||
|
||||
SQLiteDatabase db = mBlobDatabase.getWritableDatabase();
|
||||
long newRowId = db.insert(KeychainServiceBlobDatabase.TABLE, null, vals);
|
||||
Uri insertedUri = ContentUris.withAppendedId(Blobs.CONTENT_URI, newRowId);
|
||||
|
||||
return Uri.withAppendedPath(insertedUri, password);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws SecurityException,
|
||||
FileNotFoundException {
|
||||
Log.d(Constants.TAG, "openFile() called with uri: " + uri.toString() + " and mode: " + mode);
|
||||
|
||||
List<String> segments = uri.getPathSegments();
|
||||
if (segments.size() < 2) {
|
||||
throw new SecurityException("Password not found in URI");
|
||||
}
|
||||
String id = segments.get(0);
|
||||
String key = segments.get(1);
|
||||
|
||||
Log.d(Constants.TAG, "Got id: " + id + " and key: " + key);
|
||||
|
||||
// get the data
|
||||
SQLiteDatabase db = mBlobDatabase.getReadableDatabase();
|
||||
Cursor result = db.query(KeychainServiceBlobDatabase.TABLE, new String[] { BaseColumns._ID },
|
||||
BaseColumns._ID + " = ? and " + BlobsColumns.KEY + " = ?",
|
||||
new String[] { id, key }, null, null, null);
|
||||
|
||||
if (result.getCount() == 0) {
|
||||
// either the key is wrong or no id exists
|
||||
throw new FileNotFoundException("No file found with that ID and/or password");
|
||||
}
|
||||
|
||||
File targetFile = new File(STORE_PATH, id);
|
||||
if (mode.equals("w")) {
|
||||
Log.d(Constants.TAG, "Try to open file w");
|
||||
if (!targetFile.exists()) {
|
||||
try {
|
||||
targetFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Got IEOException on creating new file", e);
|
||||
throw new FileNotFoundException("Could not create file to write to");
|
||||
}
|
||||
}
|
||||
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE);
|
||||
} else if (mode.equals("r")) {
|
||||
Log.d(Constants.TAG, "Try to open file r");
|
||||
if (!targetFile.exists()) {
|
||||
throw new FileNotFoundException("Error: Could not find the file requested");
|
||||
}
|
||||
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.provider;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import org.spongycastle.bcpg.ArmoredOutputStream;
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.service.remote.AppSettings;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
public class ProviderHelper {
|
||||
|
||||
/**
|
||||
* Private helper method to get PGPKeyRing from database
|
||||
*
|
||||
* @param context
|
||||
* @param queryUri
|
||||
* @return
|
||||
*/
|
||||
public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) {
|
||||
Cursor cursor = context.getContentResolver().query(queryUri,
|
||||
new String[] { KeyRings._ID, KeyRings.KEY_RING_DATA }, null, null, null);
|
||||
|
||||
PGPKeyRing keyRing = null;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int keyRingDataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
|
||||
|
||||
byte[] data = cursor.getBlob(keyRingDataCol);
|
||||
if (data != null) {
|
||||
keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return keyRing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPPublicKeyRing object from the database blob based on the rowId
|
||||
*
|
||||
* @param context
|
||||
* @param rowId
|
||||
* @return
|
||||
*/
|
||||
public static PGPPublicKeyRing getPGPPublicKeyRingByRowId(Context context, long rowId) {
|
||||
Uri queryUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId));
|
||||
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPPublicKeyRing object from the database blob based on the maserKeyId
|
||||
*
|
||||
* @param context
|
||||
* @param masterKeyId
|
||||
* @return
|
||||
*/
|
||||
public static PGPPublicKeyRing getPGPPublicKeyRingByMasterKeyId(Context context,
|
||||
long masterKeyId) {
|
||||
Uri queryUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
|
||||
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPPublicKeyRing object from the database blob associated with a key
|
||||
* with this keyId
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
public static PGPPublicKeyRing getPGPPublicKeyRingByKeyId(Context context, long keyId) {
|
||||
Uri queryUri = KeyRings.buildPublicKeyRingsByKeyIdUri(Long.toString(keyId));
|
||||
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPPublicKey object from the database blob associated with a key with
|
||||
* this keyId
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
public static PGPPublicKey getPGPPublicKeyByKeyId(Context context, long keyId) {
|
||||
PGPPublicKeyRing keyRing = getPGPPublicKeyRingByKeyId(context, keyId);
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return keyRing.getPublicKey(keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPSecretKeyRing object from the database blob based on the rowId
|
||||
*
|
||||
* @param context
|
||||
* @param rowId
|
||||
* @return
|
||||
*/
|
||||
public static PGPSecretKeyRing getPGPSecretKeyRingByRowId(Context context, long rowId) {
|
||||
Uri queryUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId));
|
||||
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPSecretKeyRing object from the database blob based on the maserKeyId
|
||||
*
|
||||
* @param context
|
||||
* @param masterKeyId
|
||||
* @return
|
||||
*/
|
||||
public static PGPSecretKeyRing getPGPSecretKeyRingByMasterKeyId(Context context,
|
||||
long masterKeyId) {
|
||||
Uri queryUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
|
||||
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPSecretKeyRing object from the database blob associated with a key
|
||||
* with this keyId
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
public static PGPSecretKeyRing getPGPSecretKeyRingByKeyId(Context context, long keyId) {
|
||||
Uri queryUri = KeyRings.buildSecretKeyRingsByKeyIdUri(Long.toString(keyId));
|
||||
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPSecretKey object from the database blob associated with a key with
|
||||
* this keyId
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
public static PGPSecretKey getPGPSecretKeyByKeyId(Context context, long keyId) {
|
||||
PGPSecretKeyRing keyRing = getPGPSecretKeyRingByKeyId(context, keyId);
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return keyRing.getSecretKey(keyId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves PGPPublicKeyRing with its keys and userIds in DB
|
||||
*
|
||||
* @param context
|
||||
* @param keyRing
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws GeneralException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void saveKeyRing(Context context, PGPPublicKeyRing keyRing) throws IOException {
|
||||
PGPPublicKey masterKey = keyRing.getPublicKey();
|
||||
long masterKeyId = masterKey.getKeyID();
|
||||
|
||||
// delete old version of this keyRing, which also deletes all keys and userIds on cascade
|
||||
Uri deleteUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
|
||||
|
||||
try {
|
||||
context.getContentResolver().delete(deleteUri, null, null);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
|
||||
}
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
|
||||
|
||||
// insert new version of this keyRing
|
||||
Uri uri = KeyRings.buildPublicKeyRingsUri();
|
||||
Uri insertedUri = context.getContentResolver().insert(uri, values);
|
||||
long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment());
|
||||
|
||||
// save all keys and userIds included in keyRing object in database
|
||||
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
|
||||
|
||||
int rank = 0;
|
||||
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
|
||||
operations.add(buildPublicKeyOperations(context, keyRingRowId, key, rank));
|
||||
++rank;
|
||||
}
|
||||
|
||||
int userIdRank = 0;
|
||||
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
|
||||
operations.add(buildPublicUserIdOperations(context, keyRingRowId, userId, userIdRank));
|
||||
++userIdRank;
|
||||
}
|
||||
|
||||
try {
|
||||
context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "applyBatch failed!", e);
|
||||
} catch (OperationApplicationException e) {
|
||||
Log.e(Constants.TAG, "applyBatch failed!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves PGPSecretKeyRing with its keys and userIds in DB
|
||||
*
|
||||
* @param context
|
||||
* @param keyRing
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws GeneralException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void saveKeyRing(Context context, PGPSecretKeyRing keyRing) throws IOException {
|
||||
PGPSecretKey masterKey = keyRing.getSecretKey();
|
||||
long masterKeyId = masterKey.getKeyID();
|
||||
|
||||
// delete old version of this keyRing, which also deletes all keys and userIds on cascade
|
||||
Uri deleteUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
|
||||
|
||||
try {
|
||||
context.getContentResolver().delete(deleteUri, null, null);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
|
||||
}
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
|
||||
|
||||
// insert new version of this keyRing
|
||||
Uri uri = KeyRings.buildSecretKeyRingsUri();
|
||||
Uri insertedUri = context.getContentResolver().insert(uri, values);
|
||||
long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment());
|
||||
|
||||
// save all keys and userIds included in keyRing object in database
|
||||
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
|
||||
|
||||
int rank = 0;
|
||||
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
|
||||
operations.add(buildSecretKeyOperations(context, keyRingRowId, key, rank));
|
||||
++rank;
|
||||
}
|
||||
|
||||
int userIdRank = 0;
|
||||
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
|
||||
operations.add(buildSecretUserIdOperations(context, keyRingRowId, userId, userIdRank));
|
||||
++userIdRank;
|
||||
}
|
||||
|
||||
try {
|
||||
context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "applyBatch failed!", e);
|
||||
} catch (OperationApplicationException e) {
|
||||
Log.e(Constants.TAG, "applyBatch failed!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @param key
|
||||
* @param rank
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private static ContentProviderOperation buildPublicKeyOperations(Context context,
|
||||
long keyRingRowId, PGPPublicKey key, int rank) throws IOException {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Keys.KEY_ID, key.getKeyID());
|
||||
values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
|
||||
values.put(Keys.ALGORITHM, key.getAlgorithm());
|
||||
values.put(Keys.KEY_SIZE, key.getBitStrength());
|
||||
values.put(Keys.CAN_SIGN, PgpKeyHelper.isSigningKey(key));
|
||||
values.put(Keys.CAN_ENCRYPT, PgpKeyHelper.isEncryptionKey(key));
|
||||
values.put(Keys.IS_REVOKED, key.isRevoked());
|
||||
values.put(Keys.CREATION, PgpKeyHelper.getCreationDate(key).getTime() / 1000);
|
||||
Date expiryDate = PgpKeyHelper.getExpiryDate(key);
|
||||
if (expiryDate != null) {
|
||||
values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
|
||||
}
|
||||
values.put(Keys.KEY_RING_ROW_ID, keyRingRowId);
|
||||
values.put(Keys.KEY_DATA, key.getEncoded());
|
||||
values.put(Keys.RANK, rank);
|
||||
|
||||
Uri uri = Keys.buildPublicKeysUri(Long.toString(keyRingRowId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @param key
|
||||
* @param rank
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private static ContentProviderOperation buildPublicUserIdOperations(Context context,
|
||||
long keyRingRowId, String userId, int rank) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId);
|
||||
values.put(UserIds.USER_ID, userId);
|
||||
values.put(UserIds.RANK, rank);
|
||||
|
||||
Uri uri = UserIds.buildPublicUserIdsUri(Long.toString(keyRingRowId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PGPSecretKey to database corresponding to a keyRing
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @param key
|
||||
* @param rank
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private static ContentProviderOperation buildSecretKeyOperations(Context context,
|
||||
long keyRingRowId, PGPSecretKey key, int rank) throws IOException {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
boolean has_private = true;
|
||||
if (key.isMasterKey()) {
|
||||
if (PgpKeyHelper.isSecretKeyPrivateEmpty(key)) {
|
||||
has_private = false;
|
||||
}
|
||||
}
|
||||
|
||||
values.put(Keys.KEY_ID, key.getKeyID());
|
||||
values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
|
||||
values.put(Keys.ALGORITHM, key.getPublicKey().getAlgorithm());
|
||||
values.put(Keys.KEY_SIZE, key.getPublicKey().getBitStrength());
|
||||
values.put(Keys.CAN_CERTIFY, (PgpKeyHelper.isCertificationKey(key) && has_private));
|
||||
values.put(Keys.CAN_SIGN, (PgpKeyHelper.isSigningKey(key) && has_private));
|
||||
values.put(Keys.CAN_ENCRYPT, PgpKeyHelper.isEncryptionKey(key));
|
||||
values.put(Keys.IS_REVOKED, key.getPublicKey().isRevoked());
|
||||
values.put(Keys.CREATION, PgpKeyHelper.getCreationDate(key).getTime() / 1000);
|
||||
Date expiryDate = PgpKeyHelper.getExpiryDate(key);
|
||||
if (expiryDate != null) {
|
||||
values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
|
||||
}
|
||||
values.put(Keys.KEY_RING_ROW_ID, keyRingRowId);
|
||||
values.put(Keys.KEY_DATA, key.getEncoded());
|
||||
values.put(Keys.RANK, rank);
|
||||
|
||||
Uri uri = Keys.buildSecretKeysUri(Long.toString(keyRingRowId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add SecretUserIds to database corresponding to a keyRing
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @param key
|
||||
* @param rank
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private static ContentProviderOperation buildSecretUserIdOperations(Context context,
|
||||
long keyRingRowId, String userId, int rank) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId);
|
||||
values.put(UserIds.USER_ID, userId);
|
||||
values.put(UserIds.RANK, rank);
|
||||
|
||||
Uri uri = UserIds.buildSecretUserIdsUri(Long.toString(keyRingRowId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper method
|
||||
*
|
||||
* @param context
|
||||
* @param queryUri
|
||||
* @return
|
||||
*/
|
||||
private static ArrayList<Long> getKeyRingsMasterKeyIds(Context context, Uri queryUri) {
|
||||
Cursor cursor = context.getContentResolver().query(queryUri,
|
||||
new String[] { KeyRings.MASTER_KEY_ID }, null, null, null);
|
||||
|
||||
ArrayList<Long> masterKeyIds = new ArrayList<Long>();
|
||||
if (cursor != null) {
|
||||
int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
masterKeyIds.add(cursor.getLong(masterKeyIdCol));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return masterKeyIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves ids of all SecretKeyRings
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static ArrayList<Long> getSecretKeyRingsMasterKeyIds(Context context) {
|
||||
Uri queryUri = KeyRings.buildSecretKeyRingsUri();
|
||||
return getKeyRingsMasterKeyIds(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves ids of all PublicKeyRings
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static ArrayList<Long> getPublicKeyRingsMasterKeyIds(Context context) {
|
||||
Uri queryUri = KeyRings.buildPublicKeyRingsUri();
|
||||
return getKeyRingsMasterKeyIds(context, queryUri);
|
||||
}
|
||||
|
||||
public static void deletePublicKeyRing(Context context, long rowId) {
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
cr.delete(KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)), null, null);
|
||||
}
|
||||
|
||||
public static void deleteSecretKeyRing(Context context, long rowId) {
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
cr.delete(KeyRings.buildSecretKeyRingsUri(Long.toString(rowId)), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get master key id of keyring by its row id
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @return
|
||||
*/
|
||||
public static long getPublicMasterKeyId(Context context, long keyRingRowId) {
|
||||
Uri queryUri = KeyRings.buildPublicKeyRingsUri(String.valueOf(keyRingRowId));
|
||||
return getMasterKeyId(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get empty status of master key of keyring by its row id
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @return
|
||||
*/
|
||||
public static boolean getSecretMasterKeyCanSign(Context context, long keyRingRowId) {
|
||||
Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId));
|
||||
return getMasterKeyCanSign(context, queryUri, keyRingRowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper method to get master key private empty status of keyring by its row id
|
||||
*
|
||||
* @param context
|
||||
* @param queryUri
|
||||
* @param keyRingRowId
|
||||
* @return
|
||||
*/
|
||||
private static boolean getMasterKeyCanSign(Context context, Uri queryUri, long keyRingRowId) {
|
||||
String[] projection = new String[] {
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
"(SELECT COUNT(sign_keys." + Keys._ID + ") FROM " + Tables.KEYS
|
||||
+ " AS sign_keys WHERE sign_keys." + Keys.KEY_RING_ROW_ID + " = "
|
||||
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
|
||||
+ " AND sign_keys." + Keys.CAN_SIGN + " = '1' AND " + Keys.IS_MASTER_KEY
|
||||
+ " = 1) AS sign", };
|
||||
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
Cursor cursor = cr.query(queryUri, projection, null, null, null);
|
||||
|
||||
long masterKeyId = -1;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int masterKeyIdCol = cursor.getColumnIndex("sign");
|
||||
|
||||
masterKeyId = cursor.getLong(masterKeyIdCol);
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return (masterKeyId > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get master key id of keyring by its row id
|
||||
*
|
||||
* @param context
|
||||
* @param keyRingRowId
|
||||
* @return
|
||||
*/
|
||||
public static long getSecretMasterKeyId(Context context, long keyRingRowId) {
|
||||
Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId));
|
||||
return getMasterKeyId(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper method to get master key id of keyring by its row id
|
||||
*
|
||||
* @param context
|
||||
* @param queryUri
|
||||
* @param keyRingRowId
|
||||
* @return
|
||||
*/
|
||||
public static long getMasterKeyId(Context context, Uri queryUri) {
|
||||
String[] projection = new String[] { KeyRings.MASTER_KEY_ID };
|
||||
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
Cursor cursor = cr.query(queryUri, projection, null, null, null);
|
||||
|
||||
long masterKeyId = -1;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
|
||||
|
||||
masterKeyId = cursor.getLong(masterKeyIdCol);
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return masterKeyId;
|
||||
}
|
||||
|
||||
public static ArrayList<String> getPublicKeyRingsAsArmoredString(Context context,
|
||||
long[] masterKeyIds) {
|
||||
return getKeyRingsAsArmoredString(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds);
|
||||
}
|
||||
|
||||
public static ArrayList<String> getSecretKeyRingsAsArmoredString(Context context,
|
||||
long[] masterKeyIds) {
|
||||
return getKeyRingsAsArmoredString(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds);
|
||||
}
|
||||
|
||||
public static ArrayList<String> getKeyRingsAsArmoredString(Context context, Uri uri,
|
||||
long[] masterKeyIds) {
|
||||
ArrayList<String> output = new ArrayList<String>();
|
||||
|
||||
if (masterKeyIds != null && masterKeyIds.length > 0) {
|
||||
|
||||
Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds);
|
||||
|
||||
if (cursor != null) {
|
||||
int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
|
||||
int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
|
||||
|
||||
// get actual keyring data blob and write it to ByteArrayOutputStream
|
||||
try {
|
||||
Object keyRing = null;
|
||||
byte[] data = cursor.getBlob(dataCol);
|
||||
if (data != null) {
|
||||
keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
|
||||
aos.setHeader("Version", PgpHelper.getFullVersion(context));
|
||||
|
||||
if (keyRing instanceof PGPSecretKeyRing) {
|
||||
aos.write(((PGPSecretKeyRing) keyRing).getEncoded());
|
||||
} else if (keyRing instanceof PGPPublicKeyRing) {
|
||||
aos.write(((PGPPublicKeyRing) keyRing).getEncoded());
|
||||
}
|
||||
aos.close();
|
||||
|
||||
String armoredKey = bos.toString("UTF-8");
|
||||
|
||||
Log.d(Constants.TAG, "armouredKey:" + armoredKey);
|
||||
|
||||
output.add(armoredKey);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "IOException", e);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(Constants.TAG, "No master keys given!");
|
||||
}
|
||||
|
||||
if (output.size() > 0) {
|
||||
return output;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getPublicKeyRingsAsByteArray(Context context, long[] masterKeyIds) {
|
||||
return getKeyRingsAsByteArray(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds);
|
||||
}
|
||||
|
||||
public static byte[] getSecretKeyRingsAsByteArray(Context context, long[] masterKeyIds) {
|
||||
return getKeyRingsAsByteArray(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds);
|
||||
}
|
||||
|
||||
public static byte[] getKeyRingsAsByteArray(Context context, Uri uri, long[] masterKeyIds) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
if (masterKeyIds != null && masterKeyIds.length > 0) {
|
||||
|
||||
Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds);
|
||||
|
||||
if (cursor != null) {
|
||||
int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
|
||||
int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
|
||||
|
||||
// get actual keyring data blob and write it to ByteArrayOutputStream
|
||||
try {
|
||||
bos.write(cursor.getBlob(dataCol));
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "IOException", e);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(Constants.TAG, "No master keys given!");
|
||||
}
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
private static Cursor getCursorWithSelectedKeyringMasterKeyIds(Context context, Uri baseUri,
|
||||
long[] masterKeyIds) {
|
||||
Cursor cursor = null;
|
||||
if (masterKeyIds != null && masterKeyIds.length > 0) {
|
||||
|
||||
String inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
|
||||
for (int i = 0; i < masterKeyIds.length; ++i) {
|
||||
if (i != 0) {
|
||||
inMasterKeyList += ", ";
|
||||
}
|
||||
inMasterKeyList += DatabaseUtils.sqlEscapeString("" + masterKeyIds[i]);
|
||||
}
|
||||
inMasterKeyList += ")";
|
||||
|
||||
cursor = context.getContentResolver().query(baseUri,
|
||||
new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.KEY_RING_DATA },
|
||||
inMasterKeyList, null, null);
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public static ArrayList<String> getRegisteredApiApps(Context context) {
|
||||
Cursor cursor = context.getContentResolver().query(ApiApps.CONTENT_URI, null, null, null,
|
||||
null);
|
||||
|
||||
ArrayList<String> packageNames = new ArrayList<String>();
|
||||
if (cursor != null) {
|
||||
int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
packageNames.add(cursor.getString(packageNameCol));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return packageNames;
|
||||
}
|
||||
|
||||
private static ContentValues contentValueForApiApps(AppSettings appSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
|
||||
values.put(ApiApps.PACKAGE_SIGNATURE, appSettings.getPackageSignature());
|
||||
values.put(ApiApps.KEY_ID, appSettings.getKeyId());
|
||||
values.put(ApiApps.COMPRESSION, appSettings.getCompression());
|
||||
values.put(ApiApps.ENCRYPTION_ALGORITHM, appSettings.getEncryptionAlgorithm());
|
||||
values.put(ApiApps.HASH_ALORITHM, appSettings.getHashAlgorithm());
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static void insertApiApp(Context context, AppSettings appSettings) {
|
||||
context.getContentResolver().insert(ApiApps.CONTENT_URI,
|
||||
contentValueForApiApps(appSettings));
|
||||
}
|
||||
|
||||
public static void updateApiApp(Context context, AppSettings appSettings, Uri uri) {
|
||||
if (context.getContentResolver().update(uri, contentValueForApiApps(appSettings), null,
|
||||
null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
public static AppSettings getApiAppSettings(Context context, Uri uri) {
|
||||
AppSettings settings = null;
|
||||
|
||||
Cursor cur = context.getContentResolver().query(uri, null, null, null, null);
|
||||
if (cur != null && cur.moveToFirst()) {
|
||||
settings = new AppSettings();
|
||||
settings.setPackageName(cur.getString(cur
|
||||
.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME)));
|
||||
settings.setPackageSignature(cur.getBlob(cur
|
||||
.getColumnIndex(KeychainContract.ApiApps.PACKAGE_SIGNATURE)));
|
||||
settings.setKeyId(cur.getLong(cur.getColumnIndex(KeychainContract.ApiApps.KEY_ID)));
|
||||
settings.setCompression(cur.getInt(cur
|
||||
.getColumnIndexOrThrow(KeychainContract.ApiApps.COMPRESSION)));
|
||||
settings.setHashAlgorithm(cur.getInt(cur
|
||||
.getColumnIndexOrThrow(KeychainContract.ApiApps.HASH_ALORITHM)));
|
||||
settings.setEncryptionAlgorithm(cur.getInt(cur
|
||||
.getColumnIndexOrThrow(KeychainContract.ApiApps.ENCRYPTION_ALGORITHM)));
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static byte[] getApiAppSignature(Context context, String packageName) {
|
||||
Uri queryUri = KeychainContract.ApiApps.buildByPackageNameUri(packageName);
|
||||
|
||||
String[] projection = new String[] { ApiApps.PACKAGE_SIGNATURE };
|
||||
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
Cursor cursor = cr.query(queryUri, projection, null, null, null);
|
||||
|
||||
byte[] signature = null;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int signatureCol = 0;
|
||||
|
||||
signature = cursor.getBlob(signatureCol);
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.FileHelper;
|
||||
import org.sufficientlysecure.keychain.helper.OtherHelper;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpImportExport;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.DataStream;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
|
||||
import org.sufficientlysecure.keychain.util.HkpKeyServer;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
|
||||
/**
|
||||
* This Service contains all important long lasting operations for APG. It receives Intents with
|
||||
* data from the activities or other apps, queues these intents, executes them, and stops itself
|
||||
* after doing them.
|
||||
*/
|
||||
public class KeychainIntentService extends IntentService implements ProgressDialogUpdater {
|
||||
|
||||
/* extras that can be given by intent */
|
||||
public static final String EXTRA_MESSENGER = "messenger";
|
||||
public static final String EXTRA_DATA = "data";
|
||||
|
||||
/* possible actions */
|
||||
public static final String ACTION_ENCRYPT_SIGN = Constants.INTENT_PREFIX + "ENCRYPT_SIGN";
|
||||
|
||||
public static final String ACTION_DECRYPT_VERIFY = Constants.INTENT_PREFIX + "DECRYPT_VERIFY";
|
||||
|
||||
public static final String ACTION_SAVE_KEYRING = Constants.INTENT_PREFIX + "SAVE_KEYRING";
|
||||
public static final String ACTION_GENERATE_KEY = Constants.INTENT_PREFIX + "GENERATE_KEY";
|
||||
public static final String ACTION_GENERATE_DEFAULT_RSA_KEYS = Constants.INTENT_PREFIX
|
||||
+ "GENERATE_DEFAULT_RSA_KEYS";
|
||||
|
||||
public static final String ACTION_DELETE_FILE_SECURELY = Constants.INTENT_PREFIX
|
||||
+ "DELETE_FILE_SECURELY";
|
||||
|
||||
public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING";
|
||||
public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING";
|
||||
|
||||
public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING";
|
||||
public static final String ACTION_QUERY_KEYRING = Constants.INTENT_PREFIX + "QUERY_KEYRING";
|
||||
|
||||
public static final String ACTION_SIGN_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING";
|
||||
|
||||
/* keys for data bundle */
|
||||
|
||||
// encrypt, decrypt, import export
|
||||
public static final String TARGET = "target";
|
||||
// possible targets:
|
||||
public static final int TARGET_BYTES = 1;
|
||||
public static final int TARGET_FILE = 2;
|
||||
public static final int TARGET_STREAM = 3;
|
||||
|
||||
// encrypt
|
||||
public static final String ENCRYPT_SECRET_KEY_ID = "secret_key_id";
|
||||
public static final String ENCRYPT_USE_ASCII_ARMOR = "use_ascii_armor";
|
||||
public static final String ENCRYPT_ENCRYPTION_KEYS_IDS = "encryption_keys_ids";
|
||||
public static final String ENCRYPT_COMPRESSION_ID = "compression_id";
|
||||
public static final String ENCRYPT_GENERATE_SIGNATURE = "generate_signature";
|
||||
public static final String ENCRYPT_SIGN_ONLY = "sign_only";
|
||||
public static final String ENCRYPT_MESSAGE_BYTES = "message_bytes";
|
||||
public static final String ENCRYPT_INPUT_FILE = "input_file";
|
||||
public static final String ENCRYPT_OUTPUT_FILE = "output_file";
|
||||
public static final String ENCRYPT_PROVIDER_URI = "provider_uri";
|
||||
|
||||
// decrypt/verify
|
||||
public static final String DECRYPT_SIGNED_ONLY = "signed_only";
|
||||
public static final String DECRYPT_RETURN_BYTES = "return_binary";
|
||||
public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertext_bytes";
|
||||
public static final String DECRYPT_ASSUME_SYMMETRIC = "assume_symmetric";
|
||||
public static final String DECRYPT_LOOKUP_UNKNOWN_KEY = "lookup_unknownKey";
|
||||
|
||||
// save keyring
|
||||
public static final String SAVE_KEYRING_NEW_PASSPHRASE = "new_passphrase";
|
||||
public static final String SAVE_KEYRING_CURRENT_PASSPHRASE = "current_passphrase";
|
||||
public static final String SAVE_KEYRING_USER_IDS = "user_ids";
|
||||
public static final String SAVE_KEYRING_KEYS = "keys";
|
||||
public static final String SAVE_KEYRING_KEYS_USAGES = "keys_usages";
|
||||
public static final String SAVE_KEYRING_MASTER_KEY_ID = "master_key_id";
|
||||
public static final String SAVE_KEYRING_CAN_SIGN = "can_sign";
|
||||
|
||||
// generate key
|
||||
public static final String GENERATE_KEY_ALGORITHM = "algorithm";
|
||||
public static final String GENERATE_KEY_KEY_SIZE = "key_size";
|
||||
public static final String GENERATE_KEY_SYMMETRIC_PASSPHRASE = "passphrase";
|
||||
public static final String GENERATE_KEY_MASTER_KEY = "master_key";
|
||||
|
||||
// delete file securely
|
||||
public static final String DELETE_FILE = "deleteFile";
|
||||
|
||||
// import key
|
||||
public static final String IMPORT_INPUT_STREAM = "import_input_stream";
|
||||
public static final String IMPORT_FILENAME = "import_filename";
|
||||
public static final String IMPORT_BYTES = "import_bytes";
|
||||
public static final String IMPORT_KEY_LIST = "import_key_list";
|
||||
|
||||
// export key
|
||||
public static final String EXPORT_OUTPUT_STREAM = "export_output_stream";
|
||||
public static final String EXPORT_FILENAME = "export_filename";
|
||||
public static final String EXPORT_KEY_TYPE = "export_key_type";
|
||||
public static final String EXPORT_ALL = "export_all";
|
||||
public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id";
|
||||
|
||||
// upload key
|
||||
public static final String UPLOAD_KEY_SERVER = "upload_key_server";
|
||||
public static final String UPLOAD_KEY_KEYRING_ROW_ID = "upload_key_ring_id";
|
||||
|
||||
// query key
|
||||
public static final String QUERY_KEY_SERVER = "query_key_server";
|
||||
public static final String QUERY_KEY_TYPE = "query_key_type";
|
||||
public static final String QUERY_KEY_STRING = "query_key_string";
|
||||
public static final String QUERY_KEY_ID = "query_key_id";
|
||||
|
||||
// sign key
|
||||
public static final String SIGN_KEY_MASTER_KEY_ID = "sign_key_master_key_id";
|
||||
public static final String SIGN_KEY_PUB_KEY_ID = "sign_key_pub_key_id";
|
||||
|
||||
/*
|
||||
* possible data keys as result send over messenger
|
||||
*/
|
||||
// keys
|
||||
public static final String RESULT_NEW_KEY = "new_key";
|
||||
public static final String RESULT_NEW_KEY2 = "new_key2";
|
||||
|
||||
// encrypt
|
||||
public static final String RESULT_SIGNATURE_BYTES = "signature_data";
|
||||
public static final String RESULT_SIGNATURE_STRING = "signature_text";
|
||||
public static final String RESULT_ENCRYPTED_STRING = "encrypted_message";
|
||||
public static final String RESULT_ENCRYPTED_BYTES = "encrypted_data";
|
||||
public static final String RESULT_URI = "result_uri";
|
||||
|
||||
// decrypt/verify
|
||||
public static final String RESULT_DECRYPTED_STRING = "decrypted_message";
|
||||
public static final String RESULT_DECRYPTED_BYTES = "decrypted_data";
|
||||
public static final String RESULT_SIGNATURE = "signature";
|
||||
public static final String RESULT_SIGNATURE_KEY_ID = "signature_key_id";
|
||||
public static final String RESULT_SIGNATURE_USER_ID = "signature_user_id";
|
||||
|
||||
public static final String RESULT_SIGNATURE_SUCCESS = "signature_success";
|
||||
public static final String RESULT_SIGNATURE_UNKNOWN = "signature_unknown";
|
||||
public static final String RESULT_SIGNATURE_LOOKUP_KEY = "lookup_key";
|
||||
|
||||
// import
|
||||
public static final String RESULT_IMPORT_ADDED = "added";
|
||||
public static final String RESULT_IMPORT_UPDATED = "updated";
|
||||
public static final String RESULT_IMPORT_BAD = "bad";
|
||||
|
||||
// export
|
||||
public static final String RESULT_EXPORT = "exported";
|
||||
|
||||
// query
|
||||
public static final String RESULT_QUERY_KEY_DATA = "query_key_data";
|
||||
public static final String RESULT_QUERY_KEY_SEARCH_RESULT = "query_key_search_result";
|
||||
|
||||
Messenger mMessenger;
|
||||
|
||||
public KeychainIntentService() {
|
||||
super("ApgService");
|
||||
}
|
||||
|
||||
/**
|
||||
* The IntentService calls this method from the default worker thread with the intent that
|
||||
* started the service. When this method returns, IntentService stops the service, as
|
||||
* appropriate.
|
||||
*/
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
Bundle extras = intent.getExtras();
|
||||
if (extras == null) {
|
||||
Log.e(Constants.TAG, "Extras bundle is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(extras.containsKey(EXTRA_MESSENGER) || extras.containsKey(EXTRA_DATA) || (intent
|
||||
.getAction() == null))) {
|
||||
Log.e(Constants.TAG,
|
||||
"Extra bundle must contain a messenger, a data bundle, and an action!");
|
||||
return;
|
||||
}
|
||||
|
||||
mMessenger = (Messenger) extras.get(EXTRA_MESSENGER);
|
||||
Bundle data = extras.getBundle(EXTRA_DATA);
|
||||
|
||||
OtherHelper.logDebugBundle(data, "EXTRA_DATA");
|
||||
|
||||
String action = intent.getAction();
|
||||
|
||||
// execute action from extra bundle
|
||||
if (ACTION_ENCRYPT_SIGN.equals(action)) {
|
||||
try {
|
||||
/* Input */
|
||||
int target = data.getInt(TARGET);
|
||||
|
||||
long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
|
||||
String encryptionPassphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
|
||||
|
||||
boolean useAsciiArmor = data.getBoolean(ENCRYPT_USE_ASCII_ARMOR);
|
||||
long encryptionKeyIds[] = data.getLongArray(ENCRYPT_ENCRYPTION_KEYS_IDS);
|
||||
int compressionId = data.getInt(ENCRYPT_COMPRESSION_ID);
|
||||
boolean generateSignature = data.getBoolean(ENCRYPT_GENERATE_SIGNATURE);
|
||||
boolean signOnly = data.getBoolean(ENCRYPT_SIGN_ONLY);
|
||||
|
||||
InputStream inStream = null;
|
||||
long inLength = -1;
|
||||
InputData inputData = null;
|
||||
OutputStream outStream = null;
|
||||
String streamFilename = null;
|
||||
switch (target) {
|
||||
case TARGET_BYTES: /* encrypting bytes directly */
|
||||
byte[] bytes = data.getByteArray(ENCRYPT_MESSAGE_BYTES);
|
||||
|
||||
inStream = new ByteArrayInputStream(bytes);
|
||||
inLength = bytes.length;
|
||||
|
||||
inputData = new InputData(inStream, inLength);
|
||||
outStream = new ByteArrayOutputStream();
|
||||
|
||||
break;
|
||||
case TARGET_FILE: /* encrypting file */
|
||||
String inputFile = data.getString(ENCRYPT_INPUT_FILE);
|
||||
String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
|
||||
|
||||
// check if storage is ready
|
||||
if (!FileHelper.isStorageMounted(inputFile)
|
||||
|| !FileHelper.isStorageMounted(outputFile)) {
|
||||
throw new PgpGeneralException(
|
||||
getString(R.string.error_external_storage_not_ready));
|
||||
}
|
||||
|
||||
inStream = new FileInputStream(inputFile);
|
||||
File file = new File(inputFile);
|
||||
inLength = file.length();
|
||||
inputData = new InputData(inStream, inLength);
|
||||
|
||||
outStream = new FileOutputStream(outputFile);
|
||||
|
||||
break;
|
||||
|
||||
case TARGET_STREAM: /* Encrypting stream from content uri */
|
||||
Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
|
||||
|
||||
// InputStream
|
||||
InputStream in = getContentResolver().openInputStream(providerUri);
|
||||
inLength = PgpHelper.getLengthOfStream(in);
|
||||
inputData = new InputData(in, inLength);
|
||||
|
||||
// OutputStream
|
||||
try {
|
||||
while (true) {
|
||||
streamFilename = PgpHelper.generateRandomFilename(32);
|
||||
if (streamFilename == null) {
|
||||
throw new PgpGeneralException("couldn't generate random file name");
|
||||
}
|
||||
openFileInput(streamFilename).close();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// found a name that isn't used yet
|
||||
}
|
||||
outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new PgpGeneralException("No target choosen!");
|
||||
|
||||
}
|
||||
|
||||
/* Operation */
|
||||
PgpOperation operation = new PgpOperation(this, this, inputData, outStream);
|
||||
if (generateSignature) {
|
||||
Log.d(Constants.TAG, "generating signature...");
|
||||
operation.generateSignature(useAsciiArmor, false, secretKeyId,
|
||||
PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
|
||||
Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences
|
||||
.getPreferences(this).getForceV3Signatures());
|
||||
} else if (signOnly) {
|
||||
Log.d(Constants.TAG, "sign only...");
|
||||
operation.signText(secretKeyId, PassphraseCacheService.getCachedPassphrase(
|
||||
this, secretKeyId), Preferences.getPreferences(this)
|
||||
.getDefaultHashAlgorithm(), Preferences.getPreferences(this)
|
||||
.getForceV3Signatures());
|
||||
} else {
|
||||
Log.d(Constants.TAG, "encrypt...");
|
||||
operation.signAndEncrypt(useAsciiArmor, compressionId, encryptionKeyIds,
|
||||
encryptionPassphrase, Preferences.getPreferences(this)
|
||||
.getDefaultEncryptionAlgorithm(), secretKeyId, Preferences
|
||||
.getPreferences(this).getDefaultHashAlgorithm(), Preferences
|
||||
.getPreferences(this).getForceV3Signatures(),
|
||||
PassphraseCacheService.getCachedPassphrase(this, secretKeyId));
|
||||
}
|
||||
|
||||
outStream.close();
|
||||
|
||||
/* Output */
|
||||
|
||||
Bundle resultData = new Bundle();
|
||||
|
||||
switch (target) {
|
||||
case TARGET_BYTES:
|
||||
if (useAsciiArmor) {
|
||||
String output = new String(
|
||||
((ByteArrayOutputStream) outStream).toByteArray());
|
||||
if (generateSignature) {
|
||||
resultData.putString(RESULT_SIGNATURE_STRING, output);
|
||||
} else {
|
||||
resultData.putString(RESULT_ENCRYPTED_STRING, output);
|
||||
}
|
||||
} else {
|
||||
byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
|
||||
if (generateSignature) {
|
||||
resultData.putByteArray(RESULT_SIGNATURE_BYTES, output);
|
||||
} else {
|
||||
resultData.putByteArray(RESULT_ENCRYPTED_BYTES, output);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case TARGET_FILE:
|
||||
// nothing, file was written, just send okay
|
||||
|
||||
break;
|
||||
case TARGET_STREAM:
|
||||
String uri = DataStream.buildDataStreamUri(streamFilename).toString();
|
||||
resultData.putString(RESULT_URI, uri);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
OtherHelper.logDebugBundle(resultData, "resultData");
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_DECRYPT_VERIFY.equals(action)) {
|
||||
try {
|
||||
/* Input */
|
||||
int target = data.getInt(TARGET);
|
||||
|
||||
long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
|
||||
byte[] bytes = data.getByteArray(DECRYPT_CIPHERTEXT_BYTES);
|
||||
boolean signedOnly = data.getBoolean(DECRYPT_SIGNED_ONLY);
|
||||
boolean returnBytes = data.getBoolean(DECRYPT_RETURN_BYTES);
|
||||
boolean assumeSymmetricEncryption = data.getBoolean(DECRYPT_ASSUME_SYMMETRIC);
|
||||
|
||||
boolean lookupUnknownKey = data.getBoolean(DECRYPT_LOOKUP_UNKNOWN_KEY);
|
||||
|
||||
InputStream inStream = null;
|
||||
long inLength = -1;
|
||||
InputData inputData = null;
|
||||
OutputStream outStream = null;
|
||||
String streamFilename = null;
|
||||
switch (target) {
|
||||
case TARGET_BYTES: /* decrypting bytes directly */
|
||||
inStream = new ByteArrayInputStream(bytes);
|
||||
inLength = bytes.length;
|
||||
|
||||
inputData = new InputData(inStream, inLength);
|
||||
outStream = new ByteArrayOutputStream();
|
||||
|
||||
break;
|
||||
|
||||
case TARGET_FILE: /* decrypting file */
|
||||
String inputFile = data.getString(ENCRYPT_INPUT_FILE);
|
||||
String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
|
||||
|
||||
// check if storage is ready
|
||||
if (!FileHelper.isStorageMounted(inputFile)
|
||||
|| !FileHelper.isStorageMounted(outputFile)) {
|
||||
throw new PgpGeneralException(
|
||||
getString(R.string.error_external_storage_not_ready));
|
||||
}
|
||||
|
||||
// InputStream
|
||||
inLength = -1;
|
||||
inStream = new FileInputStream(inputFile);
|
||||
File file = new File(inputFile);
|
||||
inLength = file.length();
|
||||
inputData = new InputData(inStream, inLength);
|
||||
|
||||
// OutputStream
|
||||
outStream = new FileOutputStream(outputFile);
|
||||
|
||||
break;
|
||||
|
||||
case TARGET_STREAM: /* decrypting stream from content uri */
|
||||
Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
|
||||
|
||||
// InputStream
|
||||
InputStream in = getContentResolver().openInputStream(providerUri);
|
||||
inLength = PgpHelper.getLengthOfStream(in);
|
||||
inputData = new InputData(in, inLength);
|
||||
|
||||
// OutputStream
|
||||
try {
|
||||
while (true) {
|
||||
streamFilename = PgpHelper.generateRandomFilename(32);
|
||||
if (streamFilename == null) {
|
||||
throw new PgpGeneralException("couldn't generate random file name");
|
||||
}
|
||||
openFileInput(streamFilename).close();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// found a name that isn't used yet
|
||||
}
|
||||
outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new PgpGeneralException("No target choosen!");
|
||||
|
||||
}
|
||||
|
||||
/* Operation */
|
||||
|
||||
Bundle resultData = new Bundle();
|
||||
|
||||
// verifyText and decrypt returning additional resultData values for the
|
||||
// verification of signatures
|
||||
PgpOperation operation = new PgpOperation(this, this, inputData, outStream);
|
||||
if (signedOnly) {
|
||||
resultData = operation.verifyText(lookupUnknownKey);
|
||||
} else {
|
||||
resultData = operation.decryptAndVerify(
|
||||
PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
|
||||
assumeSymmetricEncryption);
|
||||
}
|
||||
|
||||
outStream.close();
|
||||
|
||||
/* Output */
|
||||
|
||||
switch (target) {
|
||||
case TARGET_BYTES:
|
||||
if (returnBytes) {
|
||||
byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
|
||||
resultData.putByteArray(RESULT_DECRYPTED_BYTES, output);
|
||||
} else {
|
||||
String output = new String(
|
||||
((ByteArrayOutputStream) outStream).toByteArray());
|
||||
resultData.putString(RESULT_DECRYPTED_STRING, output);
|
||||
}
|
||||
|
||||
break;
|
||||
case TARGET_FILE:
|
||||
// nothing, file was written, just send okay and verification bundle
|
||||
|
||||
break;
|
||||
case TARGET_STREAM:
|
||||
String uri = DataStream.buildDataStreamUri(streamFilename).toString();
|
||||
resultData.putString(RESULT_URI, uri);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
OtherHelper.logDebugBundle(resultData, "resultData");
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_SAVE_KEYRING.equals(action)) {
|
||||
try {
|
||||
/* Input */
|
||||
String oldPassPhrase = data.getString(SAVE_KEYRING_CURRENT_PASSPHRASE);
|
||||
String newPassPhrase = data.getString(SAVE_KEYRING_NEW_PASSPHRASE);
|
||||
boolean canSign = true;
|
||||
|
||||
if (data.containsKey(SAVE_KEYRING_CAN_SIGN)) {
|
||||
canSign = data.getBoolean(SAVE_KEYRING_CAN_SIGN);
|
||||
}
|
||||
|
||||
if (newPassPhrase == null) {
|
||||
newPassPhrase = oldPassPhrase;
|
||||
}
|
||||
ArrayList<String> userIds = data.getStringArrayList(SAVE_KEYRING_USER_IDS);
|
||||
ArrayList<PGPSecretKey> keys = PgpConversionHelper.BytesToPGPSecretKeyList(data
|
||||
.getByteArray(SAVE_KEYRING_KEYS));
|
||||
ArrayList<Integer> keysUsages = data.getIntegerArrayList(SAVE_KEYRING_KEYS_USAGES);
|
||||
long masterKeyId = data.getLong(SAVE_KEYRING_MASTER_KEY_ID);
|
||||
|
||||
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
|
||||
/* Operation */
|
||||
if (!canSign) {
|
||||
keyOperations.changeSecretKeyPassphrase(
|
||||
ProviderHelper.getPGPSecretKeyRingByKeyId(this, masterKeyId),
|
||||
oldPassPhrase, newPassPhrase);
|
||||
} else {
|
||||
keyOperations.buildSecretKey(userIds, keys, keysUsages, masterKeyId,
|
||||
oldPassPhrase, newPassPhrase);
|
||||
}
|
||||
PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase);
|
||||
|
||||
/* Output */
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_GENERATE_KEY.equals(action)) {
|
||||
try {
|
||||
/* Input */
|
||||
int algorithm = data.getInt(GENERATE_KEY_ALGORITHM);
|
||||
String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
|
||||
int keysize = data.getInt(GENERATE_KEY_KEY_SIZE);
|
||||
PGPSecretKey masterKey = null;
|
||||
if (data.containsKey(GENERATE_KEY_MASTER_KEY)) {
|
||||
masterKey = PgpConversionHelper.BytesToPGPSecretKey(data
|
||||
.getByteArray(GENERATE_KEY_MASTER_KEY));
|
||||
}
|
||||
|
||||
/* Operation */
|
||||
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
|
||||
PGPSecretKeyRing newKeyRing = keyOperations.createKey(algorithm, keysize,
|
||||
passphrase, masterKey);
|
||||
|
||||
/* Output */
|
||||
Bundle resultData = new Bundle();
|
||||
resultData.putByteArray(RESULT_NEW_KEY,
|
||||
PgpConversionHelper.PGPSecretKeyRingToBytes(newKeyRing));
|
||||
|
||||
OtherHelper.logDebugBundle(resultData, "resultData");
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_GENERATE_DEFAULT_RSA_KEYS.equals(action)) {
|
||||
// generate one RSA 4096 key for signing and one subkey for encrypting!
|
||||
try {
|
||||
/* Input */
|
||||
String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
|
||||
|
||||
/* Operation */
|
||||
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
|
||||
|
||||
PGPSecretKeyRing masterKeyRing = keyOperations.createKey(Id.choice.algorithm.rsa,
|
||||
4096, passphrase, null);
|
||||
|
||||
PGPSecretKeyRing subKeyRing = keyOperations.createKey(Id.choice.algorithm.rsa,
|
||||
4096, passphrase, masterKeyRing.getSecretKey());
|
||||
|
||||
/* Output */
|
||||
Bundle resultData = new Bundle();
|
||||
resultData.putByteArray(RESULT_NEW_KEY,
|
||||
PgpConversionHelper.PGPSecretKeyRingToBytes(masterKeyRing));
|
||||
resultData.putByteArray(RESULT_NEW_KEY2,
|
||||
PgpConversionHelper.PGPSecretKeyRingToBytes(subKeyRing));
|
||||
|
||||
OtherHelper.logDebugBundle(resultData, "resultData");
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_DELETE_FILE_SECURELY.equals(action)) {
|
||||
try {
|
||||
/* Input */
|
||||
String deleteFile = data.getString(DELETE_FILE);
|
||||
|
||||
/* Operation */
|
||||
try {
|
||||
PgpHelper.deleteFileSecurely(this, this, new File(deleteFile));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new PgpGeneralException(
|
||||
getString(R.string.error_file_not_found, deleteFile));
|
||||
} catch (IOException e) {
|
||||
throw new PgpGeneralException(getString(R.string.error_file_delete_failed,
|
||||
deleteFile));
|
||||
}
|
||||
|
||||
/* Output */
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_IMPORT_KEYRING.equals(action)) {
|
||||
try {
|
||||
|
||||
/* Input */
|
||||
int target = data.getInt(TARGET);
|
||||
|
||||
/* Operation */
|
||||
InputStream inStream = null;
|
||||
long inLength = -1;
|
||||
InputData inputData = null;
|
||||
switch (target) {
|
||||
case TARGET_BYTES: /* import key from bytes directly */
|
||||
byte[] bytes = data.getByteArray(IMPORT_BYTES);
|
||||
|
||||
inStream = new ByteArrayInputStream(bytes);
|
||||
inLength = bytes.length;
|
||||
|
||||
inputData = new InputData(inStream, inLength);
|
||||
|
||||
break;
|
||||
case TARGET_FILE: /* import key from file */
|
||||
String inputFile = data.getString(IMPORT_FILENAME);
|
||||
|
||||
inStream = new FileInputStream(inputFile);
|
||||
File file = new File(inputFile);
|
||||
inLength = file.length();
|
||||
inputData = new InputData(inStream, inLength);
|
||||
|
||||
break;
|
||||
|
||||
case TARGET_STREAM:
|
||||
// TODO: not implemented
|
||||
break;
|
||||
}
|
||||
|
||||
Bundle resultData = new Bundle();
|
||||
|
||||
ArrayList<Long> keyIds = (ArrayList<Long>) data.getSerializable(IMPORT_KEY_LIST);
|
||||
|
||||
PgpImportExport pgpImportExport = new PgpImportExport(this, this);
|
||||
resultData = pgpImportExport.importKeyRings(inputData, keyIds);
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_EXPORT_KEYRING.equals(action)) {
|
||||
try {
|
||||
|
||||
/* Input */
|
||||
int keyType = Id.type.public_key;
|
||||
if (data.containsKey(EXPORT_KEY_TYPE)) {
|
||||
keyType = data.getInt(EXPORT_KEY_TYPE);
|
||||
}
|
||||
|
||||
String outputFile = data.getString(EXPORT_FILENAME);
|
||||
|
||||
boolean exportAll = data.getBoolean(EXPORT_ALL);
|
||||
long keyRingMasterKeyId = -1;
|
||||
if (!exportAll) {
|
||||
keyRingMasterKeyId = data.getLong(EXPORT_KEY_RING_MASTER_KEY_ID);
|
||||
}
|
||||
|
||||
/* Operation */
|
||||
|
||||
// check if storage is ready
|
||||
if (!FileHelper.isStorageMounted(outputFile)) {
|
||||
throw new PgpGeneralException(getString(R.string.error_external_storage_not_ready));
|
||||
}
|
||||
|
||||
// OutputStream
|
||||
FileOutputStream outStream = new FileOutputStream(outputFile);
|
||||
|
||||
ArrayList<Long> keyRingMasterKeyIds = new ArrayList<Long>();
|
||||
if (exportAll) {
|
||||
// get all key ring row ids based on export type
|
||||
|
||||
if (keyType == Id.type.public_key) {
|
||||
keyRingMasterKeyIds = ProviderHelper.getPublicKeyRingsMasterKeyIds(this);
|
||||
} else {
|
||||
keyRingMasterKeyIds = ProviderHelper.getSecretKeyRingsMasterKeyIds(this);
|
||||
}
|
||||
} else {
|
||||
keyRingMasterKeyIds.add(keyRingMasterKeyId);
|
||||
}
|
||||
|
||||
Bundle resultData = new Bundle();
|
||||
|
||||
PgpImportExport pgpImportExport = new PgpImportExport(this, this);
|
||||
resultData = pgpImportExport
|
||||
.exportKeyRings(keyRingMasterKeyIds, keyType, outStream);
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_UPLOAD_KEYRING.equals(action)) {
|
||||
try {
|
||||
|
||||
/* Input */
|
||||
int keyRingRowId = data.getInt(UPLOAD_KEY_KEYRING_ROW_ID);
|
||||
String keyServer = data.getString(UPLOAD_KEY_SERVER);
|
||||
|
||||
/* Operation */
|
||||
HkpKeyServer server = new HkpKeyServer(keyServer);
|
||||
|
||||
PGPPublicKeyRing keyring = ProviderHelper.getPGPPublicKeyRingByRowId(this,
|
||||
keyRingRowId);
|
||||
if (keyring != null) {
|
||||
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
|
||||
|
||||
boolean uploaded = pgpImportExport.uploadKeyRingToServer(server,
|
||||
(PGPPublicKeyRing) keyring);
|
||||
if (!uploaded) {
|
||||
throw new PgpGeneralException("Unable to export key to selected server");
|
||||
}
|
||||
}
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_QUERY_KEYRING.equals(action)) {
|
||||
try {
|
||||
|
||||
/* Input */
|
||||
int queryType = data.getInt(QUERY_KEY_TYPE);
|
||||
String keyServer = data.getString(QUERY_KEY_SERVER);
|
||||
|
||||
String queryString = data.getString(QUERY_KEY_STRING);
|
||||
long keyId = data.getLong(QUERY_KEY_ID);
|
||||
|
||||
/* Operation */
|
||||
Bundle resultData = new Bundle();
|
||||
|
||||
HkpKeyServer server = new HkpKeyServer(keyServer);
|
||||
if (queryType == Id.keyserver.search) {
|
||||
ArrayList<KeyInfo> searchResult = server.search(queryString);
|
||||
|
||||
resultData.putParcelableArrayList(RESULT_QUERY_KEY_SEARCH_RESULT, searchResult);
|
||||
} else if (queryType == Id.keyserver.get) {
|
||||
String keyData = server.get(keyId);
|
||||
|
||||
resultData.putString(RESULT_QUERY_KEY_DATA, keyData);
|
||||
}
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
} else if (ACTION_SIGN_KEYRING.equals(action)) {
|
||||
try {
|
||||
|
||||
/* Input */
|
||||
long masterKeyId = data.getLong(SIGN_KEY_MASTER_KEY_ID);
|
||||
long pubKeyId = data.getLong(SIGN_KEY_PUB_KEY_ID);
|
||||
|
||||
/* Operation */
|
||||
String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this,
|
||||
masterKeyId);
|
||||
|
||||
PgpKeyOperation keyOperation = new PgpKeyOperation(this, this);
|
||||
PGPPublicKeyRing signedPubKeyRing = keyOperation.signKey(masterKeyId, pubKeyId,
|
||||
signaturePassPhrase);
|
||||
|
||||
// store the signed key in our local cache
|
||||
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
|
||||
int retval = pgpImportExport.storeKeyRingInCache(signedPubKeyRing);
|
||||
if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
|
||||
throw new PgpGeneralException("Failed to store signed key in local cache");
|
||||
}
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorToHandler(Exception e) {
|
||||
Log.e(Constants.TAG, "ApgService Exception: ", e);
|
||||
e.printStackTrace();
|
||||
|
||||
Bundle data = new Bundle();
|
||||
data.putString(KeychainIntentServiceHandler.DATA_ERROR, e.getMessage());
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_EXCEPTION, null, data);
|
||||
}
|
||||
|
||||
private void sendMessageToHandler(Integer arg1, Integer arg2, Bundle data) {
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = arg1;
|
||||
if (arg2 != null) {
|
||||
msg.arg2 = arg2;
|
||||
}
|
||||
if (data != null) {
|
||||
msg.setData(data);
|
||||
}
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessageToHandler(Integer arg1, Bundle data) {
|
||||
sendMessageToHandler(arg1, null, data);
|
||||
}
|
||||
|
||||
private void sendMessageToHandler(Integer arg1) {
|
||||
sendMessageToHandler(arg1, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set progress of ProgressDialog by sending message to handler on UI thread
|
||||
*/
|
||||
public void setProgress(String message, int progress, int max) {
|
||||
Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max="
|
||||
+ max);
|
||||
|
||||
Bundle data = new Bundle();
|
||||
if (message != null) {
|
||||
data.putString(KeychainIntentServiceHandler.DATA_MESSAGE, message);
|
||||
}
|
||||
data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS, progress);
|
||||
data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS_MAX, max);
|
||||
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_UPDATE_PROGRESS, null, data);
|
||||
}
|
||||
|
||||
public void setProgress(int resourceId, int progress, int max) {
|
||||
setProgress(getString(resourceId), progress, max);
|
||||
}
|
||||
|
||||
public void setProgress(int progress, int max) {
|
||||
setProgress(null, progress, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.service;
|
||||
|
||||
import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class KeychainIntentServiceHandler extends Handler {
|
||||
|
||||
// possible messages send from this service to handler on ui
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
public static final int MESSAGE_EXCEPTION = 2;
|
||||
public static final int MESSAGE_UPDATE_PROGRESS = 3;
|
||||
|
||||
// possible data keys for messages
|
||||
public static final String DATA_ERROR = "error";
|
||||
public static final String DATA_PROGRESS = "progress";
|
||||
public static final String DATA_PROGRESS_MAX = "max";
|
||||
public static final String DATA_MESSAGE = "message";
|
||||
public static final String DATA_MESSAGE_ID = "message_id";
|
||||
|
||||
Activity mActivity;
|
||||
ProgressDialogFragment mProgressDialogFragment;
|
||||
|
||||
public KeychainIntentServiceHandler(Activity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
public KeychainIntentServiceHandler(Activity activity, ProgressDialogFragment progressDialogFragment) {
|
||||
this.mActivity = activity;
|
||||
this.mProgressDialogFragment = progressDialogFragment;
|
||||
}
|
||||
|
||||
public KeychainIntentServiceHandler(Activity activity, int progressDialogMessageId, int progressDialogStyle) {
|
||||
this.mActivity = activity;
|
||||
this.mProgressDialogFragment = ProgressDialogFragment.newInstance(progressDialogMessageId,
|
||||
progressDialogStyle);
|
||||
}
|
||||
|
||||
public void showProgressDialog(FragmentActivity activity) {
|
||||
mProgressDialogFragment.show(activity.getSupportFragmentManager(), "progressDialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
Bundle data = message.getData();
|
||||
|
||||
switch (message.arg1) {
|
||||
case MESSAGE_OKAY:
|
||||
mProgressDialogFragment.dismiss();
|
||||
|
||||
break;
|
||||
|
||||
case MESSAGE_EXCEPTION:
|
||||
mProgressDialogFragment.dismiss();
|
||||
|
||||
// show error from service
|
||||
if (data.containsKey(DATA_ERROR)) {
|
||||
Toast.makeText(mActivity,
|
||||
mActivity.getString(R.string.error_message, data.getString(DATA_ERROR)),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case MESSAGE_UPDATE_PROGRESS:
|
||||
if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) {
|
||||
|
||||
// update progress from service
|
||||
if (data.containsKey(DATA_MESSAGE)) {
|
||||
mProgressDialogFragment.setProgress(data.getString(DATA_MESSAGE),
|
||||
data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
|
||||
} else if (data.containsKey(DATA_MESSAGE_ID)) {
|
||||
mProgressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID),
|
||||
data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
|
||||
} else {
|
||||
mProgressDialogFragment.setProgress(data.getInt(DATA_PROGRESS),
|
||||
data.getInt(DATA_PROGRESS_MAX));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.spongycastle.openpgp.PGPException;
|
||||
import org.spongycastle.openpgp.PGPPrivateKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This service runs in its own process, but is available to all other processes as the main
|
||||
* passphrase cache. Use the static methods addCachedPassphrase and getCachedPassphrase for
|
||||
* convenience.
|
||||
*
|
||||
*/
|
||||
public class PassphraseCacheService extends Service {
|
||||
public static final String TAG = Constants.TAG + ": PassphraseCacheService";
|
||||
|
||||
public static final String ACTION_PASSPHRASE_CACHE_ADD = Constants.INTENT_PREFIX
|
||||
+ "PASSPHRASE_CACHE_ADD";
|
||||
public static final String ACTION_PASSPHRASE_CACHE_GET = Constants.INTENT_PREFIX
|
||||
+ "PASSPHRASE_CACHE_GET";
|
||||
|
||||
public static final String BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE = Constants.INTENT_PREFIX
|
||||
+ "PASSPHRASE_CACHE_BROADCAST";
|
||||
|
||||
public static final String EXTRA_TTL = "ttl";
|
||||
public static final String EXTRA_KEY_ID = "key_id";
|
||||
public static final String EXTRA_PASSPHRASE = "passphrase";
|
||||
public static final String EXTRA_MESSENGER = "messenger";
|
||||
|
||||
private static final int REQUEST_ID = 0;
|
||||
private static final long DEFAULT_TTL = 15;
|
||||
|
||||
private BroadcastReceiver mIntentReceiver;
|
||||
|
||||
private HashMap<Long, String> mPassphraseCache = new HashMap<Long, String>();
|
||||
|
||||
Context mContext;
|
||||
|
||||
/**
|
||||
* This caches a new passphrase in memory by sending a new command to the service. An android
|
||||
* service is only run once. Thus, when the service is already started, new commands just add
|
||||
* new events to the alarm manager for new passphrases to let them timeout in the future.
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @param passphrase
|
||||
*/
|
||||
public static void addCachedPassphrase(Context context, long keyId, String passphrase) {
|
||||
Log.d(TAG, "cacheNewPassphrase() for " + keyId);
|
||||
|
||||
Intent intent = new Intent(context, PassphraseCacheService.class);
|
||||
intent.setAction(ACTION_PASSPHRASE_CACHE_ADD);
|
||||
intent.putExtra(EXTRA_TTL, Preferences.getPreferences(context).getPassPhraseCacheTtl());
|
||||
intent.putExtra(EXTRA_PASSPHRASE, passphrase);
|
||||
intent.putExtra(EXTRA_KEY_ID, keyId);
|
||||
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cached passphrase from memory by sending an intent to the service. This method is
|
||||
* designed to wait until the service returns the passphrase.
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @return passphrase or null (if no passphrase is cached for this keyId)
|
||||
*/
|
||||
public static String getCachedPassphrase(Context context, long keyId) {
|
||||
Log.d(TAG, "getCachedPassphrase() get masterKeyId for " + keyId);
|
||||
|
||||
Intent intent = new Intent(context, PassphraseCacheService.class);
|
||||
intent.setAction(ACTION_PASSPHRASE_CACHE_GET);
|
||||
|
||||
final Object mutex = new Object();
|
||||
final Bundle returnBundle = new Bundle();
|
||||
|
||||
HandlerThread handlerThread = new HandlerThread("getPassphraseThread");
|
||||
handlerThread.start();
|
||||
Handler returnHandler = new Handler(handlerThread.getLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.obj != null) {
|
||||
String passphrase = ((Bundle) message.obj).getString(EXTRA_PASSPHRASE);
|
||||
returnBundle.putString(EXTRA_PASSPHRASE, passphrase);
|
||||
}
|
||||
synchronized (mutex) {
|
||||
mutex.notify();
|
||||
}
|
||||
// quit handlerThread
|
||||
getLooper().quit();
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
intent.putExtra(EXTRA_KEY_ID, keyId);
|
||||
intent.putExtra(EXTRA_MESSENGER, messenger);
|
||||
// send intent to this service
|
||||
context.startService(intent);
|
||||
|
||||
// Wait on mutex until passphrase is returned to handlerThread
|
||||
synchronized (mutex) {
|
||||
try {
|
||||
mutex.wait(3000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (returnBundle.containsKey(EXTRA_PASSPHRASE)) {
|
||||
return returnBundle.getString(EXTRA_PASSPHRASE);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get cached passphrase.
|
||||
*
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
private String getCachedPassphraseImpl(long keyId) {
|
||||
Log.d(TAG, "getCachedPassphraseImpl() get masterKeyId for " + keyId);
|
||||
|
||||
// try to get master key id which is used as an identifier for cached passphrases
|
||||
long masterKeyId = keyId;
|
||||
if (masterKeyId != Id.key.symmetric) {
|
||||
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(this, keyId);
|
||||
if (keyRing == null) {
|
||||
return null;
|
||||
}
|
||||
PGPSecretKey masterKey = PgpKeyHelper.getMasterKey(keyRing);
|
||||
if (masterKey == null) {
|
||||
return null;
|
||||
}
|
||||
masterKeyId = masterKey.getKeyID();
|
||||
}
|
||||
Log.d(TAG, "getCachedPassphraseImpl() for masterKeyId " + masterKeyId);
|
||||
|
||||
// get cached passphrase
|
||||
String cachedPassphrase = mPassphraseCache.get(masterKeyId);
|
||||
if (cachedPassphrase == null) {
|
||||
// if key has no passphrase -> cache and return empty passphrase
|
||||
if (!hasPassphrase(this, masterKeyId)) {
|
||||
Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!");
|
||||
|
||||
addCachedPassphrase(this, masterKeyId, "");
|
||||
return "";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// set it again to reset the cache life cycle
|
||||
Log.d(TAG, "Cache passphrase again when getting it!");
|
||||
addCachedPassphrase(this, masterKeyId, cachedPassphrase);
|
||||
|
||||
return cachedPassphrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if key has a passphrase.
|
||||
*
|
||||
* @param secretKeyId
|
||||
* @return true if it has a passphrase
|
||||
*/
|
||||
public static boolean hasPassphrase(Context context, long secretKeyId) {
|
||||
// check if the key has no passphrase
|
||||
try {
|
||||
PGPSecretKey secretKey = PgpKeyHelper.getMasterKey(ProviderHelper
|
||||
.getPGPSecretKeyRingByKeyId(context, secretKeyId));
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
|
||||
"SC").build("".toCharArray());
|
||||
PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
|
||||
if (testKey != null) {
|
||||
return false;
|
||||
}
|
||||
} catch (PGPException e) {
|
||||
// silently catch
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register BroadcastReceiver that is unregistered when service is destroyed. This
|
||||
* BroadcastReceiver hears on intents with ACTION_PASSPHRASE_CACHE_SERVICE to then timeout
|
||||
* specific passphrases in memory.
|
||||
*/
|
||||
private void registerReceiver() {
|
||||
if (mIntentReceiver == null) {
|
||||
mIntentReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
|
||||
Log.d(TAG, "Received broadcast...");
|
||||
|
||||
if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) {
|
||||
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
|
||||
timeout(context, keyId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
|
||||
registerReceiver(mIntentReceiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build pending intent that is executed by alarm manager to time out a specific passphrase
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
* @return
|
||||
*/
|
||||
private static PendingIntent buildIntent(Context context, long keyId) {
|
||||
Intent intent = new Intent(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
|
||||
intent.putExtra(EXTRA_KEY_ID, keyId);
|
||||
PendingIntent sender = PendingIntent.getBroadcast(context, REQUEST_ID, intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
return sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed when service is started by intent
|
||||
*/
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.d(TAG, "onStartCommand()");
|
||||
|
||||
// register broadcastreceiver
|
||||
registerReceiver();
|
||||
|
||||
if (intent != null && intent.getAction() != null) {
|
||||
if (ACTION_PASSPHRASE_CACHE_ADD.equals(intent.getAction())) {
|
||||
long ttl = intent.getLongExtra(EXTRA_TTL, DEFAULT_TTL);
|
||||
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
|
||||
String passphrase = intent.getStringExtra(EXTRA_PASSPHRASE);
|
||||
|
||||
Log.d(TAG,
|
||||
"Received ACTION_PASSPHRASE_CACHE_ADD intent in onStartCommand() with keyId: "
|
||||
+ keyId + ", ttl: " + ttl);
|
||||
|
||||
// add keyId and passphrase to memory
|
||||
mPassphraseCache.put(keyId, passphrase);
|
||||
|
||||
if (ttl > 0) {
|
||||
// register new alarm with keyId for this passphrase
|
||||
long triggerTime = new Date().getTime() + (ttl * 1000);
|
||||
AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
|
||||
am.set(AlarmManager.RTC_WAKEUP, triggerTime, buildIntent(this, keyId));
|
||||
}
|
||||
} else if (ACTION_PASSPHRASE_CACHE_GET.equals(intent.getAction())) {
|
||||
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
|
||||
Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER);
|
||||
|
||||
String passphrase = getCachedPassphraseImpl(keyId);
|
||||
|
||||
Message msg = Message.obtain();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(EXTRA_PASSPHRASE, passphrase);
|
||||
msg.obj = bundle;
|
||||
try {
|
||||
messenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "Sending message failed", e);
|
||||
}
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Intent or Intent Action not supported!");
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one specific passphrase for keyId timed out
|
||||
*
|
||||
* @param context
|
||||
* @param keyId
|
||||
*/
|
||||
private void timeout(Context context, long keyId) {
|
||||
// remove passphrase corresponding to keyId from memory
|
||||
mPassphraseCache.remove(keyId);
|
||||
|
||||
Log.d(TAG, "Timeout of keyId " + keyId + ", removed from memory!");
|
||||
|
||||
// stop whole service if no cached passphrases remaining
|
||||
if (mPassphraseCache.isEmpty()) {
|
||||
Log.d(TAG, "No passphrases remaining in memory, stopping service!");
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mContext = this;
|
||||
Log.d(Constants.TAG, "PassphraseCacheService, onCreate()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.d(Constants.TAG, "PassphraseCacheService, onDestroy()");
|
||||
|
||||
unregisterReceiver(mIntentReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
public class PassphraseCacheBinder extends Binder {
|
||||
public PassphraseCacheService getService() {
|
||||
return PassphraseCacheService.this;
|
||||
}
|
||||
}
|
||||
|
||||
private final IBinder mBinder = new PassphraseCacheBinder();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.sufficientlysecure.keychain.service.exception;
|
||||
|
||||
public class NoUserIdsException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 7009311527126696207L;
|
||||
|
||||
public NoUserIdsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.sufficientlysecure.keychain.service.exception;
|
||||
|
||||
public class UserInteractionRequiredException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -60128148603511936L;
|
||||
|
||||
public UserInteractionRequiredException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.sufficientlysecure.keychain.service.exception;
|
||||
|
||||
public class WrongPackageSignatureException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -8294642703122196028L;
|
||||
|
||||
public WrongPackageSignatureException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.sufficientlysecure.keychain.service.exception;
|
||||
|
||||
public class WrongPassphraseException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5309689232853485740L;
|
||||
|
||||
public WrongPassphraseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import org.spongycastle.bcpg.HashAlgorithmTags;
|
||||
import org.spongycastle.openpgp.PGPEncryptedData;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
|
||||
public class AppSettings {
|
||||
private String packageName;
|
||||
private byte[] packageSignature;
|
||||
private long keyId = Id.key.none;
|
||||
private int encryptionAlgorithm;
|
||||
private int hashAlgorithm;
|
||||
private int compression;
|
||||
|
||||
public AppSettings() {
|
||||
|
||||
}
|
||||
|
||||
public AppSettings(String packageName, byte[] packageSignature) {
|
||||
super();
|
||||
this.packageName = packageName;
|
||||
this.packageSignature = packageSignature;
|
||||
// defaults:
|
||||
this.encryptionAlgorithm = PGPEncryptedData.AES_256;
|
||||
this.hashAlgorithm = HashAlgorithmTags.SHA512;
|
||||
this.compression = Id.choice.compression.zlib;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public byte[] getPackageSignature() {
|
||||
return packageSignature;
|
||||
}
|
||||
|
||||
public void setPackageSignature(byte[] packageSignature) {
|
||||
this.packageSignature = packageSignature;
|
||||
}
|
||||
|
||||
public long getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public void setKeyId(long scretKeyId) {
|
||||
this.keyId = scretKeyId;
|
||||
}
|
||||
|
||||
public int getEncryptionAlgorithm() {
|
||||
return encryptionAlgorithm;
|
||||
}
|
||||
|
||||
public void setEncryptionAlgorithm(int encryptionAlgorithm) {
|
||||
this.encryptionAlgorithm = encryptionAlgorithm;
|
||||
}
|
||||
|
||||
public int getHashAlgorithm() {
|
||||
return hashAlgorithm;
|
||||
}
|
||||
|
||||
public void setHashAlgorithm(int hashAlgorithm) {
|
||||
this.hashAlgorithm = hashAlgorithm;
|
||||
}
|
||||
|
||||
public int getCompression() {
|
||||
return compression;
|
||||
}
|
||||
|
||||
public void setCompression(int compression) {
|
||||
this.compression = compression;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
|
||||
public class AppSettingsActivity extends SherlockFragmentActivity {
|
||||
private Uri mAppUri;
|
||||
|
||||
private AppSettingsFragment mSettingsFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Inflate a "Done" custom action bar
|
||||
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.api_settings_save,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// "Done"
|
||||
save();
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(R.layout.api_app_settings_activity);
|
||||
|
||||
mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
|
||||
R.id.api_app_settings_fragment);
|
||||
|
||||
Intent intent = getIntent();
|
||||
mAppUri = intent.getData();
|
||||
if (mAppUri == null) {
|
||||
Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!");
|
||||
finish();
|
||||
return;
|
||||
} else {
|
||||
Log.d(Constants.TAG, "uri: " + mAppUri);
|
||||
loadData(mAppUri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getSupportMenuInflater().inflate(R.menu.api_app_settings, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_api_settings_revoke:
|
||||
revokeAccess();
|
||||
return true;
|
||||
case R.id.menu_api_settings_cancel:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void loadData(Uri appUri) {
|
||||
AppSettings settings = ProviderHelper.getApiAppSettings(this, appUri);
|
||||
mSettingsFragment.setAppSettings(settings);
|
||||
}
|
||||
|
||||
private void revokeAccess() {
|
||||
if (getContentResolver().delete(mAppUri, null, null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
private void save() {
|
||||
ProviderHelper.updateApiApp(this, mSettingsFragment.getAppSettings(), mAppUri);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2014 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.service.remote;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.SelectSecretKeyLayoutFragment;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyValueSpinnerAdapter;
|
||||
import org.sufficientlysecure.keychain.util.AlgorithmNames;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class AppSettingsFragment extends Fragment implements
|
||||
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback {
|
||||
|
||||
// model
|
||||
private AppSettings appSettings;
|
||||
|
||||
// view
|
||||
private LinearLayout mAdvancedSettingsContainer;
|
||||
private BootstrapButton mAdvancedSettingsButton;
|
||||
private TextView mAppNameView;
|
||||
private ImageView mAppIconView;
|
||||
private Spinner mEncryptionAlgorithm;
|
||||
private Spinner mHashAlgorithm;
|
||||
private Spinner mCompression;
|
||||
private TextView mPackageName;
|
||||
private TextView mPackageSignature;
|
||||
|
||||
private SelectSecretKeyLayoutFragment mSelectKeyFragment;
|
||||
|
||||
KeyValueSpinnerAdapter encryptionAdapter;
|
||||
KeyValueSpinnerAdapter hashAdapter;
|
||||
KeyValueSpinnerAdapter compressionAdapter;
|
||||
|
||||
public AppSettings getAppSettings() {
|
||||
return appSettings;
|
||||
}
|
||||
|
||||
public void setAppSettings(AppSettings appSettings) {
|
||||
this.appSettings = appSettings;
|
||||
setPackage(appSettings.getPackageName());
|
||||
mPackageName.setText(appSettings.getPackageName());
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(appSettings.getPackageSignature());
|
||||
byte[] digest = md.digest();
|
||||
String signature = new String(Hex.encode(digest));
|
||||
|
||||
mPackageSignature.setText(signature);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.e(Constants.TAG, "Should not happen!", e);
|
||||
}
|
||||
|
||||
mSelectKeyFragment.selectKey(appSettings.getKeyId());
|
||||
mEncryptionAlgorithm.setSelection(encryptionAdapter.getPosition(appSettings
|
||||
.getEncryptionAlgorithm()));
|
||||
mHashAlgorithm.setSelection(hashAdapter.getPosition(appSettings.getHashAlgorithm()));
|
||||
mCompression.setSelection(compressionAdapter.getPosition(appSettings.getCompression()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.api_app_settings_fragment, container, false);
|
||||
initView(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void initView(View view) {
|
||||
mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getFragmentManager().findFragmentById(
|
||||
R.id.api_app_settings_select_key_fragment);
|
||||
mSelectKeyFragment.setCallback(this);
|
||||
|
||||
mAdvancedSettingsButton = (BootstrapButton) view
|
||||
.findViewById(R.id.api_app_settings_advanced_button);
|
||||
mAdvancedSettingsContainer = (LinearLayout) view
|
||||
.findViewById(R.id.api_app_settings_advanced);
|
||||
|
||||
mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name);
|
||||
mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon);
|
||||
mEncryptionAlgorithm = (Spinner) view
|
||||
.findViewById(R.id.api_app_settings_encryption_algorithm);
|
||||
mHashAlgorithm = (Spinner) view.findViewById(R.id.api_app_settings_hash_algorithm);
|
||||
mCompression = (Spinner) view.findViewById(R.id.api_app_settings_compression);
|
||||
mPackageName = (TextView) view.findViewById(R.id.api_app_settings_package_name);
|
||||
mPackageSignature = (TextView) view.findViewById(R.id.api_app_settings_package_signature);
|
||||
|
||||
AlgorithmNames algorithmNames = new AlgorithmNames(getActivity());
|
||||
|
||||
encryptionAdapter = new KeyValueSpinnerAdapter(getActivity(),
|
||||
algorithmNames.getEncryptionNames());
|
||||
mEncryptionAlgorithm.setAdapter(encryptionAdapter);
|
||||
mEncryptionAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
appSettings.setEncryptionAlgorithm((int) id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
hashAdapter = new KeyValueSpinnerAdapter(getActivity(), algorithmNames.getHashNames());
|
||||
mHashAlgorithm.setAdapter(hashAdapter);
|
||||
mHashAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
appSettings.setHashAlgorithm((int) id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
compressionAdapter = new KeyValueSpinnerAdapter(getActivity(),
|
||||
algorithmNames.getCompressionNames());
|
||||
mCompression.setAdapter(compressionAdapter);
|
||||
mCompression.setOnItemSelectedListener(new OnItemSelectedListener() {
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
appSettings.setCompression((int) id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
final Animation visibleAnimation = new AlphaAnimation(0.0f, 1.0f);
|
||||
visibleAnimation.setDuration(250);
|
||||
final Animation invisibleAnimation = new AlphaAnimation(1.0f, 0.0f);
|
||||
invisibleAnimation.setDuration(250);
|
||||
|
||||
// TODO: Better: collapse/expand animation
|
||||
// final Animation animation2 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
|
||||
// Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, -1.0f,
|
||||
// Animation.RELATIVE_TO_SELF, 0.0f);
|
||||
// animation2.setDuration(150);
|
||||
|
||||
mAdvancedSettingsButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mAdvancedSettingsContainer.getVisibility() == View.VISIBLE) {
|
||||
mAdvancedSettingsContainer.startAnimation(invisibleAnimation);
|
||||
mAdvancedSettingsContainer.setVisibility(View.GONE);
|
||||
mAdvancedSettingsButton.setText(getString(R.string.api_settings_show_advanced));
|
||||
mAdvancedSettingsButton.setLeftIcon("fa-caret-up");
|
||||
} else {
|
||||
mAdvancedSettingsContainer.startAnimation(visibleAnimation);
|
||||
mAdvancedSettingsContainer.setVisibility(View.VISIBLE);
|
||||
mAdvancedSettingsButton.setText(getString(R.string.api_settings_hide_advanced));
|
||||
mAdvancedSettingsButton.setLeftIcon("fa-caret-down");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setPackage(String packageName) {
|
||||
PackageManager pm = getActivity().getApplicationContext().getPackageManager();
|
||||
|
||||
// get application name and icon from package manager
|
||||
String appName = null;
|
||||
Drawable appIcon = null;
|
||||
try {
|
||||
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
|
||||
|
||||
appName = (String) pm.getApplicationLabel(ai);
|
||||
appIcon = pm.getApplicationIcon(ai);
|
||||
} catch (final NameNotFoundException e) {
|
||||
// fallback
|
||||
appName = packageName;
|
||||
}
|
||||
mAppNameView.setText(appName);
|
||||
mAppIconView.setImageDrawable(appIcon);
|
||||
}
|
||||
|
||||
/**
|
||||
* callback from select secret key fragment
|
||||
*/
|
||||
@Override
|
||||
public void onKeySelected(long secretKeyId) {
|
||||
appSettings.setKeyId(secretKeyId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.PasswordCallback;
|
||||
|
||||
import org.spongycastle.openpgp.PGPPrivateKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openssl.PEMWriter;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpToX509;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
|
||||
public class ExtendedApiService extends RemoteService {
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
private void selfSignedX509CertSafe(String subjAltNameURI, IExtendedApiCallback callback,
|
||||
AppSettings appSettings) {
|
||||
|
||||
// TODO: for pgp keyrings with password
|
||||
CallbackHandler pgpPwdCallbackHandler = new PgpToX509.PredefinedPasswordCallbackHandler("");
|
||||
|
||||
try {
|
||||
long keyId = appSettings.getKeyId();
|
||||
PGPSecretKey pgpSecretKey = PgpKeyHelper.getSigningKey(this, keyId);
|
||||
|
||||
PasswordCallback pgpSecKeyPasswordCallBack = new PasswordCallback("pgp passphrase?",
|
||||
false);
|
||||
pgpPwdCallbackHandler.handle(new Callback[] { pgpSecKeyPasswordCallBack });
|
||||
PGPPrivateKey pgpPrivKey = pgpSecretKey.extractPrivateKey(
|
||||
pgpSecKeyPasswordCallBack.getPassword(), Constants.BOUNCY_CASTLE_PROVIDER_NAME);
|
||||
pgpSecKeyPasswordCallBack.clearPassword();
|
||||
|
||||
X509Certificate selfSignedCert = PgpToX509.createSelfSignedCert(pgpSecretKey,
|
||||
pgpPrivKey, subjAltNameURI);
|
||||
|
||||
// Write x509cert and privKey into files
|
||||
// FileOutputStream fosCert = context.openFileOutput(CERT_FILENAME,
|
||||
// Context.MODE_PRIVATE);
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
PEMWriter pemWriterCert = new PEMWriter(new PrintWriter(outStream));
|
||||
pemWriterCert.writeObject(selfSignedCert);
|
||||
pemWriterCert.close();
|
||||
|
||||
byte[] outputBytes = outStream.toByteArray();
|
||||
|
||||
callback.onSuccess(outputBytes);
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "ExtendedApiService", e);
|
||||
try {
|
||||
callback.onError(e.getMessage());
|
||||
} catch (RemoteException e1) {
|
||||
Log.e(Constants.TAG, "ExtendedApiService", e);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: no private key at the moment! Don't give it to others
|
||||
// PrivateKey privKey = pgpPrivKey.getKey();
|
||||
// FileOutputStream fosKey = context.openFileOutput(PRIV_KEY_FILENAME,
|
||||
// Context.MODE_PRIVATE);
|
||||
// PEMWriter pemWriterKey = new PEMWriter(new PrintWriter(fosKey));
|
||||
// pemWriterKey.writeObject(privKey);
|
||||
// pemWriterKey.close();
|
||||
}
|
||||
|
||||
private final IExtendedApiService.Stub mBinder = new IExtendedApiService.Stub() {
|
||||
|
||||
@Override
|
||||
public void encrypt(byte[] inputBytes, String passphrase, IExtendedApiCallback callback)
|
||||
throws RemoteException {
|
||||
// TODO : implement
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selfSignedX509Cert(final String subjAltNameURI,
|
||||
final IExtendedApiCallback callback) throws RemoteException {
|
||||
final AppSettings settings = getAppSettings();
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
selfSignedX509CertSafe(subjAltNameURI, callback, settings);
|
||||
}
|
||||
};
|
||||
|
||||
checkAndEnqueue(r);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import org.openintents.openpgp.IOpenPgpCallback;
|
||||
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
|
||||
import org.openintents.openpgp.IOpenPgpService;
|
||||
import org.openintents.openpgp.OpenPgpData;
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.spongycastle.util.Arrays;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.service.exception.NoUserIdsException;
|
||||
import org.sufficientlysecure.keychain.service.exception.UserInteractionRequiredException;
|
||||
import org.sufficientlysecure.keychain.service.exception.WrongPassphraseException;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
|
||||
public class OpenPgpService extends RemoteService {
|
||||
|
||||
private String getCachedPassphrase(long keyId, boolean allowUserInteraction)
|
||||
throws UserInteractionRequiredException {
|
||||
String passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), keyId);
|
||||
|
||||
if (passphrase == null) {
|
||||
if (!allowUserInteraction) {
|
||||
throw new UserInteractionRequiredException(
|
||||
"Passphrase not found in cache, please enter your passphrase!");
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "No passphrase! Activity required!");
|
||||
|
||||
// start passphrase dialog
|
||||
PassphraseActivityCallback callback = new PassphraseActivityCallback();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putLong(RemoteServiceActivity.EXTRA_SECRET_KEY_ID, keyId);
|
||||
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_CACHE_PASSPHRASE, callback,
|
||||
extras);
|
||||
|
||||
if (callback.isSuccess()) {
|
||||
Log.d(Constants.TAG, "New passphrase entered!");
|
||||
|
||||
// get again after it was entered
|
||||
passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), keyId);
|
||||
} else {
|
||||
Log.d(Constants.TAG, "Passphrase dialog canceled!");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public class PassphraseActivityCallback extends UserInputCallback {
|
||||
|
||||
private boolean success = false;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUserInput(Message msg) {
|
||||
if (msg.arg1 == OKAY) {
|
||||
success = true;
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Search database for key ids based on emails.
|
||||
*
|
||||
* @param encryptionUserIds
|
||||
* @return
|
||||
*/
|
||||
private long[] getKeyIdsFromEmails(String[] encryptionUserIds, boolean allowUserInteraction)
|
||||
throws UserInteractionRequiredException {
|
||||
// find key ids to given emails in database
|
||||
ArrayList<Long> keyIds = new ArrayList<Long>();
|
||||
|
||||
boolean missingUserIdsCheck = false;
|
||||
boolean dublicateUserIdsCheck = false;
|
||||
ArrayList<String> missingUserIds = new ArrayList<String>();
|
||||
ArrayList<String> dublicateUserIds = new ArrayList<String>();
|
||||
|
||||
for (String email : encryptionUserIds) {
|
||||
Uri uri = KeychainContract.KeyRings.buildPublicKeyRingsByEmailsUri(email);
|
||||
Cursor cur = getContentResolver().query(uri, null, null, null, null);
|
||||
if (cur.moveToFirst()) {
|
||||
long id = cur.getLong(cur.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID));
|
||||
keyIds.add(id);
|
||||
} else {
|
||||
missingUserIdsCheck = true;
|
||||
missingUserIds.add(email);
|
||||
Log.d(Constants.TAG, "user id missing");
|
||||
}
|
||||
if (cur.moveToNext()) {
|
||||
dublicateUserIdsCheck = true;
|
||||
dublicateUserIds.add(email);
|
||||
Log.d(Constants.TAG, "more than one user id with the same email");
|
||||
}
|
||||
}
|
||||
|
||||
// convert to long[]
|
||||
long[] keyIdsArray = new long[keyIds.size()];
|
||||
for (int i = 0; i < keyIdsArray.length; i++) {
|
||||
keyIdsArray[i] = keyIds.get(i);
|
||||
}
|
||||
|
||||
// allow the user to verify pub key selection
|
||||
if (allowUserInteraction && (missingUserIdsCheck || dublicateUserIdsCheck)) {
|
||||
SelectPubKeysActivityCallback callback = new SelectPubKeysActivityCallback();
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putLongArray(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray);
|
||||
extras.putStringArrayList(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds);
|
||||
extras.putStringArrayList(RemoteServiceActivity.EXTRA_DUBLICATE_USER_IDS,
|
||||
dublicateUserIds);
|
||||
|
||||
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS, callback,
|
||||
extras);
|
||||
|
||||
if (callback.isSuccess()) {
|
||||
Log.d(Constants.TAG, "New selection of pub keys!");
|
||||
keyIdsArray = callback.getPubKeyIds();
|
||||
} else {
|
||||
Log.d(Constants.TAG, "Pub key selection canceled!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// if no user interaction is allow throw exceptions on duplicate or missing pub keys
|
||||
if (!allowUserInteraction) {
|
||||
if (missingUserIdsCheck)
|
||||
throw new UserInteractionRequiredException(
|
||||
"Pub keys for these user ids are missing:" + missingUserIds.toString());
|
||||
if (dublicateUserIdsCheck)
|
||||
throw new UserInteractionRequiredException(
|
||||
"More than one pub key with these user ids exist:"
|
||||
+ dublicateUserIds.toString());
|
||||
}
|
||||
|
||||
if (keyIdsArray.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return keyIdsArray;
|
||||
}
|
||||
|
||||
public class SelectPubKeysActivityCallback extends UserInputCallback {
|
||||
public static final String PUB_KEY_IDS = "pub_key_ids";
|
||||
|
||||
private boolean success = false;
|
||||
private long[] pubKeyIds;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public long[] getPubKeyIds() {
|
||||
return pubKeyIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUserInput(Message msg) {
|
||||
if (msg.arg1 == OKAY) {
|
||||
success = true;
|
||||
pubKeyIds = msg.getData().getLongArray(PUB_KEY_IDS);
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private synchronized void getKeyIdsSafe(String[] userIds, boolean allowUserInteraction,
|
||||
IOpenPgpKeyIdsCallback callback, AppSettings appSettings) {
|
||||
try {
|
||||
long[] keyIds = getKeyIdsFromEmails(userIds, allowUserInteraction);
|
||||
if (keyIds == null) {
|
||||
throw new NoUserIdsException("No user ids!");
|
||||
}
|
||||
|
||||
callback.onSuccess(keyIds);
|
||||
} catch (UserInteractionRequiredException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
|
||||
} catch (NoUserIdsException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.NO_USER_IDS, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void encryptAndSignSafe(OpenPgpData inputData,
|
||||
final OpenPgpData outputData, long[] keyIds, boolean allowUserInteraction,
|
||||
IOpenPgpCallback callback, AppSettings appSettings, boolean sign) {
|
||||
try {
|
||||
// TODO: other options of OpenPgpData!
|
||||
byte[] inputBytes = getInput(inputData);
|
||||
boolean asciiArmor = false;
|
||||
if (outputData.getType() == OpenPgpData.TYPE_STRING) {
|
||||
asciiArmor = true;
|
||||
}
|
||||
|
||||
// add own key for encryption
|
||||
keyIds = Arrays.copyOf(keyIds, keyIds.length + 1);
|
||||
keyIds[keyIds.length - 1] = appSettings.getKeyId();
|
||||
|
||||
// build InputData and write into OutputStream
|
||||
InputStream inputStream = new ByteArrayInputStream(inputBytes);
|
||||
long inputLength = inputBytes.length;
|
||||
InputData inputDt = new InputData(inputStream, inputLength);
|
||||
|
||||
OutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
PgpOperation operation = new PgpOperation(getContext(), null, inputDt, outputStream);
|
||||
if (sign) {
|
||||
String passphrase = getCachedPassphrase(appSettings.getKeyId(),
|
||||
allowUserInteraction);
|
||||
if (passphrase == null) {
|
||||
throw new WrongPassphraseException("No or wrong passphrase!");
|
||||
}
|
||||
|
||||
operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
|
||||
appSettings.getEncryptionAlgorithm(), appSettings.getKeyId(),
|
||||
appSettings.getHashAlgorithm(), true, passphrase);
|
||||
} else {
|
||||
operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
|
||||
appSettings.getEncryptionAlgorithm(), Id.key.none,
|
||||
appSettings.getHashAlgorithm(), true, null);
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
|
||||
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
|
||||
|
||||
OpenPgpData output = null;
|
||||
if (asciiArmor) {
|
||||
output = new OpenPgpData(new String(outputBytes));
|
||||
} else {
|
||||
output = new OpenPgpData(outputBytes);
|
||||
}
|
||||
|
||||
// return over handler on client side
|
||||
callback.onSuccess(output, null);
|
||||
} catch (UserInteractionRequiredException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
|
||||
} catch (WrongPassphraseException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: asciiArmor?!
|
||||
private void signSafe(byte[] inputBytes, boolean allowUserInteraction,
|
||||
IOpenPgpCallback callback, AppSettings appSettings) {
|
||||
try {
|
||||
// build InputData and write into OutputStream
|
||||
InputStream inputStream = new ByteArrayInputStream(inputBytes);
|
||||
long inputLength = inputBytes.length;
|
||||
InputData inputData = new InputData(inputStream, inputLength);
|
||||
|
||||
OutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
String passphrase = getCachedPassphrase(appSettings.getKeyId(), allowUserInteraction);
|
||||
if (passphrase == null) {
|
||||
throw new WrongPassphraseException("No or wrong passphrase!");
|
||||
}
|
||||
|
||||
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
|
||||
operation.signText(appSettings.getKeyId(), passphrase, appSettings.getHashAlgorithm(),
|
||||
Preferences.getPreferences(this).getForceV3Signatures());
|
||||
|
||||
outputStream.close();
|
||||
|
||||
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
|
||||
OpenPgpData output = new OpenPgpData(new String(outputBytes));
|
||||
|
||||
// return over handler on client side
|
||||
callback.onSuccess(output, null);
|
||||
} catch (UserInteractionRequiredException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
|
||||
} catch (WrongPassphraseException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void decryptAndVerifySafe(byte[] inputBytes, boolean allowUserInteraction,
|
||||
IOpenPgpCallback callback, AppSettings appSettings) {
|
||||
try {
|
||||
// TODO: this is not really needed
|
||||
// checked if it is text with BEGIN and END tags
|
||||
String message = new String(inputBytes);
|
||||
Log.d(Constants.TAG, "in: " + message);
|
||||
boolean signedOnly = false;
|
||||
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(message);
|
||||
if (matcher.matches()) {
|
||||
Log.d(Constants.TAG, "PGP_MESSAGE matched");
|
||||
message = matcher.group(1);
|
||||
// replace non breakable spaces
|
||||
message = message.replaceAll("\\xa0", " ");
|
||||
|
||||
// overwrite inputBytes
|
||||
inputBytes = message.getBytes();
|
||||
} else {
|
||||
matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(message);
|
||||
if (matcher.matches()) {
|
||||
signedOnly = true;
|
||||
Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
|
||||
message = matcher.group(1);
|
||||
// replace non breakable spaces
|
||||
message = message.replaceAll("\\xa0", " ");
|
||||
|
||||
// overwrite inputBytes
|
||||
inputBytes = message.getBytes();
|
||||
} else {
|
||||
Log.d(Constants.TAG, "Nothing matched! Binary?");
|
||||
}
|
||||
}
|
||||
// END TODO
|
||||
|
||||
Log.d(Constants.TAG, "in: " + new String(inputBytes));
|
||||
|
||||
// TODO: This allows to decrypt messages with ALL secret keys, not only the one for the
|
||||
// app, Fix this?
|
||||
|
||||
String passphrase = null;
|
||||
if (!signedOnly) {
|
||||
// BEGIN Get key
|
||||
// TODO: this input stream is consumed after PgpMain.getDecryptionKeyId()... do it
|
||||
// better!
|
||||
InputStream inputStream2 = new ByteArrayInputStream(inputBytes);
|
||||
|
||||
// TODO: duplicates functions from DecryptActivity!
|
||||
long secretKeyId;
|
||||
try {
|
||||
if (inputStream2.markSupported()) {
|
||||
// should probably set this to the max size of two
|
||||
// pgpF objects, if it even needs to be anything other
|
||||
// than 0.
|
||||
inputStream2.mark(200);
|
||||
}
|
||||
secretKeyId = PgpHelper.getDecryptionKeyId(this, inputStream2);
|
||||
if (secretKeyId == Id.key.none) {
|
||||
throw new PgpGeneralException(getString(R.string.error_no_secret_key_found));
|
||||
}
|
||||
} catch (NoAsymmetricEncryptionException e) {
|
||||
if (inputStream2.markSupported()) {
|
||||
inputStream2.reset();
|
||||
}
|
||||
secretKeyId = Id.key.symmetric;
|
||||
if (!PgpOperation.hasSymmetricEncryption(this, inputStream2)) {
|
||||
throw new PgpGeneralException(
|
||||
getString(R.string.error_no_known_encryption_found));
|
||||
}
|
||||
// we do not support symmetric decryption from the API!
|
||||
throw new Exception("Symmetric decryption is not supported!");
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "secretKeyId " + secretKeyId);
|
||||
|
||||
passphrase = getCachedPassphrase(secretKeyId, allowUserInteraction);
|
||||
if (passphrase == null) {
|
||||
throw new WrongPassphraseException("No or wrong passphrase!");
|
||||
}
|
||||
}
|
||||
|
||||
// build InputData and write into OutputStream
|
||||
InputStream inputStream = new ByteArrayInputStream(inputBytes);
|
||||
long inputLength = inputBytes.length;
|
||||
InputData inputData = new InputData(inputStream, inputLength);
|
||||
|
||||
OutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
Bundle outputBundle;
|
||||
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
|
||||
if (signedOnly) {
|
||||
// TODO: download missing keys from keyserver?
|
||||
outputBundle = operation.verifyText(false);
|
||||
} else {
|
||||
outputBundle = operation.decryptAndVerify(passphrase, false);
|
||||
}
|
||||
|
||||
outputStream.close();
|
||||
|
||||
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
|
||||
|
||||
// get signature informations from bundle
|
||||
boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE);
|
||||
|
||||
OpenPgpSignatureResult sigResult = null;
|
||||
if (signature) {
|
||||
long signatureKeyId = outputBundle
|
||||
.getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
|
||||
String signatureUserId = outputBundle
|
||||
.getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
|
||||
boolean signatureSuccess = outputBundle
|
||||
.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS);
|
||||
boolean signatureUnknown = outputBundle
|
||||
.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN);
|
||||
|
||||
int signatureStatus = OpenPgpSignatureResult.SIGNATURE_ERROR;
|
||||
if (signatureSuccess) {
|
||||
signatureStatus = OpenPgpSignatureResult.SIGNATURE_SUCCESS_TRUSTED;
|
||||
} else if (signatureUnknown) {
|
||||
signatureStatus = OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY;
|
||||
}
|
||||
|
||||
sigResult = new OpenPgpSignatureResult(signatureStatus, signatureUserId,
|
||||
signedOnly, signatureKeyId);
|
||||
}
|
||||
OpenPgpData output = new OpenPgpData(new String(outputBytes));
|
||||
|
||||
// return over handler on client side
|
||||
callback.onSuccess(output, sigResult);
|
||||
} catch (UserInteractionRequiredException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
|
||||
} catch (WrongPassphraseException e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error to IOpenPgpCallback
|
||||
*
|
||||
* @param callback
|
||||
* @param errorId
|
||||
* @param message
|
||||
*/
|
||||
private void callbackOpenPgpError(IOpenPgpCallback callback, int errorId, String message) {
|
||||
try {
|
||||
callback.onError(new OpenPgpError(0, message));
|
||||
} catch (Exception t) {
|
||||
Log.e(Constants.TAG,
|
||||
"Exception while returning OpenPgpError to client via callback.onError()", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void callbackOpenPgpError(IOpenPgpKeyIdsCallback callback, int errorId, String message) {
|
||||
try {
|
||||
callback.onError(new OpenPgpError(0, message));
|
||||
} catch (Exception t) {
|
||||
Log.e(Constants.TAG,
|
||||
"Exception while returning OpenPgpError to client via callback.onError()", t);
|
||||
}
|
||||
}
|
||||
|
||||
private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
|
||||
|
||||
@Override
|
||||
public void encrypt(final OpenPgpData input, final OpenPgpData output, final long[] keyIds,
|
||||
final IOpenPgpCallback callback) throws RemoteException {
|
||||
final AppSettings settings = getAppSettings();
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
encryptAndSignSafe(input, output, keyIds, true, callback, settings, false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAndEnqueue(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signAndEncrypt(final OpenPgpData input, final OpenPgpData output,
|
||||
final long[] keyIds, final IOpenPgpCallback callback) throws RemoteException {
|
||||
final AppSettings settings = getAppSettings();
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
encryptAndSignSafe(input, output, keyIds, true, callback, settings, true);
|
||||
}
|
||||
};
|
||||
|
||||
checkAndEnqueue(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sign(final OpenPgpData input, final OpenPgpData output,
|
||||
final IOpenPgpCallback callback) throws RemoteException {
|
||||
final AppSettings settings = getAppSettings();
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
signSafe(getInput(input), true, callback, settings);
|
||||
}
|
||||
};
|
||||
|
||||
checkAndEnqueue(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptAndVerify(final OpenPgpData input, final OpenPgpData output,
|
||||
final IOpenPgpCallback callback) throws RemoteException {
|
||||
|
||||
final AppSettings settings = getAppSettings();
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
decryptAndVerifySafe(getInput(input), true, callback, settings);
|
||||
}
|
||||
};
|
||||
|
||||
checkAndEnqueue(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getKeyIds(final String[] userIds, final boolean allowUserInteraction,
|
||||
final IOpenPgpKeyIdsCallback callback) throws RemoteException {
|
||||
|
||||
final AppSettings settings = getAppSettings();
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getKeyIdsSafe(userIds, allowUserInteraction, callback, settings);
|
||||
}
|
||||
};
|
||||
|
||||
checkAndEnqueue(r);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static byte[] getInput(OpenPgpData data) {
|
||||
// TODO: support Uri and ParcelFileDescriptor
|
||||
|
||||
byte[] inBytes = null;
|
||||
switch (data.getType()) {
|
||||
case OpenPgpData.TYPE_STRING:
|
||||
inBytes = data.getString().getBytes();
|
||||
break;
|
||||
|
||||
case OpenPgpData.TYPE_BYTE_ARRAY:
|
||||
inBytes = data.getBytes();
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(Constants.TAG, "Uri and ParcelFileDescriptor not supported right now!");
|
||||
break;
|
||||
}
|
||||
|
||||
return inBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class RegisteredAppsAdapter extends CursorAdapter {
|
||||
|
||||
private LayoutInflater mInflater;
|
||||
private PackageManager pm;
|
||||
|
||||
public RegisteredAppsAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
pm = context.getApplicationContext().getPackageManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
TextView text = (TextView) view.findViewById(R.id.api_apps_adapter_item_name);
|
||||
ImageView icon = (ImageView) view.findViewById(R.id.api_apps_adapter_item_icon);
|
||||
|
||||
String packageName = cursor.getString(cursor.getColumnIndex(ApiApps.PACKAGE_NAME));
|
||||
if (packageName != null) {
|
||||
// get application name
|
||||
try {
|
||||
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
|
||||
|
||||
text.setText(pm.getApplicationLabel(ai));
|
||||
icon.setImageDrawable(pm.getApplicationIcon(ai));
|
||||
} catch (final NameNotFoundException e) {
|
||||
// fallback
|
||||
text.setText(packageName);
|
||||
}
|
||||
} else {
|
||||
// fallback
|
||||
text.setText(packageName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.api_apps_adapter_list_item, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.DrawerActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
public class RegisteredAppsListActivity extends DrawerActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.api_apps_list_activity);
|
||||
|
||||
setupDrawerNavigation(savedInstanceState);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockListFragment;
|
||||
|
||||
public class RegisteredAppsListFragment extends SherlockListFragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
// This is the Adapter being used to display the list's data.
|
||||
RegisteredAppsAdapter mAdapter;
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
getListView().setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
// edit app settings
|
||||
Intent intent = new Intent(getActivity(), AppSettingsActivity.class);
|
||||
intent.setData(ContentUris.withAppendedId(KeychainContract.ApiApps.CONTENT_URI, id));
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
// Give some text to display if there is no data. In a real
|
||||
// application this would come from a resource.
|
||||
setEmptyText(getString(R.string.api_no_apps));
|
||||
|
||||
// We have a menu item to show in action bar.
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// Create an empty adapter we will use to display the loaded data.
|
||||
mAdapter = new RegisteredAppsAdapter(getActivity(), null, 0);
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
// These are the Contacts rows that we will retrieve.
|
||||
static final String[] PROJECTION = new String[] { ApiApps._ID, ApiApps.PACKAGE_NAME };
|
||||
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
// This is called when a new Loader needs to be created. This
|
||||
// sample only has one Loader, so we don't care about the ID.
|
||||
// First, pick the base URI to use depending on whether we are
|
||||
// currently filtering.
|
||||
Uri baseUri = ApiApps.CONTENT_URI;
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null,
|
||||
ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC");
|
||||
}
|
||||
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// Swap the new cursor in. (The framework will take care of closing the
|
||||
// old cursor once we return.)
|
||||
mAdapter.swapCursor(data);
|
||||
}
|
||||
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// This is called when the last Cursor provided to onLoadFinished()
|
||||
// above is about to be closed. We need to make sure we are no
|
||||
// longer using it.
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.exception.WrongPackageSignatureException;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.PausableThreadPoolExecutor;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
|
||||
/**
|
||||
* Abstract service class for remote APIs that handle app registration and user input.
|
||||
*/
|
||||
public abstract class RemoteService extends Service {
|
||||
Context mContext;
|
||||
|
||||
private final ArrayBlockingQueue<Runnable> mPoolQueue = new ArrayBlockingQueue<Runnable>(100);
|
||||
// TODO: Are these parameters okay?
|
||||
private PausableThreadPoolExecutor mThreadPool = new PausableThreadPoolExecutor(2, 4, 10,
|
||||
TimeUnit.SECONDS, mPoolQueue);
|
||||
|
||||
private final Object userInputLock = new Object();
|
||||
|
||||
/**
|
||||
* Override handleUserInput() to handle OKAY (1) and CANCEL (0). After handling the waiting
|
||||
* threads will be notified and the queue resumed
|
||||
*/
|
||||
protected class UserInputCallback extends BaseCallback {
|
||||
|
||||
public void handleUserInput(Message msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
handleUserInput(msg);
|
||||
|
||||
// resume
|
||||
synchronized (userInputLock) {
|
||||
userInputLock.notifyAll();
|
||||
}
|
||||
mThreadPool.resume();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends Handler.Callback with OKAY (1), CANCEL (0) variables
|
||||
*/
|
||||
private class BaseCallback implements Handler.Callback {
|
||||
public static final int OKAY = 1;
|
||||
public static final int CANCEL = 0;
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be used from Stub implementations of AIDL interfaces to enqueue a runnable for
|
||||
* execution
|
||||
*
|
||||
* @param r
|
||||
*/
|
||||
protected void checkAndEnqueue(Runnable r) {
|
||||
try {
|
||||
if (isCallerAllowed(false)) {
|
||||
mThreadPool.execute(r);
|
||||
|
||||
Log.d(Constants.TAG, "Enqueued runnable…");
|
||||
} else {
|
||||
String[] callingPackages = getPackageManager().getPackagesForUid(
|
||||
Binder.getCallingUid());
|
||||
// TODO: currently simply uses first entry
|
||||
String packageName = callingPackages[0];
|
||||
|
||||
byte[] packageSignature;
|
||||
try {
|
||||
packageSignature = getPackageSignature(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(Constants.TAG, "Should not happen, returning!", e);
|
||||
return;
|
||||
}
|
||||
Log.e(Constants.TAG,
|
||||
"Not allowed to use service! Starting activity for registration!");
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName);
|
||||
extras.putByteArray(RemoteServiceActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature);
|
||||
RegisterActivityCallback callback = new RegisterActivityCallback();
|
||||
|
||||
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_REGISTER, callback,
|
||||
extras);
|
||||
|
||||
if (callback.isAllowed()) {
|
||||
mThreadPool.execute(r);
|
||||
Log.d(Constants.TAG, "Enqueued runnable…");
|
||||
} else {
|
||||
Log.d(Constants.TAG, "User disallowed app!");
|
||||
}
|
||||
}
|
||||
} catch (WrongPackageSignatureException e) {
|
||||
Log.e(Constants.TAG, e.getMessage());
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(RemoteServiceActivity.EXTRA_ERROR_MESSAGE,
|
||||
getString(R.string.api_error_wrong_signature));
|
||||
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_ERROR_MESSAGE, null, extras);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getPackageSignature(String packageName) throws NameNotFoundException {
|
||||
PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
|
||||
PackageManager.GET_SIGNATURES);
|
||||
Signature[] signatures = pkgInfo.signatures;
|
||||
// TODO: Only first signature?!
|
||||
byte[] packageSignature = signatures[0].toByteArray();
|
||||
|
||||
return packageSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks current thread and pauses execution of runnables and starts activity for user input
|
||||
*
|
||||
* @param action
|
||||
* @param messenger
|
||||
* @param extras
|
||||
*/
|
||||
protected void pauseAndStartUserInteraction(String action, BaseCallback callback, Bundle extras) {
|
||||
synchronized (userInputLock) {
|
||||
mThreadPool.pause();
|
||||
|
||||
Log.d(Constants.TAG, "starting activity...");
|
||||
Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction(action);
|
||||
|
||||
Messenger messenger = new Messenger(new Handler(getMainLooper(), callback));
|
||||
|
||||
extras.putParcelable(RemoteServiceActivity.EXTRA_MESSENGER, messenger);
|
||||
intent.putExtras(extras);
|
||||
|
||||
startActivity(intent);
|
||||
|
||||
// lock current thread for user input
|
||||
try {
|
||||
userInputLock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(Constants.TAG, "CryptoService", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves AppSettings from database for the application calling this remote service
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected AppSettings getAppSettings() {
|
||||
String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid());
|
||||
|
||||
// get app settings for this package
|
||||
for (int i = 0; i < callingPackages.length; i++) {
|
||||
String currentPkg = callingPackages[i];
|
||||
|
||||
Uri uri = KeychainContract.ApiApps.buildByPackageNameUri(currentPkg);
|
||||
|
||||
AppSettings settings = ProviderHelper.getApiAppSettings(this, uri);
|
||||
|
||||
if (settings != null)
|
||||
return settings;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class RegisterActivityCallback extends BaseCallback {
|
||||
public static final String PACKAGE_NAME = "package_name";
|
||||
|
||||
private boolean allowed = false;
|
||||
private String packageName;
|
||||
|
||||
public boolean isAllowed() {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
if (msg.arg1 == OKAY) {
|
||||
allowed = true;
|
||||
packageName = msg.getData().getString(PACKAGE_NAME);
|
||||
|
||||
// resume threads
|
||||
try {
|
||||
if (isPackageAllowed(packageName)) {
|
||||
synchronized (userInputLock) {
|
||||
userInputLock.notifyAll();
|
||||
}
|
||||
mThreadPool.resume();
|
||||
} else {
|
||||
// Should not happen!
|
||||
Log.e(Constants.TAG, "Should not happen! Emergency shutdown!");
|
||||
mThreadPool.shutdownNow();
|
||||
}
|
||||
} catch (WrongPackageSignatureException e) {
|
||||
Log.e(Constants.TAG, e.getMessage());
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(RemoteServiceActivity.EXTRA_ERROR_MESSAGE,
|
||||
getString(R.string.api_error_wrong_signature));
|
||||
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_ERROR_MESSAGE, null,
|
||||
extras);
|
||||
}
|
||||
} else {
|
||||
allowed = false;
|
||||
|
||||
synchronized (userInputLock) {
|
||||
userInputLock.notifyAll();
|
||||
}
|
||||
mThreadPool.resume();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if process that binds to this service (i.e. the package name corresponding to the
|
||||
* process) is in the list of allowed package names.
|
||||
*
|
||||
* @param allowOnlySelf
|
||||
* allow only Keychain app itself
|
||||
* @return true if process is allowed to use this service
|
||||
* @throws WrongPackageSignatureException
|
||||
*/
|
||||
private boolean isCallerAllowed(boolean allowOnlySelf) throws WrongPackageSignatureException {
|
||||
return isUidAllowed(Binder.getCallingUid(), allowOnlySelf);
|
||||
}
|
||||
|
||||
private boolean isUidAllowed(int uid, boolean allowOnlySelf)
|
||||
throws WrongPackageSignatureException {
|
||||
if (android.os.Process.myUid() == uid) {
|
||||
return true;
|
||||
}
|
||||
if (allowOnlySelf) { // barrier
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] callingPackages = getPackageManager().getPackagesForUid(uid);
|
||||
|
||||
// is calling package allowed to use this service?
|
||||
for (int i = 0; i < callingPackages.length; i++) {
|
||||
String currentPkg = callingPackages[i];
|
||||
|
||||
if (isPackageAllowed(currentPkg)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "Caller is NOT allowed!");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if packageName is a registered app for the API. Does not return true for own package!
|
||||
*
|
||||
* @param packageName
|
||||
* @return
|
||||
* @throws WrongPackageSignatureException
|
||||
*/
|
||||
private boolean isPackageAllowed(String packageName) throws WrongPackageSignatureException {
|
||||
Log.d(Constants.TAG, "packageName: " + packageName);
|
||||
|
||||
ArrayList<String> allowedPkgs = ProviderHelper.getRegisteredApiApps(this);
|
||||
Log.d(Constants.TAG, "allowed: " + allowedPkgs);
|
||||
|
||||
// check if package is allowed to use our service
|
||||
if (allowedPkgs.contains(packageName)) {
|
||||
Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
|
||||
|
||||
// check package signature
|
||||
byte[] currentSig;
|
||||
try {
|
||||
currentSig = getPackageSignature(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
throw new WrongPackageSignatureException(e.getMessage());
|
||||
}
|
||||
|
||||
byte[] storedSig = ProviderHelper.getApiAppSignature(this, packageName);
|
||||
if (Arrays.equals(currentSig, storedSig)) {
|
||||
Log.d(Constants.TAG,
|
||||
"Package signature is correct! (equals signature from database)");
|
||||
return true;
|
||||
} else {
|
||||
throw new WrongPackageSignatureException(
|
||||
"PACKAGE NOT ALLOWED! Signature wrong! (Signature not equals signature from database)");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mContext = this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.service.remote;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.sufficientlysecure.htmltextview.HtmlTextView;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
|
||||
public class RemoteServiceActivity extends SherlockFragmentActivity {
|
||||
|
||||
public static final String ACTION_REGISTER = Constants.INTENT_PREFIX + "API_ACTIVITY_REGISTER";
|
||||
public static final String ACTION_CACHE_PASSPHRASE = Constants.INTENT_PREFIX
|
||||
+ "API_ACTIVITY_CACHE_PASSPHRASE";
|
||||
public static final String ACTION_SELECT_PUB_KEYS = Constants.INTENT_PREFIX
|
||||
+ "API_ACTIVITY_SELECT_PUB_KEYS";
|
||||
public static final String ACTION_ERROR_MESSAGE = Constants.INTENT_PREFIX
|
||||
+ "API_ACTIVITY_ERROR_MESSAGE";
|
||||
|
||||
public static final String EXTRA_MESSENGER = "messenger";
|
||||
|
||||
// passphrase action
|
||||
public static final String EXTRA_SECRET_KEY_ID = "secret_key_id";
|
||||
// register action
|
||||
public static final String EXTRA_PACKAGE_NAME = "package_name";
|
||||
public static final String EXTRA_PACKAGE_SIGNATURE = "package_signature";
|
||||
// select pub keys action
|
||||
public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
|
||||
public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids";
|
||||
public static final String EXTRA_DUBLICATE_USER_IDS = "dublicate_user_ids";
|
||||
// error message
|
||||
public static final String EXTRA_ERROR_MESSAGE = "error_message";
|
||||
|
||||
private Messenger mMessenger;
|
||||
|
||||
// register view
|
||||
private AppSettingsFragment mSettingsFragment;
|
||||
// select pub keys view
|
||||
private SelectPublicKeyFragment mSelectFragment;
|
||||
|
||||
// has the user clicked one of the buttons
|
||||
// or do we need to handle the callback in onStop()
|
||||
private boolean finishHandled;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
handleActions(getIntent(), savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (!finishHandled) {
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleActions(Intent intent, Bundle savedInstanceState) {
|
||||
finishHandled = false;
|
||||
|
||||
String action = intent.getAction();
|
||||
Bundle extras = intent.getExtras();
|
||||
|
||||
if (extras == null) {
|
||||
extras = new Bundle();
|
||||
}
|
||||
|
||||
mMessenger = extras.getParcelable(EXTRA_MESSENGER);
|
||||
|
||||
/**
|
||||
* com.android.crypto actions
|
||||
*/
|
||||
if (ACTION_REGISTER.equals(action)) {
|
||||
final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
|
||||
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
|
||||
|
||||
// Inflate a "Done"/"Cancel" custom action bar view
|
||||
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.api_register_allow,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// Allow
|
||||
|
||||
// user needs to select a key!
|
||||
if (mSettingsFragment.getAppSettings().getKeyId() == Id.key.none) {
|
||||
Toast.makeText(RemoteServiceActivity.this,
|
||||
R.string.api_register_error_select_key, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
ProviderHelper.insertApiApp(RemoteServiceActivity.this,
|
||||
mSettingsFragment.getAppSettings());
|
||||
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = RemoteService.RegisterActivityCallback.OKAY;
|
||||
Bundle data = new Bundle();
|
||||
data.putString(RemoteService.RegisterActivityCallback.PACKAGE_NAME,
|
||||
packageName);
|
||||
msg.setData(data);
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
|
||||
finishHandled = true;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}, R.string.api_register_disallow, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// Disallow
|
||||
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
|
||||
finishHandled = true;
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(R.layout.api_app_register_activity);
|
||||
|
||||
mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
|
||||
R.id.api_app_settings_fragment);
|
||||
|
||||
AppSettings settings = new AppSettings(packageName, packageSignature);
|
||||
mSettingsFragment.setAppSettings(settings);
|
||||
} else if (ACTION_CACHE_PASSPHRASE.equals(action)) {
|
||||
long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID);
|
||||
|
||||
showPassphraseDialog(secretKeyId);
|
||||
} else if (ACTION_SELECT_PUB_KEYS.equals(action)) {
|
||||
long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
|
||||
ArrayList<String> missingUserIds = intent
|
||||
.getStringArrayListExtra(EXTRA_MISSING_USER_IDS);
|
||||
ArrayList<String> dublicateUserIds = intent
|
||||
.getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS);
|
||||
|
||||
String text = new String();
|
||||
text += "<b>" + getString(R.string.api_select_pub_keys_text) + "</b>";
|
||||
text += "<br/><br/>";
|
||||
if (missingUserIds != null && missingUserIds.size() > 0) {
|
||||
text += getString(R.string.api_select_pub_keys_missing_text);
|
||||
text += "<br/>";
|
||||
text += "<ul>";
|
||||
for (String userId : missingUserIds) {
|
||||
text += "<li>" + userId + "</li>";
|
||||
}
|
||||
text += "</ul>";
|
||||
text += "<br/>";
|
||||
}
|
||||
if (dublicateUserIds != null && dublicateUserIds.size() > 0) {
|
||||
text += getString(R.string.api_select_pub_keys_dublicates_text);
|
||||
text += "<br/>";
|
||||
text += "<ul>";
|
||||
for (String userId : dublicateUserIds) {
|
||||
text += "<li>" + userId + "</li>";
|
||||
}
|
||||
text += "</ul>";
|
||||
}
|
||||
|
||||
// Inflate a "Done"/"Cancel" custom action bar view
|
||||
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// ok
|
||||
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.OKAY;
|
||||
Bundle data = new Bundle();
|
||||
data.putLongArray(
|
||||
OpenPgpService.SelectPubKeysActivityCallback.PUB_KEY_IDS,
|
||||
mSelectFragment.getSelectedMasterKeyIds());
|
||||
msg.setData(data);
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
|
||||
finishHandled = true;
|
||||
finish();
|
||||
}
|
||||
}, R.string.btn_do_not_save, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// cancel
|
||||
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.CANCEL;
|
||||
;
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
|
||||
finishHandled = true;
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(R.layout.api_app_select_pub_keys_activity);
|
||||
|
||||
// set text on view
|
||||
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_select_pub_keys_text);
|
||||
textView.setHtmlFromString(text);
|
||||
|
||||
/* Load select pub keys fragment */
|
||||
// Check that the activity is using the layout version with
|
||||
// the fragment_container FrameLayout
|
||||
if (findViewById(R.id.api_select_pub_keys_fragment_container) != null) {
|
||||
|
||||
// However, if we're being restored from a previous state,
|
||||
// then we don't need to do anything and should return or else
|
||||
// we could end up with overlapping fragments.
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance of the fragment
|
||||
mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds);
|
||||
|
||||
// Add the fragment to the 'fragment_container' FrameLayout
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.api_select_pub_keys_fragment_container, mSelectFragment).commit();
|
||||
}
|
||||
} else if (ACTION_ERROR_MESSAGE.equals(action)) {
|
||||
String errorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE);
|
||||
|
||||
String text = new String();
|
||||
text += "<font color=\"red\">" + errorMessage + "</font>";
|
||||
|
||||
// Inflate a "Done" custom action bar view
|
||||
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_okay,
|
||||
new View.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(R.layout.api_app_error_message);
|
||||
|
||||
// set text on view
|
||||
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_app_error_message_text);
|
||||
textView.setHtmlFromString(text);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Wrong action!");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows passphrase dialog to cache a new passphrase the user enters for using it later for
|
||||
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
|
||||
* for a symmetric passphrase
|
||||
*/
|
||||
private void showPassphraseDialog(long secretKeyId) {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = OpenPgpService.PassphraseActivityCallback.OKAY;
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
} else {
|
||||
Message msg = Message.obtain();
|
||||
msg.arg1 = OpenPgpService.PassphraseActivityCallback.CANCEL;
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "CryptoServiceActivity", e);
|
||||
}
|
||||
}
|
||||
|
||||
finishHandled = true;
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
try {
|
||||
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
|
||||
messenger, secretKeyId);
|
||||
|
||||
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
|
||||
} catch (PgpGeneralException e) {
|
||||
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
|
||||
// send message to handler to start encryption directly
|
||||
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
||||
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
|
||||
import org.sufficientlysecure.keychain.helper.FileHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.LookupUnknownKeyDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ViewFlipper;
|
||||
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class DecryptActivity extends DrawerActivity {
|
||||
|
||||
/* Intents */
|
||||
// without permission
|
||||
public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT";
|
||||
|
||||
/* EXTRA keys for input */
|
||||
public static final String EXTRA_TEXT = "text";
|
||||
|
||||
private long mSignatureKeyId = 0;
|
||||
|
||||
private boolean mReturnResult = false;
|
||||
private boolean mSignedOnly = false;
|
||||
private boolean mAssumeSymmetricEncryption = false;
|
||||
|
||||
private EditText mMessage = null;
|
||||
private LinearLayout mSignatureLayout = null;
|
||||
private ImageView mSignatureStatusImage = null;
|
||||
private TextView mUserId = null;
|
||||
private TextView mUserIdRest = null;
|
||||
|
||||
private ViewFlipper mSource = null;
|
||||
private TextView mSourceLabel = null;
|
||||
private ImageView mSourcePrevious = null;
|
||||
private ImageView mSourceNext = null;
|
||||
|
||||
private boolean mDecryptEnabled = true;
|
||||
private String mDecryptString = "";
|
||||
private boolean mReplyEnabled = true;
|
||||
private String mReplyString = "";
|
||||
|
||||
private int mDecryptTarget;
|
||||
|
||||
private EditText mFilename = null;
|
||||
private CheckBox mDeleteAfter = null;
|
||||
private BootstrapButton mBrowse = null;
|
||||
|
||||
private String mInputFilename = null;
|
||||
private String mOutputFilename = null;
|
||||
|
||||
private Uri mContentUri = null;
|
||||
private boolean mReturnBinary = false;
|
||||
|
||||
private long mUnknownSignatureKeyId = 0;
|
||||
|
||||
private long mSecretKeyId = Id.key.none;
|
||||
|
||||
private FileDialogFragment mFileDialog;
|
||||
|
||||
private boolean mLookupUnknownKey = true;
|
||||
|
||||
private boolean mDecryptImmediately = false;
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
|
||||
if (mDecryptEnabled) {
|
||||
menu.add(1, Id.menu.option.decrypt, 0, mDecryptString).setShowAsAction(
|
||||
MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
|
||||
}
|
||||
if (mReplyEnabled) {
|
||||
menu.add(1, Id.menu.option.reply, 1, mReplyString).setShowAsAction(
|
||||
MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
|
||||
case Id.menu.option.decrypt: {
|
||||
decryptClicked();
|
||||
|
||||
return true;
|
||||
}
|
||||
case Id.menu.option.reply: {
|
||||
replyClicked();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
mSource = (ViewFlipper) findViewById(R.id.source);
|
||||
mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
|
||||
mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
|
||||
mSourceNext = (ImageView) findViewById(R.id.sourceNext);
|
||||
|
||||
mSourcePrevious.setClickable(true);
|
||||
mSourcePrevious.setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
|
||||
R.anim.push_right_in));
|
||||
mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
|
||||
R.anim.push_right_out));
|
||||
mSource.showPrevious();
|
||||
updateSource();
|
||||
}
|
||||
});
|
||||
|
||||
mSourceNext.setClickable(true);
|
||||
OnClickListener nextSourceClickListener = new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
|
||||
R.anim.push_left_in));
|
||||
mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
|
||||
R.anim.push_left_out));
|
||||
mSource.showNext();
|
||||
updateSource();
|
||||
}
|
||||
};
|
||||
mSourceNext.setOnClickListener(nextSourceClickListener);
|
||||
|
||||
mSourceLabel.setClickable(true);
|
||||
mSourceLabel.setOnClickListener(nextSourceClickListener);
|
||||
|
||||
mMessage = (EditText) findViewById(R.id.message);
|
||||
mSignatureLayout = (LinearLayout) findViewById(R.id.signature);
|
||||
mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status);
|
||||
mUserId = (TextView) findViewById(R.id.mainUserId);
|
||||
mUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
|
||||
|
||||
// measure the height of the source_file view and set the message view's min height to that,
|
||||
// so it fills mSource fully... bit of a hack.
|
||||
View tmp = findViewById(R.id.sourceFile);
|
||||
tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
int height = tmp.getMeasuredHeight();
|
||||
mMessage.setMinimumHeight(height);
|
||||
|
||||
mFilename = (EditText) findViewById(R.id.filename);
|
||||
mBrowse = (BootstrapButton) findViewById(R.id.btn_browse);
|
||||
mBrowse.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
FileHelper.openFile(DecryptActivity.this, mFilename.getText().toString(), "*/*",
|
||||
Id.request.filename);
|
||||
}
|
||||
});
|
||||
|
||||
mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption);
|
||||
|
||||
// default: message source
|
||||
mSource.setInAnimation(null);
|
||||
mSource.setOutAnimation(null);
|
||||
while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
|
||||
mSource.showNext();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.decrypt_activity);
|
||||
|
||||
// set actionbar without home button if called from another app
|
||||
ActionBarHelper.setBackButton(this);
|
||||
|
||||
initView();
|
||||
|
||||
setupDrawerNavigation(savedInstanceState);
|
||||
|
||||
// Handle intent actions
|
||||
handleActions(getIntent());
|
||||
|
||||
if (mSource.getCurrentView().getId() == R.id.sourceMessage
|
||||
&& mMessage.getText().length() == 0) {
|
||||
|
||||
CharSequence clipboardText = ClipboardReflection.getClipboardText(this);
|
||||
|
||||
String data = "";
|
||||
if (clipboardText != null) {
|
||||
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(clipboardText);
|
||||
if (!matcher.matches()) {
|
||||
matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(clipboardText);
|
||||
}
|
||||
if (matcher.matches()) {
|
||||
data = matcher.group(1);
|
||||
mMessage.setText(data);
|
||||
Toast.makeText(this, R.string.using_clipboard_content, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mSignatureLayout.setVisibility(View.GONE);
|
||||
mSignatureLayout.setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
if (mSignatureKeyId == 0) {
|
||||
return;
|
||||
}
|
||||
PGPPublicKeyRing key = ProviderHelper.getPGPPublicKeyRingByKeyId(
|
||||
DecryptActivity.this, mSignatureKeyId);
|
||||
if (key != null) {
|
||||
Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
|
||||
intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
|
||||
intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, mSignatureKeyId);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mReplyEnabled = false;
|
||||
|
||||
// build new actionbar
|
||||
invalidateOptionsMenu();
|
||||
|
||||
if (mReturnResult) {
|
||||
mSourcePrevious.setClickable(false);
|
||||
mSourcePrevious.setEnabled(false);
|
||||
mSourcePrevious.setVisibility(View.INVISIBLE);
|
||||
|
||||
mSourceNext.setClickable(false);
|
||||
mSourceNext.setEnabled(false);
|
||||
mSourceNext.setVisibility(View.INVISIBLE);
|
||||
|
||||
mSourceLabel.setClickable(false);
|
||||
mSourceLabel.setEnabled(false);
|
||||
}
|
||||
|
||||
updateSource();
|
||||
|
||||
if (mDecryptImmediately
|
||||
|| (mSource.getCurrentView().getId() == R.id.sourceMessage && (mMessage.getText()
|
||||
.length() > 0 || mContentUri != null))) {
|
||||
decryptClicked();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all actions with this intent
|
||||
*
|
||||
* @param intent
|
||||
*/
|
||||
private void handleActions(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Bundle extras = intent.getExtras();
|
||||
String type = intent.getType();
|
||||
Uri uri = intent.getData();
|
||||
|
||||
if (extras == null) {
|
||||
extras = new Bundle();
|
||||
}
|
||||
|
||||
/*
|
||||
* Android's Action
|
||||
*/
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||
// When sending to Keychain Encrypt via share menu
|
||||
if ("text/plain".equals(type)) {
|
||||
// Plain text
|
||||
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
if (sharedText != null) {
|
||||
// handle like normal text decryption, override action and extras to later
|
||||
// execute ACTION_DECRYPT in main actions
|
||||
extras.putString(EXTRA_TEXT, sharedText);
|
||||
action = ACTION_DECRYPT;
|
||||
}
|
||||
} else {
|
||||
// Binary via content provider (could also be files)
|
||||
// override uri to get stream from send
|
||||
uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
action = ACTION_DECRYPT;
|
||||
}
|
||||
} else if (Intent.ACTION_VIEW.equals(action)) {
|
||||
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
|
||||
|
||||
// override action
|
||||
action = ACTION_DECRYPT;
|
||||
}
|
||||
|
||||
String textData = extras.getString(EXTRA_TEXT);
|
||||
|
||||
/**
|
||||
* Main Actions
|
||||
*/
|
||||
if (ACTION_DECRYPT.equals(action) && textData != null) {
|
||||
Log.d(Constants.TAG, "textData null, matching text ...");
|
||||
Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(textData);
|
||||
if (matcher.matches()) {
|
||||
Log.d(Constants.TAG, "PGP_MESSAGE matched");
|
||||
textData = matcher.group(1);
|
||||
// replace non breakable spaces
|
||||
textData = textData.replaceAll("\\xa0", " ");
|
||||
mMessage.setText(textData);
|
||||
} else {
|
||||
matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(textData);
|
||||
if (matcher.matches()) {
|
||||
Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
|
||||
textData = matcher.group(1);
|
||||
// replace non breakable spaces
|
||||
textData = textData.replaceAll("\\xa0", " ");
|
||||
mMessage.setText(textData);
|
||||
|
||||
mDecryptString = getString(R.string.btn_verify);
|
||||
// build new action bar
|
||||
invalidateOptionsMenu();
|
||||
} else {
|
||||
Log.d(Constants.TAG, "Nothing matched!");
|
||||
}
|
||||
}
|
||||
} else if (ACTION_DECRYPT.equals(action) && uri != null) {
|
||||
// get file path from uri
|
||||
String path = FileHelper.getPath(this, uri);
|
||||
|
||||
if (path != null) {
|
||||
mInputFilename = path;
|
||||
mFilename.setText(mInputFilename);
|
||||
guessOutputFilename();
|
||||
mSource.setInAnimation(null);
|
||||
mSource.setOutAnimation(null);
|
||||
while (mSource.getCurrentView().getId() != R.id.sourceFile) {
|
||||
mSource.showNext();
|
||||
}
|
||||
} else {
|
||||
Log.e(Constants.TAG,
|
||||
"Direct binary data without actual file in filesystem is not supported. Please use the Remote Service API!");
|
||||
Toast.makeText(this, R.string.error_only_files_are_supported, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
// end activity
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
Log.e(Constants.TAG,
|
||||
"Include the extra 'text' or an Uri with setData() in your Intent!");
|
||||
}
|
||||
}
|
||||
|
||||
private void guessOutputFilename() {
|
||||
mInputFilename = mFilename.getText().toString();
|
||||
File file = new File(mInputFilename);
|
||||
String filename = file.getName();
|
||||
if (filename.endsWith(".asc") || filename.endsWith(".gpg") || filename.endsWith(".pgp")) {
|
||||
filename = filename.substring(0, filename.length() - 4);
|
||||
}
|
||||
mOutputFilename = Constants.path.APP_DIR + "/" + filename;
|
||||
}
|
||||
|
||||
private void updateSource() {
|
||||
switch (mSource.getCurrentView().getId()) {
|
||||
case R.id.sourceFile: {
|
||||
mSourceLabel.setText(R.string.label_file);
|
||||
mDecryptString = getString(R.string.btn_decrypt);
|
||||
|
||||
// build new action bar
|
||||
invalidateOptionsMenu();
|
||||
break;
|
||||
}
|
||||
|
||||
case R.id.sourceMessage: {
|
||||
mSourceLabel.setText(R.string.label_message);
|
||||
mDecryptString = getString(R.string.btn_decrypt);
|
||||
|
||||
// build new action bar
|
||||
invalidateOptionsMenu();
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void decryptClicked() {
|
||||
if (mSource.getCurrentView().getId() == R.id.sourceFile) {
|
||||
mDecryptTarget = Id.target.file;
|
||||
} else {
|
||||
mDecryptTarget = Id.target.message;
|
||||
}
|
||||
initiateDecryption();
|
||||
}
|
||||
|
||||
private void initiateDecryption() {
|
||||
if (mDecryptTarget == Id.target.file) {
|
||||
String currentFilename = mFilename.getText().toString();
|
||||
if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
|
||||
guessOutputFilename();
|
||||
}
|
||||
|
||||
if (mInputFilename.equals("")) {
|
||||
Toast.makeText(this, R.string.no_file_selected, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mInputFilename.startsWith("file")) {
|
||||
File file = new File(mInputFilename);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.error_message,
|
||||
getString(R.string.error_file_not_found)), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mDecryptTarget == Id.target.message) {
|
||||
String messageData = mMessage.getText().toString();
|
||||
Matcher matcher = PgpHelper.PGP_SIGNED_MESSAGE.matcher(messageData);
|
||||
if (matcher.matches()) {
|
||||
mSignedOnly = true;
|
||||
decryptStart();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// else treat it as an decrypted message/file
|
||||
mSignedOnly = false;
|
||||
|
||||
getDecryptionKeyFromInputStream();
|
||||
|
||||
// if we need a symmetric passphrase or a passphrase to use a secret key ask for it
|
||||
if (mSecretKeyId == Id.key.symmetric
|
||||
|| PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) {
|
||||
showPassphraseDialog();
|
||||
} else {
|
||||
if (mDecryptTarget == Id.target.file) {
|
||||
askForOutputFilename();
|
||||
} else {
|
||||
decryptStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows passphrase dialog to cache a new passphrase the user enters for using it later for
|
||||
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
|
||||
* for a symmetric passphrase
|
||||
*/
|
||||
private void showPassphraseDialog() {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
|
||||
if (mDecryptTarget == Id.target.file) {
|
||||
askForOutputFilename();
|
||||
} else {
|
||||
decryptStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
try {
|
||||
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
|
||||
messenger, mSecretKeyId);
|
||||
|
||||
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
|
||||
} catch (PgpGeneralException e) {
|
||||
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
|
||||
// send message to handler to start encryption directly
|
||||
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Rework function, remove global variables
|
||||
*/
|
||||
private void getDecryptionKeyFromInputStream() {
|
||||
InputStream inStream = null;
|
||||
if (mContentUri != null) {
|
||||
try {
|
||||
inStream = getContentResolver().openInputStream(mContentUri);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(Constants.TAG, "File not found!", e);
|
||||
Toast.makeText(this, getString(R.string.error_file_not_found, e.getMessage()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (mDecryptTarget == Id.target.file) {
|
||||
// check if storage is ready
|
||||
if (!FileHelper.isStorageMounted(mInputFilename)) {
|
||||
Toast.makeText(this, getString(R.string.error_external_storage_not_ready),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
inStream = new BufferedInputStream(new FileInputStream(mInputFilename));
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(Constants.TAG, "File not found!", e);
|
||||
Toast.makeText(this, getString(R.string.error_file_not_found, e.getMessage()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
inStream = new ByteArrayInputStream(mMessage.getText().toString().getBytes());
|
||||
}
|
||||
|
||||
// get decryption key for this inStream
|
||||
try {
|
||||
try {
|
||||
if (inStream.markSupported()) {
|
||||
inStream.mark(200); // should probably set this to the max size of two pgpF
|
||||
// objects, if it even needs to be anything other than 0.
|
||||
}
|
||||
mSecretKeyId = PgpHelper.getDecryptionKeyId(this, inStream);
|
||||
if (mSecretKeyId == Id.key.none) {
|
||||
throw new PgpGeneralException(getString(R.string.error_no_secret_key_found));
|
||||
}
|
||||
mAssumeSymmetricEncryption = false;
|
||||
} catch (NoAsymmetricEncryptionException e) {
|
||||
if (inStream.markSupported()) {
|
||||
inStream.reset();
|
||||
}
|
||||
mSecretKeyId = Id.key.symmetric;
|
||||
if (!PgpOperation.hasSymmetricEncryption(this, inStream)) {
|
||||
throw new PgpGeneralException(
|
||||
getString(R.string.error_no_known_encryption_found));
|
||||
}
|
||||
mAssumeSymmetricEncryption = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, getString(R.string.error_message, e.getMessage()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void replyClicked() {
|
||||
Intent intent = new Intent(this, EncryptActivity.class);
|
||||
intent.setAction(EncryptActivity.ACTION_ENCRYPT);
|
||||
String data = mMessage.getText().toString();
|
||||
data = data.replaceAll("(?m)^", "> ");
|
||||
data = "\n\n" + data;
|
||||
intent.putExtra(EncryptActivity.EXTRA_TEXT, data);
|
||||
intent.putExtra(EncryptActivity.EXTRA_SIGNATURE_KEY_ID, mSecretKeyId);
|
||||
intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId });
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void askForOutputFilename() {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == FileDialogFragment.MESSAGE_OKAY) {
|
||||
Bundle data = message.getData();
|
||||
mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
|
||||
decryptStart();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
mFileDialog = FileDialogFragment.newInstance(messenger,
|
||||
getString(R.string.title_decrypt_to_file),
|
||||
getString(R.string.specify_file_to_decrypt_to), mOutputFilename, null);
|
||||
|
||||
mFileDialog.show(getSupportFragmentManager(), "fileDialog");
|
||||
}
|
||||
|
||||
private void lookupUnknownKey(long unknownKeyId) {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_OKAY) {
|
||||
// the result is handled by onActivityResult() as LookupUnknownKeyDialogFragment
|
||||
// starts a new Intent which then returns data
|
||||
} else if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_CANCEL) {
|
||||
// decrypt again, but don't lookup unknown keys!
|
||||
mLookupUnknownKey = false;
|
||||
decryptStart();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
LookupUnknownKeyDialogFragment lookupKeyDialog = LookupUnknownKeyDialogFragment
|
||||
.newInstance(messenger, unknownKeyId);
|
||||
|
||||
lookupKeyDialog.show(getSupportFragmentManager(), "unknownKeyDialog");
|
||||
}
|
||||
|
||||
private void decryptStart() {
|
||||
Log.d(Constants.TAG, "decryptStart");
|
||||
|
||||
// Send all information needed to service to decrypt in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY);
|
||||
|
||||
// choose action based on input: decrypt stream, file or bytes
|
||||
if (mContentUri != null) {
|
||||
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM);
|
||||
|
||||
data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mContentUri);
|
||||
} else if (mDecryptTarget == Id.target.file) {
|
||||
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
|
||||
|
||||
Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
|
||||
+ mOutputFilename);
|
||||
|
||||
data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
|
||||
data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
|
||||
} else {
|
||||
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
|
||||
|
||||
String message = mMessage.getText().toString();
|
||||
data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, message.getBytes());
|
||||
}
|
||||
|
||||
data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId);
|
||||
|
||||
data.putBoolean(KeychainIntentService.DECRYPT_SIGNED_ONLY, mSignedOnly);
|
||||
data.putBoolean(KeychainIntentService.DECRYPT_LOOKUP_UNKNOWN_KEY, mLookupUnknownKey);
|
||||
data.putBoolean(KeychainIntentService.DECRYPT_RETURN_BYTES, mReturnBinary);
|
||||
data.putBoolean(KeychainIntentService.DECRYPT_ASSUME_SYMMETRIC, mAssumeSymmetricEncryption);
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after encrypting is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_decrypting, ProgressDialog.STYLE_HORIZONTAL) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
// get returned data bundle
|
||||
Bundle returnData = message.getData();
|
||||
|
||||
// if key is unknown show lookup dialog
|
||||
if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY)
|
||||
&& mLookupUnknownKey) {
|
||||
mUnknownSignatureKeyId = returnData
|
||||
.getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
|
||||
lookupUnknownKey(mUnknownSignatureKeyId);
|
||||
return;
|
||||
}
|
||||
|
||||
mSignatureKeyId = 0;
|
||||
mSignatureLayout.setVisibility(View.GONE);
|
||||
mReplyEnabled = false;
|
||||
|
||||
// build new action bar
|
||||
invalidateOptionsMenu();
|
||||
|
||||
Toast.makeText(DecryptActivity.this, R.string.decryption_successful,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
if (mReturnResult) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtras(returnData);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mDecryptTarget) {
|
||||
case Id.target.message:
|
||||
String decryptedMessage = returnData
|
||||
.getString(KeychainIntentService.RESULT_DECRYPTED_STRING);
|
||||
mMessage.setText(decryptedMessage);
|
||||
mMessage.setHorizontallyScrolling(false);
|
||||
mReplyEnabled = false;
|
||||
|
||||
// build new action bar
|
||||
invalidateOptionsMenu();
|
||||
break;
|
||||
|
||||
case Id.target.file:
|
||||
if (mDeleteAfter.isChecked()) {
|
||||
// Create and show dialog to delete original file
|
||||
DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
|
||||
.newInstance(mInputFilename);
|
||||
deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// shouldn't happen
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE)) {
|
||||
String userId = returnData
|
||||
.getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
|
||||
mSignatureKeyId = returnData
|
||||
.getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
|
||||
mUserIdRest.setText("id: "
|
||||
+ PgpKeyHelper.convertKeyIdToHex(mSignatureKeyId));
|
||||
if (userId == null) {
|
||||
userId = getResources().getString(R.string.unknown_user_id);
|
||||
}
|
||||
String chunks[] = userId.split(" <", 2);
|
||||
userId = chunks[0];
|
||||
if (chunks.length > 1) {
|
||||
mUserIdRest.setText("<" + chunks[1]);
|
||||
}
|
||||
mUserId.setText(userId);
|
||||
|
||||
if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS)) {
|
||||
mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
|
||||
} else if (returnData
|
||||
.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN)) {
|
||||
mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
|
||||
Toast.makeText(DecryptActivity.this,
|
||||
R.string.unknown_signature_key_touch_to_look_up,
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
|
||||
}
|
||||
mSignatureLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case Id.request.filename: {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
try {
|
||||
String path = FileHelper.getPath(this, data.getData());
|
||||
Log.d(Constants.TAG, "path=" + path);
|
||||
|
||||
mFilename.setText(path);
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(Constants.TAG, "Nullpointer while retrieving path!");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// this request is returned after LookupUnknownKeyDialogFragment started
|
||||
// KeyServerQueryActivity and user looked uo key
|
||||
case Id.request.look_up_key_id: {
|
||||
Log.d(Constants.TAG, "Returning from Lookup Key...");
|
||||
// decrypt again without lookup
|
||||
mLookupUnknownKey = false;
|
||||
decryptStart();
|
||||
return;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.service.remote.RegisteredAppsListActivity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ActionBarDrawerToggle;
|
||||
import android.support.v4.view.GravityCompat;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
import android.view.ActionProvider;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import com.beardedhen.androidbootstrap.FontAwesomeText;
|
||||
|
||||
/**
|
||||
* some fundamental ideas from https://github.com/tobykurien/SherlockNavigationDrawer
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class DrawerActivity extends SherlockFragmentActivity {
|
||||
private DrawerLayout mDrawerLayout;
|
||||
private ListView mDrawerList;
|
||||
private ActionBarDrawerToggle mDrawerToggle;
|
||||
|
||||
private CharSequence mDrawerTitle;
|
||||
private CharSequence mTitle;
|
||||
|
||||
private static Class[] mItemsClass = new Class[] { KeyListPublicActivity.class,
|
||||
EncryptActivity.class, DecryptActivity.class, ImportKeysActivity.class,
|
||||
KeyListSecretActivity.class, RegisteredAppsListActivity.class };
|
||||
|
||||
private static final int MENU_ID_PREFERENCE = 222;
|
||||
private static final int MENU_ID_HELP = 223;
|
||||
|
||||
protected void setupDrawerNavigation(Bundle savedInstanceState) {
|
||||
mDrawerTitle = getString(R.string.app_name);
|
||||
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
|
||||
mDrawerList = (ListView) findViewById(R.id.left_drawer);
|
||||
|
||||
// set a custom shadow that overlays the main content when the drawer
|
||||
// opens
|
||||
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
|
||||
|
||||
NavItem mItemIconTexts[] = new NavItem[] {
|
||||
new NavItem("fa-user", getString(R.string.nav_contacts)),
|
||||
new NavItem("fa-lock", getString(R.string.nav_encrypt)),
|
||||
new NavItem("fa-unlock", getString(R.string.nav_decrypt)),
|
||||
new NavItem("fa-download", getString(R.string.nav_import)),
|
||||
new NavItem("fa-key", getString(R.string.nav_secret_keys)),
|
||||
new NavItem("fa-android", getString(R.string.nav_apps)) };
|
||||
|
||||
mDrawerList.setAdapter(new NavigationDrawerAdapter(this, R.layout.drawer_list_item,
|
||||
mItemIconTexts));
|
||||
|
||||
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
|
||||
|
||||
// enable ActionBar app icon to behave as action to toggle nav drawer
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setHomeButtonEnabled(true);
|
||||
|
||||
// ActionBarDrawerToggle ties together the the proper interactions
|
||||
// between the sliding drawer and the action bar app icon
|
||||
mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */
|
||||
mDrawerLayout, /* DrawerLayout object */
|
||||
R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */
|
||||
R.string.drawer_open, /* "open drawer" description for accessibility */
|
||||
R.string.drawer_close /* "close drawer" description for accessibility */
|
||||
) {
|
||||
public void onDrawerClosed(View view) {
|
||||
getSupportActionBar().setTitle(mTitle);
|
||||
// creates call to onPrepareOptionsMenu()
|
||||
supportInvalidateOptionsMenu();
|
||||
}
|
||||
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
mTitle = getSupportActionBar().getTitle();
|
||||
getSupportActionBar().setTitle(mDrawerTitle);
|
||||
// creates call to onPrepareOptionsMenu()
|
||||
supportInvalidateOptionsMenu();
|
||||
}
|
||||
};
|
||||
mDrawerLayout.setDrawerListener(mDrawerToggle);
|
||||
|
||||
// if (savedInstanceState == null) {
|
||||
// selectItem(0);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(42, MENU_ID_PREFERENCE, 100, R.string.menu_preferences);
|
||||
menu.add(42, MENU_ID_HELP, 101, R.string.menu_help);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
/* Called whenever we call invalidateOptionsMenu() */
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
// If the nav drawer is open, hide action items related to the content
|
||||
// view
|
||||
boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
|
||||
// menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
// The action bar home/up action should open or close the drawer.
|
||||
// ActionBarDrawerToggle will take care of this.
|
||||
if (mDrawerToggle.onOptionsItemSelected(getMenuItem(item))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case MENU_ID_PREFERENCE: {
|
||||
Intent intent = new Intent(this, PreferencesActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case MENU_ID_HELP: {
|
||||
Intent intent = new Intent(this, HelpActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
// Handle action buttons
|
||||
// switch (item.getItemId()) {
|
||||
// case R.id.action_websearch:
|
||||
// // create intent to perform web search for this planet
|
||||
// Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
|
||||
// intent.putExtra(SearchManager.QUERY, getSupportActionBar().getTitle());
|
||||
// // catch event that there's no activity to handle intent
|
||||
// if (intent.resolveActivity(getPackageManager()) != null) {
|
||||
// startActivity(intent);
|
||||
// } else {
|
||||
// Toast.makeText(this, R.string.app_not_available, Toast.LENGTH_LONG).show();
|
||||
// }
|
||||
// return true;
|
||||
// default:
|
||||
// return super.onOptionsItemSelected(item);
|
||||
// }
|
||||
}
|
||||
|
||||
private android.view.MenuItem getMenuItem(final MenuItem item) {
|
||||
return new android.view.MenuItem() {
|
||||
@Override
|
||||
public int getItemId() {
|
||||
return item.getItemId();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collapseActionView() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean expandActionView() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionProvider getActionProvider() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getActionView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getAlphabeticShortcut() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGroupId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getIcon() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getIntent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContextMenuInfo getMenuInfo() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getNumericShortcut() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubMenu getSubMenu() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitleCondensed() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSubMenu() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActionViewExpanded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCheckable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setActionProvider(ActionProvider actionProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setActionView(View view) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setActionView(int resId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setAlphabeticShortcut(char alphaChar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setCheckable(boolean checkable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setChecked(boolean checked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setEnabled(boolean enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setIcon(Drawable icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setIcon(int iconRes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setIntent(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setNumericShortcut(char numericChar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setOnMenuItemClickListener(
|
||||
OnMenuItemClickListener menuItemClickListener) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setShortcut(char numericChar, char alphaChar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShowAsAction(int actionEnum) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setShowAsActionFlags(int actionEnum) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setTitle(CharSequence title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setTitle(int title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setTitleCondensed(CharSequence title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.view.MenuItem setVisible(boolean visible) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* The click listener for ListView in the navigation drawer */
|
||||
private class DrawerItemClickListener implements ListView.OnItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
selectItem(position);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectItem(int position) {
|
||||
// update selected item and title, then close the drawer
|
||||
mDrawerList.setItemChecked(position, true);
|
||||
// setTitle(mDrawerTitles[position]);
|
||||
mDrawerLayout.closeDrawer(mDrawerList);
|
||||
|
||||
finish();
|
||||
overridePendingTransition(0, 0);
|
||||
|
||||
Intent intent = new Intent(this, mItemsClass[position]);
|
||||
startActivity(intent);
|
||||
// disable animation of activity start
|
||||
overridePendingTransition(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* When using the ActionBarDrawerToggle, you must call it during onPostCreate() and
|
||||
* onConfigurationChanged()...
|
||||
*/
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
// Sync the toggle state after onRestoreInstanceState has occurred.
|
||||
mDrawerToggle.syncState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
// Pass any configuration change to the drawer toggles
|
||||
mDrawerToggle.onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
private class NavItem {
|
||||
public String icon;
|
||||
public String title;
|
||||
|
||||
public NavItem(String icon, String title) {
|
||||
super();
|
||||
this.icon = icon;
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
private class NavigationDrawerAdapter extends ArrayAdapter<NavItem> {
|
||||
Context context;
|
||||
int layoutResourceId;
|
||||
NavItem data[] = null;
|
||||
|
||||
public NavigationDrawerAdapter(Context context, int layoutResourceId, NavItem[] data) {
|
||||
super(context, layoutResourceId, data);
|
||||
this.layoutResourceId = layoutResourceId;
|
||||
this.context = context;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View row = convertView;
|
||||
NavItemHolder holder = null;
|
||||
|
||||
if (row == null) {
|
||||
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
|
||||
row = inflater.inflate(layoutResourceId, parent, false);
|
||||
|
||||
holder = new NavItemHolder();
|
||||
holder.img = (FontAwesomeText) row.findViewById(R.id.drawer_item_icon);
|
||||
holder.txtTitle = (TextView) row.findViewById(R.id.drawer_item_text);
|
||||
|
||||
row.setTag(holder);
|
||||
} else {
|
||||
holder = (NavItemHolder) row.getTag();
|
||||
}
|
||||
|
||||
NavItem item = data[position];
|
||||
holder.txtTitle.setText(item.title);
|
||||
holder.img.setIcon(item.icon);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NavItemHolder {
|
||||
FontAwesomeText img;
|
||||
TextView txtTitle;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,666 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
|
||||
import org.sufficientlysecure.keychain.helper.ExportHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.widget.KeyEditor;
|
||||
import org.sufficientlysecure.keychain.ui.widget.SectionView;
|
||||
import org.sufficientlysecure.keychain.ui.widget.UserIdEditor;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class EditKeyActivity extends SherlockFragmentActivity {
|
||||
|
||||
// Actions for internal use only:
|
||||
public static final String ACTION_CREATE_KEY = Constants.INTENT_PREFIX + "CREATE_KEY";
|
||||
public static final String ACTION_EDIT_KEY = Constants.INTENT_PREFIX + "EDIT_KEY";
|
||||
|
||||
// possible extra keys
|
||||
public static final String EXTRA_USER_IDS = "user_ids";
|
||||
public static final String EXTRA_NO_PASSPHRASE = "no_passphrase";
|
||||
public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generate_default_keys";
|
||||
|
||||
// results when saving key
|
||||
public static final String RESULT_EXTRA_MASTER_KEY_ID = "master_key_id";
|
||||
public static final String RESULT_EXTRA_USER_ID = "user_id";
|
||||
|
||||
// EDIT
|
||||
private Uri mDataUri;
|
||||
|
||||
private PGPSecretKeyRing mKeyRing = null;
|
||||
|
||||
private SectionView mUserIdsView;
|
||||
private SectionView mKeysView;
|
||||
|
||||
private String mCurrentPassPhrase = null;
|
||||
private String mNewPassPhrase = null;
|
||||
|
||||
private BootstrapButton mChangePassPhrase;
|
||||
|
||||
private CheckBox mNoPassphrase;
|
||||
|
||||
Vector<String> mUserIds;
|
||||
Vector<PGPSecretKey> mKeys;
|
||||
Vector<Integer> mKeysUsages;
|
||||
boolean masterCanSign = true;
|
||||
|
||||
ExportHelper mExportHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mExportHelper = new ExportHelper(this);
|
||||
|
||||
mUserIds = new Vector<String>();
|
||||
mKeys = new Vector<PGPSecretKey>();
|
||||
mKeysUsages = new Vector<Integer>();
|
||||
|
||||
// Catch Intents opened from other apps
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
if (ACTION_CREATE_KEY.equals(action)) {
|
||||
handleActionCreateKey(intent);
|
||||
} else if (ACTION_EDIT_KEY.equals(action)) {
|
||||
handleActionEditKey(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle intent action to create new key
|
||||
*
|
||||
* @param intent
|
||||
*/
|
||||
private void handleActionCreateKey(Intent intent) {
|
||||
// Inflate a "Done"/"Cancel" custom action bar
|
||||
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_save,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveClicked();
|
||||
}
|
||||
}, R.string.btn_do_not_save, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cancelClicked();
|
||||
}
|
||||
});
|
||||
|
||||
Bundle extras = intent.getExtras();
|
||||
|
||||
mCurrentPassPhrase = "";
|
||||
|
||||
if (extras != null) {
|
||||
// if userId is given, prefill the fields
|
||||
if (extras.containsKey(EXTRA_USER_IDS)) {
|
||||
Log.d(Constants.TAG, "UserIds are given!");
|
||||
mUserIds.add(extras.getString(EXTRA_USER_IDS));
|
||||
}
|
||||
|
||||
// if no passphrase is given
|
||||
if (extras.containsKey(EXTRA_NO_PASSPHRASE)) {
|
||||
boolean noPassphrase = extras.getBoolean(EXTRA_NO_PASSPHRASE);
|
||||
if (noPassphrase) {
|
||||
// check "no passphrase" checkbox and remove button
|
||||
mNoPassphrase.setChecked(true);
|
||||
mChangePassPhrase.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// generate key
|
||||
if (extras.containsKey(EXTRA_GENERATE_DEFAULT_KEYS)) {
|
||||
boolean generateDefaultKeys = extras.getBoolean(EXTRA_GENERATE_DEFAULT_KEYS);
|
||||
if (generateDefaultKeys) {
|
||||
|
||||
// Send all information needed to service generate keys in other thread
|
||||
Intent serviceIntent = new Intent(this, KeychainIntentService.class);
|
||||
serviceIntent.setAction(KeychainIntentService.ACTION_GENERATE_DEFAULT_RSA_KEYS);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE,
|
||||
mCurrentPassPhrase);
|
||||
|
||||
serviceIntent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after generating is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
|
||||
this, R.string.progress_generating, ProgressDialog.STYLE_SPINNER) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
// get new key from data bundle returned from service
|
||||
Bundle data = message.getData();
|
||||
PGPSecretKeyRing masterKeyRing = (PGPSecretKeyRing) PgpConversionHelper
|
||||
.BytesToPGPKeyRing(data
|
||||
.getByteArray(KeychainIntentService.RESULT_NEW_KEY));
|
||||
PGPSecretKeyRing subKeyRing = (PGPSecretKeyRing) PgpConversionHelper
|
||||
.BytesToPGPKeyRing(data
|
||||
.getByteArray(KeychainIntentService.RESULT_NEW_KEY2));
|
||||
|
||||
// add master key
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSecretKey> masterIt = masterKeyRing.getSecretKeys();
|
||||
mKeys.add(masterIt.next());
|
||||
mKeysUsages.add(Id.choice.usage.sign_only);
|
||||
|
||||
// add sub key
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSecretKey> subIt = subKeyRing.getSecretKeys();
|
||||
subIt.next(); // masterkey
|
||||
mKeys.add(subIt.next());
|
||||
mKeysUsages.add(Id.choice.usage.encrypt_only);
|
||||
|
||||
buildLayout();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
serviceIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(serviceIntent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buildLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle intent action to edit existing key
|
||||
*
|
||||
* @param intent
|
||||
*/
|
||||
private void handleActionEditKey(Intent intent) {
|
||||
// Inflate a "Done"/"Cancel" custom action bar
|
||||
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_save,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveClicked();
|
||||
}
|
||||
});
|
||||
|
||||
mDataUri = intent.getData();
|
||||
if (mDataUri == null) {
|
||||
Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
|
||||
finish();
|
||||
return;
|
||||
} else {
|
||||
Log.d(Constants.TAG, "uri: " + mDataUri);
|
||||
|
||||
long keyRingRowId = Long.valueOf(mDataUri.getLastPathSegment());
|
||||
|
||||
// get master key id using row id
|
||||
long masterKeyId = ProviderHelper.getSecretMasterKeyId(this, keyRingRowId);
|
||||
|
||||
boolean masterCanSign = ProviderHelper.getSecretMasterKeyCanSign(this, keyRingRowId);
|
||||
|
||||
String passphrase = PassphraseCacheService.getCachedPassphrase(this, masterKeyId);
|
||||
if (passphrase == null) {
|
||||
showPassphraseDialog(masterKeyId, masterCanSign);
|
||||
} else {
|
||||
// PgpMain.setEditPassPhrase(passPhrase);
|
||||
mCurrentPassPhrase = passphrase;
|
||||
|
||||
finallyEdit(masterKeyId, masterCanSign);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showPassphraseDialog(final long masterKeyId, final boolean masterCanSign) {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
|
||||
String passPhrase = PassphraseCacheService.getCachedPassphrase(
|
||||
EditKeyActivity.this, masterKeyId);
|
||||
mCurrentPassPhrase = passPhrase;
|
||||
finallyEdit(masterKeyId, masterCanSign);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
try {
|
||||
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(
|
||||
EditKeyActivity.this, messenger, masterKeyId);
|
||||
|
||||
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
|
||||
} catch (PgpGeneralException e) {
|
||||
Log.d(Constants.TAG, "No passphrase for this secret key!");
|
||||
// send message to handler to start encryption directly
|
||||
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
// show menu only on edit
|
||||
if (mDataUri != null) {
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getSupportMenuInflater().inflate(R.menu.key_edit, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_key_edit_cancel:
|
||||
cancelClicked();
|
||||
return true;
|
||||
case R.id.menu_key_edit_export_file:
|
||||
mExportHelper.showExportKeysDialog(mDataUri, Id.type.secret_key, Constants.path.APP_DIR
|
||||
+ "/secexport.asc");
|
||||
return true;
|
||||
case R.id.menu_key_edit_delete: {
|
||||
// Message is received after key is deleted
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mExportHelper.deleteKey(mDataUri, Id.type.secret_key, returnHandler);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void finallyEdit(final long masterKeyId, final boolean masterCanSign) {
|
||||
if (masterKeyId != 0) {
|
||||
PGPSecretKey masterKey = null;
|
||||
mKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, masterKeyId);
|
||||
if (mKeyRing != null) {
|
||||
masterKey = PgpKeyHelper.getMasterKey(mKeyRing);
|
||||
for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(mKeyRing.getSecretKeys())) {
|
||||
mKeys.add(key);
|
||||
mKeysUsages.add(-1); // get usage when view is created
|
||||
}
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Keyring not found with masterKeyId: " + masterKeyId);
|
||||
Toast.makeText(this, R.string.error_no_secret_key_found, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
if (masterKey != null) {
|
||||
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
|
||||
Log.d(Constants.TAG, "Added userId " + userId);
|
||||
mUserIds.add(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: ???
|
||||
if (mCurrentPassPhrase == null) {
|
||||
mCurrentPassPhrase = "";
|
||||
}
|
||||
|
||||
buildLayout();
|
||||
|
||||
if (mCurrentPassPhrase.equals("")) {
|
||||
// check "no passphrase" checkbox and remove button
|
||||
mNoPassphrase.setChecked(true);
|
||||
mChangePassPhrase.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the dialog to set a new passphrase
|
||||
*/
|
||||
private void showSetPassphraseDialog() {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) {
|
||||
Bundle data = message.getData();
|
||||
|
||||
// set new returned passphrase!
|
||||
mNewPassPhrase = data
|
||||
.getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);
|
||||
|
||||
updatePassPhraseButtonText();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
// set title based on isPassphraseSet()
|
||||
int title = -1;
|
||||
if (isPassphraseSet()) {
|
||||
title = R.string.title_change_pass_phrase;
|
||||
} else {
|
||||
title = R.string.title_set_passphrase;
|
||||
}
|
||||
|
||||
SetPassphraseDialogFragment setPassphraseDialog = SetPassphraseDialogFragment.newInstance(
|
||||
messenger, title);
|
||||
|
||||
setPassphraseDialog.show(getSupportFragmentManager(), "setPassphraseDialog");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build layout based on mUserId, mKeys and mKeysUsages Vectors. It creates Views for every user
|
||||
* id and key.
|
||||
*/
|
||||
private void buildLayout() {
|
||||
setContentView(R.layout.edit_key_activity);
|
||||
|
||||
// find views
|
||||
mChangePassPhrase = (BootstrapButton) findViewById(R.id.edit_key_btn_change_pass_phrase);
|
||||
mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase);
|
||||
|
||||
// Build layout based on given userIds and keys
|
||||
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
LinearLayout container = (LinearLayout) findViewById(R.id.edit_key_container);
|
||||
mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
|
||||
mUserIdsView.setType(Id.type.user_id);
|
||||
mUserIdsView.setCanEdit(masterCanSign);
|
||||
mUserIdsView.setUserIds(mUserIds);
|
||||
container.addView(mUserIdsView);
|
||||
mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
|
||||
mKeysView.setType(Id.type.key);
|
||||
mKeysView.setCanEdit(masterCanSign);
|
||||
mKeysView.setKeys(mKeys, mKeysUsages);
|
||||
container.addView(mKeysView);
|
||||
|
||||
updatePassPhraseButtonText();
|
||||
|
||||
mChangePassPhrase.setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
showSetPassphraseDialog();
|
||||
}
|
||||
});
|
||||
|
||||
// disable passphrase when no passphrase checkobox is checked!
|
||||
mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (isChecked) {
|
||||
// remove passphrase
|
||||
mNewPassPhrase = null;
|
||||
|
||||
mChangePassPhrase.setVisibility(View.GONE);
|
||||
} else {
|
||||
mChangePassPhrase.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private long getMasterKeyId() {
|
||||
if (mKeysView.getEditors().getChildCount() == 0) {
|
||||
return 0;
|
||||
}
|
||||
return ((KeyEditor) mKeysView.getEditors().getChildAt(0)).getValue().getKeyID();
|
||||
}
|
||||
|
||||
public boolean isPassphraseSet() {
|
||||
if (mNoPassphrase.isChecked()) {
|
||||
return true;
|
||||
} else if ((!mCurrentPassPhrase.equals(""))
|
||||
|| (mNewPassPhrase != null && !mNewPassPhrase.equals(""))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveClicked() {
|
||||
try {
|
||||
if (!isPassphraseSet()) {
|
||||
throw new PgpGeneralException(this.getString(R.string.set_a_passphrase));
|
||||
}
|
||||
|
||||
// Send all information needed to service to edit key in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
data.putString(KeychainIntentService.SAVE_KEYRING_CURRENT_PASSPHRASE,
|
||||
mCurrentPassPhrase);
|
||||
data.putString(KeychainIntentService.SAVE_KEYRING_NEW_PASSPHRASE, mNewPassPhrase);
|
||||
data.putStringArrayList(KeychainIntentService.SAVE_KEYRING_USER_IDS,
|
||||
getUserIds(mUserIdsView));
|
||||
ArrayList<PGPSecretKey> keys = getKeys(mKeysView);
|
||||
data.putByteArray(KeychainIntentService.SAVE_KEYRING_KEYS,
|
||||
PgpConversionHelper.PGPSecretKeyArrayListToBytes(keys));
|
||||
data.putIntegerArrayList(KeychainIntentService.SAVE_KEYRING_KEYS_USAGES,
|
||||
getKeysUsages(mKeysView));
|
||||
data.putLong(KeychainIntentService.SAVE_KEYRING_MASTER_KEY_ID, getMasterKeyId());
|
||||
data.putBoolean(KeychainIntentService.SAVE_KEYRING_CAN_SIGN, masterCanSign);
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after saving is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_saving, ProgressDialog.STYLE_HORIZONTAL) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
Intent data = new Intent();
|
||||
data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, getMasterKeyId());
|
||||
ArrayList<String> userIds = null;
|
||||
try {
|
||||
userIds = getUserIds(mUserIdsView);
|
||||
} catch (PgpGeneralException e) {
|
||||
Log.e(Constants.TAG, "exception while getting user ids", e);
|
||||
}
|
||||
data.putExtra(RESULT_EXTRA_USER_ID, userIds.get(0));
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
} catch (PgpGeneralException e) {
|
||||
Toast.makeText(this, getString(R.string.error_message, e.getMessage()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelClicked() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns user ids from the SectionView
|
||||
*
|
||||
* @param userIdsView
|
||||
* @return
|
||||
*/
|
||||
private ArrayList<String> getUserIds(SectionView userIdsView) throws PgpGeneralException {
|
||||
ArrayList<String> userIds = new ArrayList<String>();
|
||||
|
||||
ViewGroup userIdEditors = userIdsView.getEditors();
|
||||
|
||||
boolean gotMainUserId = false;
|
||||
for (int i = 0; i < userIdEditors.getChildCount(); ++i) {
|
||||
UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i);
|
||||
String userId = null;
|
||||
try {
|
||||
userId = editor.getValue();
|
||||
} catch (UserIdEditor.NoNameException e) {
|
||||
throw new PgpGeneralException(this.getString(R.string.error_user_id_needs_a_name));
|
||||
} catch (UserIdEditor.NoEmailException e) {
|
||||
throw new PgpGeneralException(
|
||||
this.getString(R.string.error_user_id_needs_an_email_address));
|
||||
} catch (UserIdEditor.InvalidEmailException e) {
|
||||
throw new PgpGeneralException(e.getMessage());
|
||||
}
|
||||
|
||||
if (userId.equals("")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (editor.isMainUserId()) {
|
||||
userIds.add(0, userId);
|
||||
gotMainUserId = true;
|
||||
} else {
|
||||
userIds.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size() == 0) {
|
||||
throw new PgpGeneralException(getString(R.string.error_key_needs_a_user_id));
|
||||
}
|
||||
|
||||
if (!gotMainUserId) {
|
||||
throw new PgpGeneralException(getString(R.string.error_main_user_id_must_not_be_empty));
|
||||
}
|
||||
|
||||
return userIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns keys from the SectionView
|
||||
*
|
||||
* @param keysView
|
||||
* @return
|
||||
*/
|
||||
private ArrayList<PGPSecretKey> getKeys(SectionView keysView) throws PgpGeneralException {
|
||||
ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
|
||||
|
||||
ViewGroup keyEditors = keysView.getEditors();
|
||||
|
||||
if (keyEditors.getChildCount() == 0) {
|
||||
throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
|
||||
}
|
||||
|
||||
for (int i = 0; i < keyEditors.getChildCount(); ++i) {
|
||||
KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
|
||||
keys.add(editor.getValue());
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns usage selections of keys from the SectionView
|
||||
*
|
||||
* @param keysView
|
||||
* @return
|
||||
*/
|
||||
private ArrayList<Integer> getKeysUsages(SectionView keysView) throws PgpGeneralException {
|
||||
ArrayList<Integer> getKeysUsages = new ArrayList<Integer>();
|
||||
|
||||
ViewGroup keyEditors = keysView.getEditors();
|
||||
|
||||
if (keyEditors.getChildCount() == 0) {
|
||||
throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
|
||||
}
|
||||
|
||||
for (int i = 0; i < keyEditors.getChildCount(); ++i) {
|
||||
KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
|
||||
getKeysUsages.add(editor.getUsage());
|
||||
}
|
||||
|
||||
return getKeysUsages;
|
||||
}
|
||||
|
||||
private void updatePassPhraseButtonText() {
|
||||
mChangePassPhrase.setText(isPassphraseSet() ? getString(R.string.btn_change_passphrase)
|
||||
: getString(R.string.btn_set_passphrase));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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 java.util.ArrayList;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.actionbarsherlock.app.ActionBar;
|
||||
import com.actionbarsherlock.app.ActionBar.Tab;
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
|
||||
public class HelpActivity extends SherlockFragmentActivity {
|
||||
public static final String EXTRA_SELECTED_TAB = "selectedTab";
|
||||
|
||||
ViewPager mViewPager;
|
||||
TabsAdapter mTabsAdapter;
|
||||
TextView tabCenter;
|
||||
TextView tabText;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.help_activity);
|
||||
|
||||
mViewPager = (ViewPager) findViewById(R.id.pager);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setHomeButtonEnabled(false);
|
||||
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
|
||||
|
||||
mTabsAdapter = new TabsAdapter(this, mViewPager);
|
||||
|
||||
int selectedTab = 0;
|
||||
Intent intent = getIntent();
|
||||
if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) {
|
||||
selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB);
|
||||
}
|
||||
|
||||
Bundle startBundle = new Bundle();
|
||||
startBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_start);
|
||||
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_start)),
|
||||
HelpFragmentHtml.class, startBundle, (selectedTab == 0 ? true : false));
|
||||
|
||||
Bundle nfcBundle = new Bundle();
|
||||
nfcBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_nfc_beam);
|
||||
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_nfc_beam)),
|
||||
HelpFragmentHtml.class, nfcBundle, (selectedTab == 1 ? true : false));
|
||||
|
||||
Bundle changelogBundle = new Bundle();
|
||||
changelogBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_changelog);
|
||||
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_changelog)),
|
||||
HelpFragmentHtml.class, changelogBundle, (selectedTab == 2 ? true : false));
|
||||
|
||||
mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_about)),
|
||||
HelpFragmentAbout.class, null, (selectedTab == 3 ? true : false));
|
||||
}
|
||||
|
||||
public static class TabsAdapter extends FragmentPagerAdapter implements ActionBar.TabListener,
|
||||
ViewPager.OnPageChangeListener {
|
||||
private final Context mContext;
|
||||
private final ActionBar mActionBar;
|
||||
private final ViewPager mViewPager;
|
||||
private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
|
||||
|
||||
static final class TabInfo {
|
||||
private final Class<?> clss;
|
||||
private final Bundle args;
|
||||
|
||||
TabInfo(Class<?> _class, Bundle _args) {
|
||||
clss = _class;
|
||||
args = _args;
|
||||
}
|
||||
}
|
||||
|
||||
public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
|
||||
super(activity.getSupportFragmentManager());
|
||||
mContext = activity;
|
||||
mActionBar = activity.getSupportActionBar();
|
||||
mViewPager = pager;
|
||||
mViewPager.setAdapter(this);
|
||||
mViewPager.setOnPageChangeListener(this);
|
||||
}
|
||||
|
||||
public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args, boolean selected) {
|
||||
TabInfo info = new TabInfo(clss, args);
|
||||
tab.setTag(info);
|
||||
tab.setTabListener(this);
|
||||
mTabs.add(info);
|
||||
mActionBar.addTab(tab, selected);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mTabs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
TabInfo info = mTabs.get(position);
|
||||
return Fragment.instantiate(mContext, info.clss.getName(), info.args);
|
||||
}
|
||||
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
}
|
||||
|
||||
public void onPageSelected(int position) {
|
||||
mActionBar.setSelectedNavigationItem(position);
|
||||
}
|
||||
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
}
|
||||
|
||||
public void onTabSelected(Tab tab, FragmentTransaction ft) {
|
||||
Object tag = tab.getTag();
|
||||
for (int i = 0; i < mTabs.size(); i++) {
|
||||
if (mTabs.get(i) == tag) {
|
||||
mViewPager.setCurrentItem(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
|
||||
}
|
||||
|
||||
public void onTabReselected(Tab tab, FragmentTransaction ft) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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 org.sufficientlysecure.htmltextview.HtmlTextView;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragment;
|
||||
|
||||
public class HelpFragmentAbout extends SherlockFragment {
|
||||
|
||||
/**
|
||||
* Workaround for Android Bug. See
|
||||
* http://stackoverflow.com/questions/8748064/starting-activity-from
|
||||
* -fragment-causes-nullpointerexception
|
||||
*/
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
setUserVisibleHint(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.help_about_fragment, container, false);
|
||||
|
||||
TextView versionText = (TextView) view.findViewById(R.id.help_about_version);
|
||||
versionText.setText(getString(R.string.help_about_version) + " " + getVersion());
|
||||
|
||||
HtmlTextView aboutTextView = (HtmlTextView) view.findViewById(R.id.help_about_text);
|
||||
|
||||
// load html from raw resource (Parsing handled by HtmlTextView library)
|
||||
aboutTextView.setHtmlFromRawResource(getActivity(), R.raw.help_about);
|
||||
|
||||
// no flickering when clicking textview for Android < 4
|
||||
aboutTextView.setTextColor(getResources().getColor(android.R.color.black));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current package version.
|
||||
*
|
||||
* @return The current version.
|
||||
*/
|
||||
private String getVersion() {
|
||||
String result = "";
|
||||
try {
|
||||
PackageManager manager = getActivity().getPackageManager();
|
||||
PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
|
||||
|
||||
result = String.format("%s (%s)", info.versionName, info.versionCode);
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.w(Constants.TAG, "Unable to get application version: " + e.getMessage());
|
||||
result = "Unable to get application version.";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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 org.sufficientlysecure.htmltextview.HtmlTextView;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragment;
|
||||
|
||||
public class HelpFragmentHtml extends SherlockFragment {
|
||||
private Activity mActivity;
|
||||
|
||||
private int htmlFile;
|
||||
|
||||
public static final String ARG_HTML_FILE = "htmlFile";
|
||||
|
||||
/**
|
||||
* Create a new instance of HelpFragmentHtml, providing "htmlFile" as an argument.
|
||||
*/
|
||||
static HelpFragmentHtml newInstance(int htmlFile) {
|
||||
HelpFragmentHtml f = new HelpFragmentHtml();
|
||||
|
||||
// Supply html raw file input as an argument.
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_HTML_FILE, htmlFile);
|
||||
f.setArguments(args);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for Android Bug. See
|
||||
* http://stackoverflow.com/questions/8748064/starting-activity-from
|
||||
* -fragment-causes-nullpointerexception
|
||||
*/
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
setUserVisibleHint(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
mActivity = getActivity();
|
||||
|
||||
htmlFile = getArguments().getInt(ARG_HTML_FILE);
|
||||
|
||||
ScrollView scroller = new ScrollView(mActivity);
|
||||
HtmlTextView text = new HtmlTextView(mActivity);
|
||||
|
||||
// padding
|
||||
int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, mActivity
|
||||
.getResources().getDisplayMetrics());
|
||||
text.setPadding(padding, padding, padding, 0);
|
||||
|
||||
scroller.addView(text);
|
||||
|
||||
// load html from raw resource (Parsing handled by HtmlTextView library)
|
||||
text.setHtmlFromRawResource(getActivity(), htmlFile);
|
||||
|
||||
// no flickering when clicking textview for Android < 4
|
||||
text.setTextColor(getResources().getColor(android.R.color.black));
|
||||
|
||||
return scroller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Senecaso
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.nfc.NdefMessage;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.ActionBar;
|
||||
import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class ImportKeysActivity extends DrawerActivity implements OnNavigationListener {
|
||||
public static final String ACTION_IMPORT_KEY = Constants.INTENT_PREFIX + "IMPORT_KEY";
|
||||
public static final String ACTION_IMPORT_KEY_FROM_QR_CODE = Constants.INTENT_PREFIX
|
||||
+ "IMPORT_KEY_FROM_QR_CODE";
|
||||
|
||||
// Actions for internal use only:
|
||||
public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX
|
||||
+ "IMPORT_KEY_FROM_FILE";
|
||||
public static final String ACTION_IMPORT_KEY_FROM_NFC = Constants.INTENT_PREFIX
|
||||
+ "IMPORT_KEY_FROM_NFC";
|
||||
|
||||
// only used by IMPORT
|
||||
public static final String EXTRA_KEY_BYTES = "key_bytes";
|
||||
|
||||
// TODO: import keys from server
|
||||
// public static final String EXTRA_KEY_ID = "keyId";
|
||||
|
||||
protected boolean mDeleteAfterImport = false;
|
||||
|
||||
FileDialogFragment mFileDialog;
|
||||
ImportKeysListFragment mListFragment;
|
||||
OnNavigationListener mOnNavigationListener;
|
||||
String[] mNavigationStrings;
|
||||
|
||||
Fragment mCurrentFragment;
|
||||
|
||||
BootstrapButton mImportButton;
|
||||
|
||||
// BootstrapButton mImportSignUploadButton;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.import_keys_activity);
|
||||
|
||||
mImportButton = (BootstrapButton) findViewById(R.id.import_import);
|
||||
mImportButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
importKeys();
|
||||
}
|
||||
});
|
||||
// mImportSignUploadButton = (BootstrapButton) findViewById(R.id.import_sign_and_upload);
|
||||
// mImportSignUploadButton.setOnClickListener(new OnClickListener() {
|
||||
// @Override
|
||||
// public void onClick(View v) {
|
||||
// signAndUploadOnClick();
|
||||
// }
|
||||
// });
|
||||
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
|
||||
setupDrawerNavigation(savedInstanceState);
|
||||
|
||||
// set actionbar without home button if called from another app
|
||||
// ActionBarHelper.setBackButton(this);
|
||||
|
||||
// set drop down navigation
|
||||
mNavigationStrings = getResources().getStringArray(R.array.import_action_list);
|
||||
Context context = getSupportActionBar().getThemedContext();
|
||||
ArrayAdapter<CharSequence> list = ArrayAdapter.createFromResource(context,
|
||||
R.array.import_action_list, R.layout.sherlock_spinner_item);
|
||||
list.setDropDownViewResource(R.layout.sherlock_spinner_dropdown_item);
|
||||
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
|
||||
getSupportActionBar().setListNavigationCallbacks(list, this);
|
||||
|
||||
handleActions(savedInstanceState, getIntent());
|
||||
}
|
||||
|
||||
protected void handleActions(Bundle savedInstanceState, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Bundle extras = intent.getExtras();
|
||||
|
||||
if (extras == null) {
|
||||
extras = new Bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Android Standard Actions
|
||||
*/
|
||||
if (Intent.ACTION_VIEW.equals(action)) {
|
||||
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
|
||||
// override action to delegate it to Keychain's ACTION_IMPORT_KEY
|
||||
action = ACTION_IMPORT_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keychain's own Actions
|
||||
*/
|
||||
if (ACTION_IMPORT_KEY.equals(action)) {
|
||||
if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
|
||||
String importFilename = intent.getData().getPath();
|
||||
|
||||
// display selected filename
|
||||
getSupportActionBar().setSelectedNavigationItem(1);
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ImportKeysFileFragment.ARG_PATH, importFilename);
|
||||
loadFragment(ImportKeysFileFragment.class, args, mNavigationStrings[1]);
|
||||
|
||||
// directly load data
|
||||
startListFragment(savedInstanceState, null, importFilename);
|
||||
} else if (extras.containsKey(EXTRA_KEY_BYTES)) {
|
||||
byte[] importData = intent.getByteArrayExtra(EXTRA_KEY_BYTES);
|
||||
|
||||
// directly load data
|
||||
startListFragment(savedInstanceState, importData, null);
|
||||
}
|
||||
} else {
|
||||
// Internal actions
|
||||
startListFragment(savedInstanceState, null, null);
|
||||
|
||||
if (ACTION_IMPORT_KEY_FROM_FILE.equals(action)) {
|
||||
getSupportActionBar().setSelectedNavigationItem(1);
|
||||
loadFragment(ImportKeysFileFragment.class, null, mNavigationStrings[1]);
|
||||
} else if (ACTION_IMPORT_KEY_FROM_QR_CODE.equals(action)) {
|
||||
getSupportActionBar().setSelectedNavigationItem(2);
|
||||
loadFragment(ImportKeysQrCodeFragment.class, null, mNavigationStrings[2]);
|
||||
} else if (ACTION_IMPORT_KEY_FROM_NFC.equals(action)) {
|
||||
getSupportActionBar().setSelectedNavigationItem(3);
|
||||
loadFragment(ImportKeysNFCFragment.class, null, mNavigationStrings[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startListFragment(Bundle savedInstanceState, byte[] bytes, String filename) {
|
||||
// Check that the activity is using the layout version with
|
||||
// the fragment_container FrameLayout
|
||||
if (findViewById(R.id.import_keys_list_container) != null) {
|
||||
|
||||
// However, if we're being restored from a previous state,
|
||||
// then we don't need to do anything and should return or else
|
||||
// we could end up with overlapping fragments.
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance of the fragment
|
||||
mListFragment = ImportKeysListFragment.newInstance(bytes, filename);
|
||||
|
||||
// Add the fragment to the 'fragment_container' FrameLayout
|
||||
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.import_keys_list_container, mListFragment)
|
||||
.commitAllowingStateLoss();
|
||||
// do it immediately!
|
||||
getSupportFragmentManager().executePendingTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
|
||||
// Create new fragment from our own Fragment class
|
||||
switch (itemPosition) {
|
||||
case 0:
|
||||
loadFragment(ImportKeysServerFragment.class, null, mNavigationStrings[itemPosition]);
|
||||
break;
|
||||
case 1:
|
||||
loadFragment(ImportKeysFileFragment.class, null, mNavigationStrings[itemPosition]);
|
||||
break;
|
||||
case 2:
|
||||
loadFragment(ImportKeysQrCodeFragment.class, null, mNavigationStrings[itemPosition]);
|
||||
break;
|
||||
case 3:
|
||||
loadFragment(ImportKeysClipboardFragment.class, null, mNavigationStrings[itemPosition]);
|
||||
break;
|
||||
case 4:
|
||||
loadFragment(ImportKeysNFCFragment.class, null, mNavigationStrings[itemPosition]);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadFragment(Class<?> clss, Bundle args, String tag) {
|
||||
mCurrentFragment = Fragment.instantiate(this, clss.getName(), args);
|
||||
|
||||
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
|
||||
// Replace whatever is in the fragment container with this fragment
|
||||
// and give the fragment a tag name equal to the string at the position selected
|
||||
ft.replace(R.id.import_navigation_fragment, mCurrentFragment, tag);
|
||||
// Apply changes
|
||||
ft.commit();
|
||||
}
|
||||
|
||||
public void loadCallback(byte[] importData, String importFilename) {
|
||||
mListFragment.loadNew(importData, importFilename);
|
||||
}
|
||||
|
||||
// private void importAndSignOld(final long keyId, final String expectedFingerprint) {
|
||||
// if (expectedFingerprint != null && expectedFingerprint.length() > 0) {
|
||||
//
|
||||
// Thread t = new Thread() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// try {
|
||||
// // TODO: display some sort of spinner here while the user waits
|
||||
//
|
||||
// // TODO: there should be only 1
|
||||
// HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]);
|
||||
// String encodedKey = server.get(keyId);
|
||||
//
|
||||
// PGPKeyRing keyring = PGPHelper.decodeKeyRing(new ByteArrayInputStream(
|
||||
// encodedKey.getBytes()));
|
||||
// if (keyring != null && keyring instanceof PGPPublicKeyRing) {
|
||||
// PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
|
||||
//
|
||||
// // make sure the fingerprints match before we cache this thing
|
||||
// String actualFingerprint = PGPHelper.convertFingerprintToHex(publicKeyRing
|
||||
// .getPublicKey().getFingerprint());
|
||||
// if (expectedFingerprint.equals(actualFingerprint)) {
|
||||
// // store the signed key in our local cache
|
||||
// int retval = PGPMain.storeKeyRingInCache(publicKeyRing);
|
||||
// if (retval != Id.return_value.ok
|
||||
// && retval != Id.return_value.updated) {
|
||||
// status.putString(EXTRA_ERROR,
|
||||
// "Failed to store signed key in local cache");
|
||||
// } else {
|
||||
// Intent intent = new Intent(ImportFromQRCodeActivity.this,
|
||||
// SignKeyActivity.class);
|
||||
// intent.putExtra(EXTRA_KEY_ID, keyId);
|
||||
// startActivityForResult(intent, Id.request.sign_key);
|
||||
// }
|
||||
// } else {
|
||||
// status.putString(
|
||||
// EXTRA_ERROR,
|
||||
// "Scanned fingerprint does NOT match the fingerprint of the received key. You shouldnt trust this key.");
|
||||
// }
|
||||
// }
|
||||
// } catch (QueryException e) {
|
||||
// Log.e(TAG, "Failed to query KeyServer", e);
|
||||
// status.putString(EXTRA_ERROR, "Failed to query KeyServer");
|
||||
// status.putInt(Constants.extras.STATUS, Id.message.done);
|
||||
// } catch (IOException e) {
|
||||
// Log.e(TAG, "Failed to query KeyServer", e);
|
||||
// status.putString(EXTRA_ERROR, "Failed to query KeyServer");
|
||||
// status.putInt(Constants.extras.STATUS, Id.message.done);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// t.setName("KeyExchange Download Thread");
|
||||
// t.setDaemon(true);
|
||||
// t.start();
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Import keys with mImportData
|
||||
*/
|
||||
public void importKeys() {
|
||||
if (mListFragment.getKeyBytes() != null || mListFragment.getImportFilename() != null) {
|
||||
Log.d(Constants.TAG, "importKeys started");
|
||||
|
||||
// Send all information needed to service to import key in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
// get selected key ids
|
||||
List<ImportKeysListEntry> listEntries = mListFragment.getData();
|
||||
ArrayList<Long> selectedKeyIds = new ArrayList<Long>();
|
||||
for (ImportKeysListEntry entry : listEntries) {
|
||||
if (entry.isSelected()) {
|
||||
selectedKeyIds.add(entry.keyId);
|
||||
}
|
||||
}
|
||||
|
||||
data.putSerializable(KeychainIntentService.IMPORT_KEY_LIST, selectedKeyIds);
|
||||
|
||||
if (mListFragment.getKeyBytes() != null) {
|
||||
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
|
||||
data.putByteArray(KeychainIntentService.IMPORT_BYTES, mListFragment.getKeyBytes());
|
||||
} else {
|
||||
data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
|
||||
data.putString(KeychainIntentService.IMPORT_FILENAME,
|
||||
mListFragment.getImportFilename());
|
||||
}
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after importing is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_importing, ProgressDialog.STYLE_HORIZONTAL) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
// get returned data bundle
|
||||
Bundle returnData = message.getData();
|
||||
|
||||
int added = returnData.getInt(KeychainIntentService.RESULT_IMPORT_ADDED);
|
||||
int updated = returnData
|
||||
.getInt(KeychainIntentService.RESULT_IMPORT_UPDATED);
|
||||
int bad = returnData.getInt(KeychainIntentService.RESULT_IMPORT_BAD);
|
||||
String toastMessage;
|
||||
if (added > 0 && updated > 0) {
|
||||
String addedStr = getResources().getQuantityString(
|
||||
R.plurals.keys_added_and_updated_1, added, added);
|
||||
String updatedStr = getResources().getQuantityString(
|
||||
R.plurals.keys_added_and_updated_2, updated, updated);
|
||||
toastMessage = addedStr + updatedStr;
|
||||
} else if (added > 0) {
|
||||
toastMessage = getResources().getQuantityString(R.plurals.keys_added,
|
||||
added, added);
|
||||
} else if (updated > 0) {
|
||||
toastMessage = getResources().getQuantityString(R.plurals.keys_updated,
|
||||
updated, updated);
|
||||
} else {
|
||||
toastMessage = getString(R.string.no_keys_added_or_updated);
|
||||
}
|
||||
Toast.makeText(ImportKeysActivity.this, toastMessage, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
if (bad > 0) {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(
|
||||
ImportKeysActivity.this);
|
||||
|
||||
alert.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
alert.setTitle(R.string.warning);
|
||||
|
||||
alert.setMessage(ImportKeysActivity.this.getResources()
|
||||
.getQuantityString(R.plurals.bad_keys_encountered, bad, bad));
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
alert.setCancelable(true);
|
||||
alert.create().show();
|
||||
} else if (mDeleteAfterImport) {
|
||||
// everything went well, so now delete, if that was turned on
|
||||
DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
|
||||
.newInstance(mListFragment.getImportFilename());
|
||||
deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_nothing_import, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
public void importOnClick() {
|
||||
importKeys();
|
||||
}
|
||||
|
||||
// public void signAndUploadOnClick() {
|
||||
// // first, import!
|
||||
// // importOnClick(view);
|
||||
//
|
||||
// // TODO: implement sign and upload!
|
||||
// Toast.makeText(ImportKeysActivity.this, "Not implemented right now!", Toast.LENGTH_SHORT)
|
||||
// .show();
|
||||
// }
|
||||
|
||||
/**
|
||||
* NFC
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Check to see that the Activity started due to an Android Beam
|
||||
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
|
||||
handleActionNdefDiscovered(getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC
|
||||
*/
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
// onResume gets called after this to handle the intent
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC: Parses the NDEF Message from the intent and prints to the TextView
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
void handleActionNdefDiscovered(Intent intent) {
|
||||
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
|
||||
// only one message sent during the beam
|
||||
NdefMessage msg = (NdefMessage) rawMsgs[0];
|
||||
// record 0 contains the MIME type, record 1 is the AAR, if present
|
||||
byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload();
|
||||
|
||||
Intent importIntent = new Intent(this, ImportKeysActivity.class);
|
||||
importIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY);
|
||||
importIntent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES, receivedKeyringBytes);
|
||||
|
||||
handleActions(null, importIntent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class ImportKeysClipboardFragment extends Fragment {
|
||||
|
||||
private ImportKeysActivity mImportActivity;
|
||||
private BootstrapButton mButton;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ImportKeysClipboardFragment newInstance() {
|
||||
ImportKeysClipboardFragment frag = new ImportKeysClipboardFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.import_keys_clipboard_fragment, container, false);
|
||||
|
||||
mButton = (BootstrapButton) view.findViewById(R.id.import_clipboard_button);
|
||||
mButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity());
|
||||
String sendText = "";
|
||||
if (clipboardText != null)
|
||||
sendText = clipboardText.toString();
|
||||
mImportActivity.loadCallback(sendText.getBytes(), null);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mImportActivity = (ImportKeysActivity) getActivity();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
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 com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class ImportKeysFileFragment extends Fragment {
|
||||
public static final String ARG_PATH = "path";
|
||||
|
||||
private ImportKeysActivity mImportActivity;
|
||||
private EditText mFilename;
|
||||
private BootstrapButton mBrowse;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ImportKeysFileFragment newInstance(String path) {
|
||||
ImportKeysFileFragment frag = new ImportKeysFileFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ARG_PATH, path);
|
||||
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.import_keys_file_fragment, container, false);
|
||||
|
||||
mFilename = (EditText) view.findViewById(R.id.import_keys_file_input);
|
||||
mBrowse = (BootstrapButton) view.findViewById(R.id.import_keys_file_browse);
|
||||
|
||||
mBrowse.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
// open .asc or .gpg files
|
||||
// setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
|
||||
// or gpg types!
|
||||
FileHelper.openFile(ImportKeysFileFragment.this, mFilename.getText().toString(),
|
||||
"*/*", Id.request.filename);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mImportActivity = (ImportKeysActivity) getActivity();
|
||||
|
||||
// set default path
|
||||
String path = Constants.path.APP_DIR + "/";
|
||||
if (getArguments() != null && getArguments().containsKey(ARG_PATH)) {
|
||||
path = getArguments().getString(ARG_PATH);
|
||||
}
|
||||
mFilename.setText(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode & 0xFFFF) {
|
||||
case Id.request.filename: {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
String path = null;
|
||||
try {
|
||||
path = data.getData().getPath();
|
||||
Log.d(Constants.TAG, "path=" + path);
|
||||
|
||||
// set filename to edittext
|
||||
mFilename.setText(path);
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
|
||||
}
|
||||
|
||||
// load data
|
||||
mImportActivity.loadCallback(null, path);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.List;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockListFragment;
|
||||
|
||||
public class ImportKeysListFragment extends SherlockListFragment implements
|
||||
LoaderManager.LoaderCallbacks<List<ImportKeysListEntry>> {
|
||||
private static final String ARG_FILENAME = "filename";
|
||||
private static final String ARG_BYTES = "bytes";
|
||||
|
||||
private Activity mActivity;
|
||||
private ImportKeysAdapter mAdapter;
|
||||
|
||||
private byte[] mKeyBytes;
|
||||
private String mImportFilename;
|
||||
|
||||
public byte[] getKeyBytes() {
|
||||
return mKeyBytes;
|
||||
}
|
||||
|
||||
public String getImportFilename() {
|
||||
return mImportFilename;
|
||||
}
|
||||
|
||||
public List<ImportKeysListEntry> getData() {
|
||||
return mAdapter.getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ImportKeysListFragment newInstance(byte[] bytes, String filename) {
|
||||
ImportKeysListFragment frag = new ImportKeysListFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putByteArray(ARG_BYTES, bytes);
|
||||
args.putString(ARG_FILENAME, filename);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define Adapter and Loader on create of Activity
|
||||
*/
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
if (getArguments() != null) {
|
||||
mImportFilename = getArguments().getString(ARG_FILENAME);
|
||||
mKeyBytes = getArguments().getByteArray(ARG_BYTES);
|
||||
}
|
||||
|
||||
// Give some text to display if there is no data. In a real
|
||||
// application this would come from a resource.
|
||||
setEmptyText(mActivity.getString(R.string.error_nothing_import));
|
||||
|
||||
// Create an empty adapter we will use to display the loaded data.
|
||||
mAdapter = new ImportKeysAdapter(mActivity);
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// Start out with a progress indicator.
|
||||
setListShown(false);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
// give arguments to onCreateLoader()
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
super.onListItemClick(l, v, position, id);
|
||||
|
||||
// Select checkbox!
|
||||
// Update underlying data and notify adapter of change. The adapter will
|
||||
// update the view automatically.
|
||||
ImportKeysListEntry entry = mAdapter.getItem(position);
|
||||
entry.setSelected(!entry.isSelected());
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void loadNew(byte[] importData, String importFilename) {
|
||||
this.mKeyBytes = importData;
|
||||
this.mImportFilename = importFilename;
|
||||
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<List<ImportKeysListEntry>> onCreateLoader(int id, Bundle args) {
|
||||
InputData inputData = getInputData(mKeyBytes, mImportFilename);
|
||||
return new ImportKeysListLoader(mActivity, inputData);
|
||||
}
|
||||
|
||||
private InputData getInputData(byte[] importBytes, String importFilename) {
|
||||
InputData inputData = null;
|
||||
if (importBytes != null) {
|
||||
inputData = new InputData(new ByteArrayInputStream(importBytes), importBytes.length);
|
||||
} else if (importFilename != null) {
|
||||
try {
|
||||
inputData = new InputData(new FileInputStream(importFilename),
|
||||
importFilename.length());
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(Constants.TAG, "Failed to init FileInputStream!", e);
|
||||
}
|
||||
}
|
||||
|
||||
return inputData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<ImportKeysListEntry>> loader,
|
||||
List<ImportKeysListEntry> data) {
|
||||
Log.d(Constants.TAG, "data: " + data);
|
||||
|
||||
// swap in the real data!
|
||||
mAdapter.setData(data);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// The list should now be shown.
|
||||
if (isResumed()) {
|
||||
setListShown(true);
|
||||
} else {
|
||||
setListShownNoAnimation(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<List<ImportKeysListEntry>> loader) {
|
||||
// Clear the data in the adapter.
|
||||
mAdapter.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 org.sufficientlysecure.keychain.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class ImportKeysNFCFragment extends Fragment {
|
||||
|
||||
private BootstrapButton mButton;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ImportKeysNFCFragment newInstance() {
|
||||
ImportKeysNFCFragment frag = new ImportKeysNFCFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.import_keys_nfc_fragment, container, false);
|
||||
|
||||
mButton = (BootstrapButton) view.findViewById(R.id.import_nfc_button);
|
||||
mButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// show nfc help
|
||||
Intent intent = new Intent(getActivity(), HelpActivity.class);
|
||||
intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 1);
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2014 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 java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
import com.google.zxing.integration.android.IntentResult;
|
||||
|
||||
public class ImportKeysQrCodeFragment extends Fragment {
|
||||
|
||||
private ImportKeysActivity mImportActivity;
|
||||
private BootstrapButton mButton;
|
||||
private TextView mText;
|
||||
private ProgressBar mProgress;
|
||||
|
||||
private String[] mScannedContent;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ImportKeysQrCodeFragment newInstance() {
|
||||
ImportKeysQrCodeFragment frag = new ImportKeysQrCodeFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.import_keys_qr_code_fragment, container, false);
|
||||
|
||||
mButton = (BootstrapButton) view.findViewById(R.id.import_qrcode_button);
|
||||
mText = (TextView) view.findViewById(R.id.import_qrcode_text);
|
||||
mProgress = (ProgressBar) view.findViewById(R.id.import_qrcode_progress);
|
||||
|
||||
mButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// scan using xzing's Barcode Scanner
|
||||
new IntentIntegratorSupportV4(ImportKeysQrCodeFragment.this).initiateScan();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mImportActivity = (ImportKeysActivity) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode & 0xFFFF) {
|
||||
case IntentIntegratorSupportV4.REQUEST_CODE: {
|
||||
IntentResult scanResult = IntentIntegratorSupportV4.parseActivityResult(requestCode,
|
||||
resultCode, data);
|
||||
if (scanResult != null && scanResult.getFormatName() != null) {
|
||||
|
||||
Log.d(Constants.TAG, "scanResult content: " + scanResult.getContents());
|
||||
|
||||
// look if it's fingerprint only
|
||||
if (scanResult.getContents().toLowerCase(Locale.ENGLISH).startsWith("openpgp4fpr")) {
|
||||
importFingerprint(scanResult.getContents().toLowerCase(Locale.ENGLISH));
|
||||
return;
|
||||
}
|
||||
|
||||
// look if it is the whole key
|
||||
String[] parts = scanResult.getContents().split(",");
|
||||
if (parts.length == 3) {
|
||||
importParts(parts);
|
||||
return;
|
||||
}
|
||||
|
||||
// fail...
|
||||
Toast.makeText(getActivity(), R.string.import_qr_code_wrong, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void importFingerprint(String uri) {
|
||||
String fingerprint = uri.split(":")[1];
|
||||
|
||||
Log.d(Constants.TAG, "fingerprint: " + fingerprint);
|
||||
|
||||
if (fingerprint.length() < 16) {
|
||||
Toast.makeText(getActivity(), R.string.import_qr_code_too_short_fingerprint,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent queryIntent = new Intent(getActivity(), KeyServerQueryActivity.class);
|
||||
queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
|
||||
queryIntent.putExtra(KeyServerQueryActivity.EXTRA_FINGERPRINT, fingerprint);
|
||||
startActivity(queryIntent);
|
||||
}
|
||||
|
||||
private void importParts(String[] parts) {
|
||||
int counter = Integer.valueOf(parts[0]);
|
||||
int size = Integer.valueOf(parts[1]);
|
||||
String content = parts[2];
|
||||
|
||||
Log.d(Constants.TAG, "" + counter);
|
||||
Log.d(Constants.TAG, "" + size);
|
||||
Log.d(Constants.TAG, "" + content);
|
||||
|
||||
// first qr code -> setup
|
||||
if (counter == 0) {
|
||||
mScannedContent = new String[size];
|
||||
mProgress.setMax(size);
|
||||
mProgress.setVisibility(View.VISIBLE);
|
||||
mText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (mScannedContent == null || counter > mScannedContent.length) {
|
||||
Toast.makeText(getActivity(), R.string.import_qr_code_start_with_one, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// save scanned content
|
||||
mScannedContent[counter] = content;
|
||||
|
||||
// get missing numbers
|
||||
ArrayList<Integer> missing = new ArrayList<Integer>();
|
||||
for (int i = 0; i < mScannedContent.length; i++) {
|
||||
if (mScannedContent[i] == null) {
|
||||
missing.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// update progress and text
|
||||
int alreadyScanned = mScannedContent.length - missing.size();
|
||||
mProgress.setProgress(alreadyScanned);
|
||||
|
||||
String missingString = "";
|
||||
for (int m : missing) {
|
||||
if (!missingString.equals("")) {
|
||||
missingString += ", ";
|
||||
}
|
||||
missingString += String.valueOf(m + 1);
|
||||
}
|
||||
|
||||
String missingText = getResources().getQuantityString(R.plurals.import_qr_code_missing,
|
||||
missing.size(), missingString);
|
||||
mText.setText(missingText);
|
||||
|
||||
// finished!
|
||||
if (missing.size() == 0) {
|
||||
mText.setText(R.string.import_qr_code_finished);
|
||||
String result = "";
|
||||
for (String in : mScannedContent) {
|
||||
result += in;
|
||||
}
|
||||
mImportActivity.loadCallback(result.getBytes(), null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 org.sufficientlysecure.keychain.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class ImportKeysServerFragment extends Fragment {
|
||||
private BootstrapButton mButton;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ImportKeysServerFragment newInstance() {
|
||||
ImportKeysServerFragment frag = new ImportKeysServerFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.import_keys_keyserver_fragment, container, false);
|
||||
|
||||
mButton = (BootstrapButton) view.findViewById(R.id.import_keyserver_button);
|
||||
mButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// TODO: use fragment instead of activity, handle onresult here!
|
||||
startActivityForResult(new Intent(getActivity(), KeyServerQueryActivity.class), 0);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 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 org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ExportHelper;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
|
||||
public class KeyListPublicActivity extends DrawerActivity {
|
||||
|
||||
ExportHelper mExportHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mExportHelper = new ExportHelper(this);
|
||||
|
||||
setContentView(R.layout.key_list_public_activity);
|
||||
|
||||
// now setup navigation drawer in DrawerActivity...
|
||||
setupDrawerNavigation(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getSupportMenuInflater().inflate(R.menu.key_list_public, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_key_list_public_import:
|
||||
Intent intentImport = new Intent(this, ImportKeysActivity.class);
|
||||
startActivityForResult(intentImport, 0);
|
||||
|
||||
return true;
|
||||
case R.id.menu_key_list_public_export:
|
||||
mExportHelper.showExportKeysDialog(null, Id.type.public_key, Constants.path.APP_DIR
|
||||
+ "/pubexport.asc");
|
||||
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 java.util.Set;
|
||||
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyListPublicAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
|
||||
|
||||
import se.emilsjolander.stickylistheaders.ApiLevelTooLowException;
|
||||
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
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.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView.MultiChoiceModeListener;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
/**
|
||||
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
|
||||
* StickyListHeaders library which does not extend upon ListView.
|
||||
*/
|
||||
public class KeyListPublicFragment extends Fragment implements AdapterView.OnItemClickListener,
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private KeyListPublicAdapter mAdapter;
|
||||
private StickyListHeadersListView mStickyList;
|
||||
|
||||
// empty layout
|
||||
private BootstrapButton mButtonEmptyCreate;
|
||||
private BootstrapButton mButtonEmptyImport;
|
||||
|
||||
/**
|
||||
* Load custom layout with StickyListView from library
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.key_list_public_fragment, container, false);
|
||||
|
||||
mButtonEmptyCreate = (BootstrapButton) view.findViewById(R.id.key_list_empty_button_create);
|
||||
mButtonEmptyCreate.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(getActivity(), EditKeyActivity.class);
|
||||
intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
|
||||
intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
|
||||
intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
});
|
||||
|
||||
mButtonEmptyImport = (BootstrapButton) view.findViewById(R.id.key_list_empty_button_import);
|
||||
mButtonEmptyImport.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intentImportFromFile = new Intent(getActivity(), ImportKeysActivity.class);
|
||||
intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE);
|
||||
startActivityForResult(intentImportFromFile, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define Adapter and Loader on create of Activity
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
// mKeyListPublicActivity = (KeyListPublicActivity) getActivity();
|
||||
mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list);
|
||||
|
||||
mStickyList.setOnItemClickListener(this);
|
||||
mStickyList.setAreHeadersSticky(true);
|
||||
mStickyList.setDrawingListUnderStickyHeader(false);
|
||||
mStickyList.setFastScrollEnabled(true);
|
||||
try {
|
||||
mStickyList.setFastScrollAlwaysVisible(true);
|
||||
} catch (ApiLevelTooLowException e) {
|
||||
}
|
||||
|
||||
// this view is made visible if no data is available
|
||||
mStickyList.setEmptyView(getActivity().findViewById(R.id.empty));
|
||||
|
||||
/*
|
||||
* ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only
|
||||
* available for Android >= 3.0
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
|
||||
|
||||
private int count = 0;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
android.view.MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.key_list_public_multi, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Set<Integer> positions = mAdapter.getCurrentCheckedPosition();
|
||||
|
||||
// get IDs for checked positions as long array
|
||||
long[] ids = new long[positions.size()];
|
||||
int i = 0;
|
||||
for (int pos : positions) {
|
||||
ids[i] = mAdapter.getItemId(pos);
|
||||
i++;
|
||||
}
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_key_list_public_multi_encrypt: {
|
||||
encrypt(ids);
|
||||
|
||||
break;
|
||||
}
|
||||
case R.id.menu_key_list_public_multi_delete: {
|
||||
showDeleteKeyDialog(ids);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
count = 0;
|
||||
mAdapter.clearSelection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
|
||||
boolean checked) {
|
||||
if (checked) {
|
||||
count++;
|
||||
mAdapter.setNewSelection(position, checked);
|
||||
} else {
|
||||
count--;
|
||||
mAdapter.removeSelection(position);
|
||||
}
|
||||
|
||||
String keysSelected = getResources().getQuantityString(
|
||||
R.plurals.key_list_selected_keys, count, count);
|
||||
mode.setTitle(keysSelected);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: Not supported by StickyListHeader, thus no indicator is shown while loading
|
||||
// Start out with a progress indicator.
|
||||
// setListShown(false);
|
||||
|
||||
// Create an empty adapter we will use to display the loaded data.
|
||||
mAdapter = new KeyListPublicAdapter(getActivity(), null, Id.type.public_key, USER_ID_INDEX);
|
||||
mStickyList.setAdapter(mAdapter);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
// These are the rows that we will retrieve.
|
||||
static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
|
||||
UserIds.USER_ID };
|
||||
|
||||
static final int USER_ID_INDEX = 2;
|
||||
|
||||
static final String SORT_ORDER = UserIds.USER_ID + " ASC";
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
// This is called when a new Loader needs to be created. This
|
||||
// sample only has one Loader, so we don't care about the ID.
|
||||
Uri baseUri = KeyRings.buildPublicKeyRingsUri();
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// Swap the new cursor in. (The framework will take care of closing the
|
||||
// old cursor once we return.)
|
||||
mAdapter.swapCursor(data);
|
||||
|
||||
mStickyList.setAdapter(mAdapter);
|
||||
|
||||
// NOTE: Not supported by StickyListHeader, thus no indicator is shown while loading
|
||||
// The list should now be shown.
|
||||
// if (isResumed()) {
|
||||
// setListShown(true);
|
||||
// } else {
|
||||
// setListShownNoAnimation(true);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// This is called when the last Cursor provided to onLoadFinished()
|
||||
// above is about to be closed. We need to make sure we are no
|
||||
// longer using it.
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* On click on item, start key view activity
|
||||
*/
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
Intent viewIntent = null;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
||||
viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
|
||||
} else {
|
||||
viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class);
|
||||
}
|
||||
viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id)));
|
||||
startActivity(viewIntent);
|
||||
}
|
||||
|
||||
public void encrypt(long[] keyRingRowIds) {
|
||||
// get master key ids from row ids
|
||||
long[] keyRingIds = new long[keyRingRowIds.length];
|
||||
for (int i = 0; i < keyRingRowIds.length; i++) {
|
||||
keyRingIds[i] = ProviderHelper.getPublicMasterKeyId(getActivity(), keyRingRowIds[i]);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(getActivity(), EncryptActivity.class);
|
||||
intent.setAction(EncryptActivity.ACTION_ENCRYPT);
|
||||
intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, keyRingIds);
|
||||
// used instead of startActivity set actionbar based on callingPackage
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dialog to delete key
|
||||
*
|
||||
* @param keyRingRowIds
|
||||
*/
|
||||
public void showDeleteKeyDialog(long[] keyRingRowIds) {
|
||||
DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(null,
|
||||
keyRingRowIds, Id.type.public_key);
|
||||
|
||||
deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ExportHelper;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
|
||||
public class KeyListSecretActivity extends DrawerActivity {
|
||||
|
||||
ExportHelper mExportHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mExportHelper = new ExportHelper(this);
|
||||
|
||||
setContentView(R.layout.key_list_secret_activity);
|
||||
|
||||
// now setup navigation drawer in DrawerActivity...
|
||||
setupDrawerNavigation(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getSupportMenuInflater().inflate(R.menu.key_list_secret, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_key_list_secret_create:
|
||||
createKey();
|
||||
|
||||
return true;
|
||||
case R.id.menu_key_list_secret_create_expert:
|
||||
createKeyExpert();
|
||||
|
||||
return true;
|
||||
case R.id.menu_key_list_secret_export:
|
||||
mExportHelper.showExportKeysDialog(null, Id.type.secret_key, Constants.path.APP_DIR
|
||||
+ "/secexport.asc");
|
||||
|
||||
return true;
|
||||
case R.id.menu_key_list_secret_import:
|
||||
Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
|
||||
intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE);
|
||||
startActivityForResult(intentImportFromFile, 0);
|
||||
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void createKey() {
|
||||
Intent intent = new Intent(this, EditKeyActivity.class);
|
||||
intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
|
||||
intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
|
||||
intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
private void createKeyExpert() {
|
||||
Intent intent = new Intent(this, EditKeyActivity.class);
|
||||
intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 java.util.Set;
|
||||
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyListSecretAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.AbsListView.MultiChoiceModeListener;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockListFragment;
|
||||
|
||||
public class KeyListSecretFragment extends SherlockListFragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor>, OnItemClickListener {
|
||||
|
||||
private KeyListSecretActivity mKeyListSecretActivity;
|
||||
private KeyListSecretAdapter mAdapter;
|
||||
|
||||
/**
|
||||
* Define Adapter and Loader on create of Activity
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mKeyListSecretActivity = (KeyListSecretActivity) getActivity();
|
||||
|
||||
getListView().setOnItemClickListener(this);
|
||||
|
||||
// Give some text to display if there is no data. In a real
|
||||
// application this would come from a resource.
|
||||
setEmptyText(getString(R.string.list_empty));
|
||||
|
||||
/*
|
||||
* ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only
|
||||
* available for Android >= 3.0
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() {
|
||||
|
||||
private int count = 0;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
android.view.MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.key_list_secret_multi, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Set<Integer> positions = mAdapter.getCurrentCheckedPosition();
|
||||
|
||||
// get IDs for checked positions as long array
|
||||
long[] ids = new long[positions.size()];
|
||||
int i = 0;
|
||||
for (int pos : positions) {
|
||||
ids[i] = mAdapter.getItemId(pos);
|
||||
i++;
|
||||
}
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_key_list_public_multi_delete: {
|
||||
showDeleteKeyDialog(ids);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
count = 0;
|
||||
mAdapter.clearSelection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
|
||||
boolean checked) {
|
||||
if (checked) {
|
||||
count++;
|
||||
mAdapter.setNewSelection(position, checked);
|
||||
} else {
|
||||
count--;
|
||||
mAdapter.removeSelection(position);
|
||||
}
|
||||
|
||||
String keysSelected = getResources().getQuantityString(
|
||||
R.plurals.key_list_selected_keys, count, count);
|
||||
mode.setTitle(keysSelected);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// We have a menu item to show in action bar.
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// Start out with a progress indicator.
|
||||
setListShown(false);
|
||||
|
||||
// Create an empty adapter we will use to display the loaded data.
|
||||
mAdapter = new KeyListSecretAdapter(mKeyListSecretActivity, null, 0);
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
// These are the rows that we will retrieve.
|
||||
static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
|
||||
UserIds.USER_ID };
|
||||
static final String SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC";
|
||||
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
// This is called when a new Loader needs to be created. This
|
||||
// sample only has one Loader, so we don't care about the ID.
|
||||
// First, pick the base URI to use depending on whether we are
|
||||
// currently filtering.
|
||||
Uri baseUri = KeyRings.buildSecretKeyRingsUri();
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER);
|
||||
}
|
||||
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// Swap the new cursor in. (The framework will take care of closing the
|
||||
// old cursor once we return.)
|
||||
mAdapter.swapCursor(data);
|
||||
|
||||
// The list should now be shown.
|
||||
if (isResumed()) {
|
||||
setListShown(true);
|
||||
} else {
|
||||
setListShownNoAnimation(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// This is called when the last Cursor provided to onLoadFinished()
|
||||
// above is about to be closed. We need to make sure we are no
|
||||
// longer using it.
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* On click on item, start key view activity
|
||||
*/
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
Intent editIntent = new Intent(mKeyListSecretActivity, EditKeyActivity.class);
|
||||
editIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsUri(Long.toString(id)));
|
||||
editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY);
|
||||
startActivityForResult(editIntent, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dialog to delete key
|
||||
*
|
||||
* @param keyRingRowIds
|
||||
*/
|
||||
public void showDeleteKeyDialog(long[] keyRingRowIds) {
|
||||
DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(null,
|
||||
keyRingRowIds, Id.type.secret_key);
|
||||
|
||||
deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
import android.widget.Toast;
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class KeyServerQueryActivity extends SherlockFragmentActivity {
|
||||
|
||||
// possible intent actions for this activity
|
||||
public static final String ACTION_LOOK_UP_KEY_ID = Constants.INTENT_PREFIX + "LOOK_UP_KEY_ID";
|
||||
|
||||
public static final String ACTION_LOOK_UP_KEY_ID_AND_RETURN = Constants.INTENT_PREFIX
|
||||
+ "LOOK_UP_KEY_ID_AND_RETURN";
|
||||
|
||||
public static final String EXTRA_KEY_ID = "key_id";
|
||||
public static final String EXTRA_FINGERPRINT = "fingerprint";
|
||||
|
||||
public static final String RESULT_EXTRA_TEXT = "text";
|
||||
|
||||
private ListView mList;
|
||||
private EditText mQuery;
|
||||
private Button mSearch;
|
||||
private Spinner mKeyServer;
|
||||
|
||||
private KeyInfoListAdapter mAdapter;
|
||||
|
||||
private int mQueryType;
|
||||
private String mQueryString;
|
||||
private long mQueryId;
|
||||
|
||||
private volatile List<KeyInfo> mSearchResult;
|
||||
|
||||
private volatile String mKeyData;
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
|
||||
case android.R.id.home:
|
||||
// app icon in Action Bar clicked; go home
|
||||
Intent intent = new Intent(this, KeyListPublicActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.key_server_query);
|
||||
|
||||
mQuery = (EditText) findViewById(R.id.query);
|
||||
mSearch = (Button) findViewById(R.id.btn_search);
|
||||
mList = (ListView) findViewById(R.id.list);
|
||||
mAdapter = new KeyInfoListAdapter(this);
|
||||
mList.setAdapter(mAdapter);
|
||||
|
||||
mKeyServer = (Spinner) findViewById(R.id.sign_key_keyserver);
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
|
||||
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
|
||||
.getKeyServers());
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mKeyServer.setAdapter(adapter);
|
||||
if (adapter.getCount() > 0) {
|
||||
mKeyServer.setSelection(0);
|
||||
} else {
|
||||
mSearch.setEnabled(false);
|
||||
}
|
||||
|
||||
mList.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapter, View view, int position, long keyId) {
|
||||
get(keyId);
|
||||
}
|
||||
});
|
||||
|
||||
mSearch.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String query = mQuery.getText().toString();
|
||||
search(query);
|
||||
}
|
||||
});
|
||||
mQuery.setOnEditorActionListener(new OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
String query = mQuery.getText().toString();
|
||||
search(query);
|
||||
return false; // FIXME This is a hack to hide a keyboard
|
||||
// after search http://tinyurl.com/pwdc3q9
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
if (ACTION_LOOK_UP_KEY_ID.equals(action) || ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(action)) {
|
||||
long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0);
|
||||
if (keyId != 0) {
|
||||
String query = "0x" + PgpKeyHelper.convertKeyToHex(keyId);
|
||||
mQuery.setText(query);
|
||||
search(query);
|
||||
}
|
||||
String fingerprint = intent.getStringExtra(EXTRA_FINGERPRINT);
|
||||
if (fingerprint != null) {
|
||||
fingerprint = "0x" + fingerprint;
|
||||
mQuery.setText(fingerprint);
|
||||
search(fingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String query) {
|
||||
mQueryType = Id.keyserver.search;
|
||||
mQueryString = query;
|
||||
mAdapter.setKeys(new ArrayList<KeyInfo>());
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
private void get(long keyId) {
|
||||
mQueryType = Id.keyserver.get;
|
||||
mQueryId = keyId;
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
private void start() {
|
||||
Log.d(Constants.TAG, "start search with service");
|
||||
|
||||
// Send all information needed to service to query keys in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_QUERY_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
String server = (String) mKeyServer.getSelectedItem();
|
||||
data.putString(KeychainIntentService.QUERY_KEY_SERVER, server);
|
||||
|
||||
data.putInt(KeychainIntentService.QUERY_KEY_TYPE, mQueryType);
|
||||
|
||||
if (mQueryType == Id.keyserver.search) {
|
||||
data.putString(KeychainIntentService.QUERY_KEY_STRING, mQueryString);
|
||||
} else if (mQueryType == Id.keyserver.get) {
|
||||
data.putLong(KeychainIntentService.QUERY_KEY_ID, mQueryId);
|
||||
}
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after querying is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_querying, ProgressDialog.STYLE_SPINNER) {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
// get returned data bundle
|
||||
Bundle returnData = message.getData();
|
||||
|
||||
if (mQueryType == Id.keyserver.search) {
|
||||
mSearchResult = returnData
|
||||
.getParcelableArrayList(KeychainIntentService.RESULT_QUERY_KEY_SEARCH_RESULT);
|
||||
} else if (mQueryType == Id.keyserver.get) {
|
||||
mKeyData = returnData
|
||||
.getString(KeychainIntentService.RESULT_QUERY_KEY_DATA);
|
||||
}
|
||||
|
||||
// TODO: IMPROVE CODE!!! some global variables can be
|
||||
// avoided!!!
|
||||
if (mQueryType == Id.keyserver.search) {
|
||||
if (mSearchResult != null) {
|
||||
Toast.makeText(
|
||||
KeyServerQueryActivity.this,
|
||||
getResources().getQuantityString(R.plurals.keys_found,
|
||||
mSearchResult.size(), mSearchResult.size()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
mAdapter.setKeys(mSearchResult);
|
||||
}
|
||||
} else if (mQueryType == Id.keyserver.get) {
|
||||
Intent orgIntent = getIntent();
|
||||
if (ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
|
||||
if (mKeyData != null) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(RESULT_EXTRA_TEXT, mKeyData);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
finish();
|
||||
} else {
|
||||
if (mKeyData != null) {
|
||||
Intent intent = new Intent(KeyServerQueryActivity.this,
|
||||
ImportKeysActivity.class);
|
||||
intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY);
|
||||
intent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES,
|
||||
mKeyData.getBytes());
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
public class KeyInfoListAdapter extends BaseAdapter {
|
||||
protected LayoutInflater mInflater;
|
||||
|
||||
protected Activity mActivity;
|
||||
|
||||
protected List<KeyInfo> mKeys;
|
||||
|
||||
public KeyInfoListAdapter(Activity activity) {
|
||||
mActivity = activity;
|
||||
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
mKeys = new ArrayList<KeyInfo>();
|
||||
}
|
||||
|
||||
public void setKeys(List<KeyInfo> keys) {
|
||||
mKeys = keys;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mKeys.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mKeys.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mKeys.get(position).keyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
KeyInfo keyInfo = mKeys.get(position);
|
||||
|
||||
View view = mInflater.inflate(R.layout.key_server_query_result_item, null);
|
||||
|
||||
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
|
||||
mainUserId.setText(R.string.unknown_user_id);
|
||||
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
|
||||
mainUserIdRest.setText("");
|
||||
TextView keyId = (TextView) view.findViewById(R.id.keyId);
|
||||
keyId.setText(R.string.no_key);
|
||||
TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
|
||||
algorithm.setText("");
|
||||
TextView status = (TextView) view.findViewById(R.id.status);
|
||||
status.setText("");
|
||||
|
||||
String userId = keyInfo.userIds.get(0);
|
||||
if (userId != null) {
|
||||
String chunks[] = userId.split(" <", 2);
|
||||
userId = chunks[0];
|
||||
if (chunks.length > 1) {
|
||||
mainUserIdRest.setText("<" + chunks[1]);
|
||||
}
|
||||
mainUserId.setText(userId);
|
||||
}
|
||||
|
||||
keyId.setText(PgpKeyHelper.convertKeyIdToHex(keyInfo.keyId));
|
||||
|
||||
if (mainUserIdRest.getText().length() == 0) {
|
||||
mainUserIdRest.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
algorithm.setText("" + keyInfo.size + "/" + keyInfo.algorithm);
|
||||
|
||||
if (keyInfo.revoked != null) {
|
||||
status.setText("revoked");
|
||||
} else {
|
||||
status.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
|
||||
if (keyInfo.userIds.size() == 1) {
|
||||
ll.setVisibility(View.GONE);
|
||||
} else {
|
||||
boolean first = true;
|
||||
boolean second = true;
|
||||
for (String uid : keyInfo.userIds) {
|
||||
if (first) {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
if (!second) {
|
||||
View sep = new View(mActivity);
|
||||
sep.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
|
||||
sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
|
||||
ll.addView(sep);
|
||||
}
|
||||
TextView uidView = (TextView) mInflater.inflate(
|
||||
R.layout.key_server_query_result_user_id, null);
|
||||
uidView.setText(uid);
|
||||
ll.addView(uidView);
|
||||
second = false;
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Senecaso
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
/**
|
||||
* gpg --send-key activity
|
||||
*
|
||||
* Sends the selected public key to a key server
|
||||
*/
|
||||
public class KeyServerUploadActivity extends SherlockFragmentActivity {
|
||||
|
||||
// Not used in sourcode, but listed in AndroidManifest!
|
||||
public static final String ACTION_EXPORT_KEY_TO_SERVER = Constants.INTENT_PREFIX
|
||||
+ "EXPORT_KEY_TO_SERVER";
|
||||
|
||||
public static final String EXTRA_KEYRING_ROW_ID = "key_row_id";
|
||||
|
||||
private BootstrapButton mUploadButton;
|
||||
private Spinner mKeyServerSpinner;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.key_server_export);
|
||||
|
||||
mUploadButton = (BootstrapButton) findViewById(R.id.btn_export_to_server);
|
||||
mKeyServerSpinner = (Spinner) findViewById(R.id.sign_key_keyserver);
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
|
||||
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
|
||||
.getKeyServers());
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mKeyServerSpinner.setAdapter(adapter);
|
||||
if (adapter.getCount() > 0) {
|
||||
mKeyServerSpinner.setSelection(0);
|
||||
} else {
|
||||
mUploadButton.setEnabled(false);
|
||||
}
|
||||
|
||||
mUploadButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
uploadKey();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void uploadKey() {
|
||||
// Send all information needed to service to upload key in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
int keyRingId = getIntent().getIntExtra(EXTRA_KEYRING_ROW_ID, -1);
|
||||
data.putInt(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, keyRingId);
|
||||
|
||||
String server = (String) mKeyServerSpinner.getSelectedItem();
|
||||
data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after uploading is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
|
||||
Toast.makeText(KeyServerUploadActivity.this, R.string.key_send_success,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import org.spongycastle.bcpg.HashAlgorithmTags;
|
||||
import org.spongycastle.openpgp.PGPEncryptedData;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceScreen;
|
||||
|
||||
import com.actionbarsherlock.app.ActionBar;
|
||||
import com.actionbarsherlock.app.SherlockPreferenceActivity;
|
||||
|
||||
public class PreferencesActivity extends SherlockPreferenceActivity {
|
||||
private IntegerListPreference mPassPhraseCacheTtl = null;
|
||||
private IntegerListPreference mEncryptionAlgorithm = null;
|
||||
private IntegerListPreference mHashAlgorithm = null;
|
||||
private IntegerListPreference mMessageCompression = null;
|
||||
private IntegerListPreference mFileCompression = null;
|
||||
private CheckBoxPreference mAsciiArmour = null;
|
||||
private CheckBoxPreference mForceV3Signatures = null;
|
||||
private PreferenceScreen mKeyServerPreference = null;
|
||||
private Preferences mPreferences;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mPreferences = Preferences.getPreferences(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setHomeButtonEnabled(false);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
mPassPhraseCacheTtl = (IntegerListPreference) findPreference(Constants.pref.PASS_PHRASE_CACHE_TTL);
|
||||
mPassPhraseCacheTtl.setValue("" + mPreferences.getPassPhraseCacheTtl());
|
||||
mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
|
||||
mPassPhraseCacheTtl
|
||||
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mPassPhraseCacheTtl.setValue(newValue.toString());
|
||||
mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
|
||||
mPreferences.setPassPhraseCacheTtl(Integer.parseInt(newValue.toString()));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mEncryptionAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM);
|
||||
int valueIds[] = { PGPEncryptedData.AES_128, PGPEncryptedData.AES_192,
|
||||
PGPEncryptedData.AES_256, PGPEncryptedData.BLOWFISH, PGPEncryptedData.TWOFISH,
|
||||
PGPEncryptedData.CAST5, PGPEncryptedData.DES, PGPEncryptedData.TRIPLE_DES,
|
||||
PGPEncryptedData.IDEA, };
|
||||
String entries[] = { "AES-128", "AES-192", "AES-256", "Blowfish", "Twofish", "CAST5",
|
||||
"DES", "Triple DES", "IDEA", };
|
||||
String values[] = new String[valueIds.length];
|
||||
for (int i = 0; i < values.length; ++i) {
|
||||
values[i] = "" + valueIds[i];
|
||||
}
|
||||
mEncryptionAlgorithm.setEntries(entries);
|
||||
mEncryptionAlgorithm.setEntryValues(values);
|
||||
mEncryptionAlgorithm.setValue("" + mPreferences.getDefaultEncryptionAlgorithm());
|
||||
mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
|
||||
mEncryptionAlgorithm
|
||||
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mEncryptionAlgorithm.setValue(newValue.toString());
|
||||
mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
|
||||
mPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue
|
||||
.toString()));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mHashAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_HASH_ALGORITHM);
|
||||
valueIds = new int[] { HashAlgorithmTags.MD5, HashAlgorithmTags.RIPEMD160,
|
||||
HashAlgorithmTags.SHA1, HashAlgorithmTags.SHA224, HashAlgorithmTags.SHA256,
|
||||
HashAlgorithmTags.SHA384, HashAlgorithmTags.SHA512, };
|
||||
entries = new String[] { "MD5", "RIPEMD-160", "SHA-1", "SHA-224", "SHA-256", "SHA-384",
|
||||
"SHA-512", };
|
||||
values = new String[valueIds.length];
|
||||
for (int i = 0; i < values.length; ++i) {
|
||||
values[i] = "" + valueIds[i];
|
||||
}
|
||||
mHashAlgorithm.setEntries(entries);
|
||||
mHashAlgorithm.setEntryValues(values);
|
||||
mHashAlgorithm.setValue("" + mPreferences.getDefaultHashAlgorithm());
|
||||
mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
|
||||
mHashAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mHashAlgorithm.setValue(newValue.toString());
|
||||
mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
|
||||
mPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString()));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mMessageCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_MESSAGE_COMPRESSION);
|
||||
valueIds = new int[] { Id.choice.compression.none, Id.choice.compression.zip,
|
||||
Id.choice.compression.zlib, Id.choice.compression.bzip2, };
|
||||
entries = new String[] {
|
||||
getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")",
|
||||
"ZIP (" + getString(R.string.compression_fast) + ")",
|
||||
"ZLIB (" + getString(R.string.compression_fast) + ")",
|
||||
"BZIP2 (" + getString(R.string.compression_very_slow) + ")", };
|
||||
values = new String[valueIds.length];
|
||||
for (int i = 0; i < values.length; ++i) {
|
||||
values[i] = "" + valueIds[i];
|
||||
}
|
||||
mMessageCompression.setEntries(entries);
|
||||
mMessageCompression.setEntryValues(values);
|
||||
mMessageCompression.setValue("" + mPreferences.getDefaultMessageCompression());
|
||||
mMessageCompression.setSummary(mMessageCompression.getEntry());
|
||||
mMessageCompression
|
||||
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mMessageCompression.setValue(newValue.toString());
|
||||
mMessageCompression.setSummary(mMessageCompression.getEntry());
|
||||
mPreferences.setDefaultMessageCompression(Integer.parseInt(newValue
|
||||
.toString()));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mFileCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_FILE_COMPRESSION);
|
||||
mFileCompression.setEntries(entries);
|
||||
mFileCompression.setEntryValues(values);
|
||||
mFileCompression.setValue("" + mPreferences.getDefaultFileCompression());
|
||||
mFileCompression.setSummary(mFileCompression.getEntry());
|
||||
mFileCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mFileCompression.setValue(newValue.toString());
|
||||
mFileCompression.setSummary(mFileCompression.getEntry());
|
||||
mPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString()));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mAsciiArmour = (CheckBoxPreference) findPreference(Constants.pref.DEFAULT_ASCII_ARMOUR);
|
||||
mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
|
||||
mAsciiArmour.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mAsciiArmour.setChecked((Boolean) newValue);
|
||||
mPreferences.setDefaultAsciiArmour((Boolean) newValue);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mForceV3Signatures = (CheckBoxPreference) findPreference(Constants.pref.FORCE_V3_SIGNATURES);
|
||||
mForceV3Signatures.setChecked(mPreferences.getForceV3Signatures());
|
||||
mForceV3Signatures
|
||||
.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
mForceV3Signatures.setChecked((Boolean) newValue);
|
||||
mPreferences.setForceV3Signatures((Boolean) newValue);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
mKeyServerPreference = (PreferenceScreen) findPreference(Constants.pref.KEY_SERVERS);
|
||||
String servers[] = mPreferences.getKeyServers();
|
||||
mKeyServerPreference.setSummary(getResources().getQuantityString(R.plurals.n_key_servers,
|
||||
servers.length, servers.length));
|
||||
mKeyServerPreference
|
||||
.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(PreferencesActivity.this,
|
||||
PreferencesKeyServerActivity.class);
|
||||
intent.putExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS,
|
||||
mPreferences.getKeyServers());
|
||||
startActivityForResult(intent, Id.request.key_server_preference);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case Id.request.key_server_preference: {
|
||||
if (resultCode == RESULT_CANCELED || data == null) {
|
||||
return;
|
||||
}
|
||||
String servers[] = data
|
||||
.getStringArrayExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS);
|
||||
mPreferences.setKeyServers(servers);
|
||||
mKeyServerPreference.setSummary(getResources().getQuantityString(
|
||||
R.plurals.n_key_servers, servers.length, servers.length));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
|
||||
import org.sufficientlysecure.keychain.ui.widget.Editor;
|
||||
import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
|
||||
import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockActivity;
|
||||
|
||||
public class PreferencesKeyServerActivity extends SherlockActivity implements OnClickListener,
|
||||
EditorListener {
|
||||
|
||||
public static final String EXTRA_KEY_SERVERS = "key_servers";
|
||||
|
||||
private LayoutInflater mInflater;
|
||||
private ViewGroup mEditors;
|
||||
private View mAdd;
|
||||
private TextView mTitle;
|
||||
private TextView mSummary;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Inflate a "Done"/"Cancel" custom action bar view
|
||||
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// ok
|
||||
okClicked();
|
||||
}
|
||||
}, R.string.btn_do_not_save, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// cancel
|
||||
cancelClicked();
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(R.layout.key_server_preference);
|
||||
|
||||
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
mTitle = (TextView) findViewById(R.id.title);
|
||||
mSummary = (TextView) findViewById(R.id.summary);
|
||||
|
||||
mTitle.setText(R.string.label_key_servers);
|
||||
|
||||
mEditors = (ViewGroup) findViewById(R.id.editors);
|
||||
mAdd = findViewById(R.id.add);
|
||||
mAdd.setOnClickListener(this);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS);
|
||||
if (servers != null) {
|
||||
for (int i = 0; i < servers.length; ++i) {
|
||||
KeyServerEditor view = (KeyServerEditor) mInflater.inflate(
|
||||
R.layout.key_server_editor, mEditors, false);
|
||||
view.setEditorListener(this);
|
||||
view.setValue(servers[i]);
|
||||
mEditors.addView(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onDeleted(Editor editor) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
public void onClick(View v) {
|
||||
KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor,
|
||||
mEditors, false);
|
||||
view.setEditorListener(this);
|
||||
mEditors.addView(view);
|
||||
}
|
||||
|
||||
private void cancelClicked() {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void okClicked() {
|
||||
Intent data = new Intent();
|
||||
Vector<String> servers = new Vector<String>();
|
||||
for (int i = 0; i < mEditors.getChildCount(); ++i) {
|
||||
KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i);
|
||||
String tmp = editor.getValue();
|
||||
if (tmp.length() > 0) {
|
||||
servers.add(tmp);
|
||||
}
|
||||
}
|
||||
String[] dummy = new String[0];
|
||||
data.putExtra(EXTRA_KEY_SERVERS, servers.toArray(dummy));
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
|
||||
public class SelectPublicKeyActivity extends SherlockFragmentActivity {
|
||||
|
||||
// Actions for internal use only:
|
||||
public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX
|
||||
+ "SELECT_PUBLIC_KEYRINGS";
|
||||
|
||||
public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
|
||||
|
||||
public static final String RESULT_EXTRA_MASTER_KEY_IDS = "master_key_ids";
|
||||
public static final String RESULT_EXTRA_USER_IDS = "user_ids";
|
||||
|
||||
SelectPublicKeyFragment mSelectFragment;
|
||||
|
||||
long selectedMasterKeyIds[];
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Inflate a "Done"/"Cancel" custom action bar view
|
||||
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// ok
|
||||
okClicked();
|
||||
}
|
||||
}, R.string.btn_do_not_save, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// cancel
|
||||
cancelClicked();
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(R.layout.select_public_key_activity);
|
||||
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
handleIntent(getIntent());
|
||||
|
||||
// Check that the activity is using the layout version with
|
||||
// the fragment_container FrameLayout
|
||||
if (findViewById(R.id.select_public_key_fragment_container) != null) {
|
||||
|
||||
// However, if we're being restored from a previous state,
|
||||
// then we don't need to do anything and should return or else
|
||||
// we could end up with overlapping fragments.
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance of the fragment
|
||||
mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds);
|
||||
|
||||
// Add the fragment to the 'fragment_container' FrameLayout
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.select_public_key_fragment_container, mSelectFragment).commit();
|
||||
}
|
||||
|
||||
// TODO: reimplement!
|
||||
// mFilterLayout = findViewById(R.id.layout_filter);
|
||||
// mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
|
||||
// mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
|
||||
//
|
||||
// mClearFilterButton.setOnClickListener(new OnClickListener() {
|
||||
// public void onClick(View v) {
|
||||
// handleIntent(new Intent());
|
||||
// }
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
// TODO: reimplement search!
|
||||
|
||||
// String searchString = null;
|
||||
// if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
// searchString = intent.getStringExtra(SearchManager.QUERY);
|
||||
// if (searchString != null && searchString.trim().length() == 0) {
|
||||
// searchString = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (searchString == null) {
|
||||
// mFilterLayout.setVisibility(View.GONE);
|
||||
// } else {
|
||||
// mFilterLayout.setVisibility(View.VISIBLE);
|
||||
// mFilterInfo.setText(getString(R.string.filterInfo, searchString));
|
||||
// }
|
||||
|
||||
// preselected master keys
|
||||
selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
|
||||
}
|
||||
|
||||
private void cancelClicked() {
|
||||
setResult(RESULT_CANCELED, null);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void okClicked() {
|
||||
Intent data = new Intent();
|
||||
data.putExtra(RESULT_EXTRA_MASTER_KEY_IDS, mSelectFragment.getSelectedMasterKeyIds());
|
||||
data.putExtra(RESULT_EXTRA_USER_IDS, mSelectFragment.getSelectedUserIds());
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.widget.ListView;
|
||||
|
||||
public class SelectPublicKeyFragment extends ListFragmentWorkaround implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public static final String ARG_PRESELECTED_KEY_IDS = "preselected_key_ids";
|
||||
|
||||
private Activity mActivity;
|
||||
private SelectKeyCursorAdapter mAdapter;
|
||||
private ListView mListView;
|
||||
|
||||
private long mSelectedMasterKeyIds[];
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static SelectPublicKeyFragment newInstance(long[] preselectedKeyIds) {
|
||||
SelectPublicKeyFragment frag = new SelectPublicKeyFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putLongArray(ARG_PRESELECTED_KEY_IDS, preselectedKeyIds);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mSelectedMasterKeyIds = getArguments().getLongArray(ARG_PRESELECTED_KEY_IDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define Adapter and Loader on create of Activity
|
||||
*/
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mActivity = getSherlockActivity();
|
||||
mListView = getListView();
|
||||
|
||||
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||
|
||||
// Give some text to display if there is no data. In a real
|
||||
// application this would come from a resource.
|
||||
setEmptyText(getString(R.string.list_empty));
|
||||
|
||||
mAdapter = new SelectKeyCursorAdapter(mActivity, null, 0, mListView, Id.type.public_key);
|
||||
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// Start out with a progress indicator.
|
||||
setListShown(false);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects items based on master key ids in list view
|
||||
*
|
||||
* @param masterKeyIds
|
||||
*/
|
||||
private void preselectMasterKeyIds(long[] masterKeyIds) {
|
||||
if (masterKeyIds != null) {
|
||||
for (int i = 0; i < mListView.getCount(); ++i) {
|
||||
long keyId = mAdapter.getMasterKeyId(i);
|
||||
for (int j = 0; j < masterKeyIds.length; ++j) {
|
||||
if (keyId == masterKeyIds[j]) {
|
||||
mListView.setItemChecked(i, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all selected master key ids
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public long[] getSelectedMasterKeyIds() {
|
||||
// mListView.getCheckedItemIds() would give the row ids of the KeyRings not the master key
|
||||
// ids!
|
||||
Vector<Long> vector = new Vector<Long>();
|
||||
for (int i = 0; i < mListView.getCount(); ++i) {
|
||||
if (mListView.isItemChecked(i)) {
|
||||
vector.add(mAdapter.getMasterKeyId(i));
|
||||
}
|
||||
}
|
||||
|
||||
// convert to long array
|
||||
long[] selectedMasterKeyIds = new long[vector.size()];
|
||||
for (int i = 0; i < vector.size(); ++i) {
|
||||
selectedMasterKeyIds[i] = vector.get(i);
|
||||
}
|
||||
|
||||
return selectedMasterKeyIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all selected user ids
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String[] getSelectedUserIds() {
|
||||
Vector<String> userIds = new Vector<String>();
|
||||
for (int i = 0; i < mListView.getCount(); ++i) {
|
||||
if (mListView.isItemChecked(i)) {
|
||||
userIds.add((String) mAdapter.getUserId(i));
|
||||
}
|
||||
}
|
||||
|
||||
// make empty array to not return null
|
||||
String userIdArray[] = new String[0];
|
||||
return userIds.toArray(userIdArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
// This is called when a new Loader needs to be created. This
|
||||
// sample only has one Loader, so we don't care about the ID.
|
||||
Uri baseUri = KeyRings.buildPublicKeyRingsUri();
|
||||
|
||||
// These are the rows that we will retrieve.
|
||||
long now = new Date().getTime() / 1000;
|
||||
String[] projection = new String[] {
|
||||
KeyRings._ID,
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
UserIds.USER_ID,
|
||||
"(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
|
||||
+ " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
|
||||
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
|
||||
+ " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
|
||||
+ Keys.CAN_ENCRYPT + " = '1') AS "
|
||||
+ SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
|
||||
"(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
|
||||
+ " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
|
||||
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
|
||||
+ " AND valid_keys." + Keys.IS_REVOKED + " = '0' AND valid_keys."
|
||||
+ Keys.CAN_ENCRYPT + " = '1' AND valid_keys." + Keys.CREATION + " <= '"
|
||||
+ now + "' AND " + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys."
|
||||
+ Keys.EXPIRY + " >= '" + now + "')) AS "
|
||||
+ SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
|
||||
|
||||
String inMasterKeyList = null;
|
||||
if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) {
|
||||
inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
|
||||
for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) {
|
||||
if (i != 0) {
|
||||
inMasterKeyList += ", ";
|
||||
}
|
||||
inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]);
|
||||
}
|
||||
inMasterKeyList += ")";
|
||||
}
|
||||
|
||||
// if (searchString != null && searchString.trim().length() > 0) {
|
||||
// String[] chunks = searchString.trim().split(" +");
|
||||
// qb.appendWhere("(EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
|
||||
// + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "."
|
||||
// + Keys._ID);
|
||||
// for (int i = 0; i < chunks.length; ++i) {
|
||||
// qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
|
||||
// qb.appendWhereEscapeString("%" + chunks[i] + "%");
|
||||
// }
|
||||
// qb.appendWhere("))");
|
||||
//
|
||||
// if (inIdList != null) {
|
||||
// qb.appendWhere(" OR (" + inIdList + ")");
|
||||
// }
|
||||
// }
|
||||
|
||||
String orderBy = UserIds.USER_ID + " ASC";
|
||||
if (inMasterKeyList != null) {
|
||||
// sort by selected master keys
|
||||
orderBy = inMasterKeyList + " DESC, " + orderBy;
|
||||
}
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), baseUri, projection, null, null, orderBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// Swap the new cursor in. (The framework will take care of closing the
|
||||
// old cursor once we return.)
|
||||
mAdapter.swapCursor(data);
|
||||
|
||||
// The list should now be shown.
|
||||
if (isResumed()) {
|
||||
setListShown(true);
|
||||
} else {
|
||||
setListShownNoAnimation(true);
|
||||
}
|
||||
|
||||
// preselect given master keys
|
||||
preselectMasterKeyIds(mSelectedMasterKeyIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// This is called when the last Cursor provided to onLoadFinished()
|
||||
// above is about to be closed. We need to make sure we are no
|
||||
// longer using it.
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.actionbarsherlock.app.ActionBar;
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
|
||||
public class SelectSecretKeyActivity extends SherlockFragmentActivity {
|
||||
|
||||
// Actions for internal use only:
|
||||
public static final String ACTION_SELECT_SECRET_KEY = Constants.INTENT_PREFIX
|
||||
+ "SELECT_SECRET_KEYRING";
|
||||
|
||||
public static final String EXTRA_FILTER_CERTIFY = "filter_certify";
|
||||
|
||||
public static final String RESULT_EXTRA_MASTER_KEY_ID = "master_key_id";
|
||||
public static final String RESULT_EXTRA_USER_ID = "user_id";
|
||||
|
||||
private boolean mFilterCertify = false;
|
||||
private SelectSecretKeyFragment mSelectFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.select_secret_key_activity);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setHomeButtonEnabled(false);
|
||||
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
// TODO: reimplement!
|
||||
// mFilterLayout = findViewById(R.id.layout_filter);
|
||||
// mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
|
||||
// mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
|
||||
//
|
||||
// mClearFilterButton.setOnClickListener(new OnClickListener() {
|
||||
// public void onClick(View v) {
|
||||
// handleIntent(new Intent());
|
||||
// }
|
||||
// });
|
||||
|
||||
mFilterCertify = getIntent().getBooleanExtra(EXTRA_FILTER_CERTIFY, false);
|
||||
|
||||
handleIntent(getIntent());
|
||||
|
||||
// Check that the activity is using the layout version with
|
||||
// the fragment_container FrameLayout
|
||||
if (findViewById(R.id.select_secret_key_fragment_container) != null) {
|
||||
|
||||
// However, if we're being restored from a previous state,
|
||||
// then we don't need to do anything and should return or else
|
||||
// we could end up with overlapping fragments.
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance of the fragment
|
||||
mSelectFragment = SelectSecretKeyFragment.newInstance(mFilterCertify);
|
||||
|
||||
// Add the fragment to the 'fragment_container' FrameLayout
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.select_secret_key_fragment_container, mSelectFragment).commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is executed by SelectSecretKeyFragment after clicking on an item
|
||||
*
|
||||
* @param masterKeyId
|
||||
* @param userId
|
||||
*/
|
||||
public void afterListSelection(long masterKeyId, String userId) {
|
||||
Intent data = new Intent();
|
||||
data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, masterKeyId);
|
||||
data.putExtra(RESULT_EXTRA_USER_ID, (String) userId);
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
// TODO: reimplement!
|
||||
|
||||
// String searchString = null;
|
||||
// if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
// searchString = intent.getStringExtra(SearchManager.QUERY);
|
||||
// if (searchString != null && searchString.trim().length() == 0) {
|
||||
// searchString = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (searchString == null) {
|
||||
// mFilterLayout.setVisibility(View.GONE);
|
||||
// } else {
|
||||
// mFilterLayout.setVisibility(View.VISIBLE);
|
||||
// mFilterInfo.setText(getString(R.string.filterInfo, searchString));
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// TODO: reimplement!
|
||||
// menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon(
|
||||
// android.R.drawable.ic_menu_search);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockListFragment;
|
||||
|
||||
public class SelectSecretKeyFragment extends SherlockListFragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private SelectSecretKeyActivity mActivity;
|
||||
private SelectKeyCursorAdapter mAdapter;
|
||||
private ListView mListView;
|
||||
|
||||
private boolean mFilterCertify;
|
||||
|
||||
private static final String ARG_FILTER_CERTIFY = "filter_certify";
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static SelectSecretKeyFragment newInstance(boolean filterCertify) {
|
||||
SelectSecretKeyFragment frag = new SelectSecretKeyFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putBoolean(ARG_FILTER_CERTIFY, filterCertify);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mFilterCertify = getArguments().getBoolean(ARG_FILTER_CERTIFY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define Adapter and Loader on create of Activity
|
||||
*/
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
mActivity = (SelectSecretKeyActivity) getSherlockActivity();
|
||||
mListView = getListView();
|
||||
|
||||
mListView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
long masterKeyId = mAdapter.getMasterKeyId(position);
|
||||
String userId = mAdapter.getUserId(position);
|
||||
|
||||
// return data to activity, which results in finishing it
|
||||
mActivity.afterListSelection(masterKeyId, userId);
|
||||
}
|
||||
});
|
||||
|
||||
// Give some text to display if there is no data. In a real
|
||||
// application this would come from a resource.
|
||||
setEmptyText(getString(R.string.list_empty));
|
||||
|
||||
mAdapter = new SelectKeyCursorAdapter(mActivity, null, 0, mListView, Id.type.secret_key);
|
||||
|
||||
setListAdapter(mAdapter);
|
||||
|
||||
// Start out with a progress indicator.
|
||||
setListShown(false);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
// This is called when a new Loader needs to be created. This
|
||||
// sample only has one Loader, so we don't care about the ID.
|
||||
Uri baseUri = KeyRings.buildSecretKeyRingsUri();
|
||||
|
||||
String CapFilter = null;
|
||||
if (mFilterCertify) {
|
||||
CapFilter = "(cert > 0)";
|
||||
}
|
||||
|
||||
// These are the rows that we will retrieve.
|
||||
long now = new Date().getTime() / 1000;
|
||||
String[] projection = new String[] {
|
||||
KeyRings._ID,
|
||||
KeyRings.MASTER_KEY_ID,
|
||||
UserIds.USER_ID,
|
||||
"(SELECT COUNT(cert_keys." + Keys._ID + ") FROM " + Tables.KEYS
|
||||
+ " AS cert_keys WHERE cert_keys." + Keys.KEY_RING_ROW_ID + " = "
|
||||
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND cert_keys."
|
||||
+ Keys.CAN_CERTIFY + " = '1') AS cert",
|
||||
"(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
|
||||
+ " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
|
||||
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
|
||||
+ " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
|
||||
+ Keys.CAN_SIGN + " = '1') AS "
|
||||
+ SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
|
||||
"(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
|
||||
+ " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
|
||||
+ KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys."
|
||||
+ Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_SIGN
|
||||
+ " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND "
|
||||
+ "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY
|
||||
+ " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
|
||||
|
||||
// if (searchString != null && searchString.trim().length() > 0) {
|
||||
// String[] chunks = searchString.trim().split(" +");
|
||||
// qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
|
||||
// + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "."
|
||||
// + Keys._ID);
|
||||
// for (int i = 0; i < chunks.length; ++i) {
|
||||
// qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
|
||||
// qb.appendWhereEscapeString("%" + chunks[i] + "%");
|
||||
// }
|
||||
// qb.appendWhere(")");
|
||||
// }
|
||||
|
||||
String orderBy = UserIds.USER_ID + " ASC";
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), baseUri, projection, CapFilter, null, orderBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// Swap the new cursor in. (The framework will take care of closing the
|
||||
// old cursor once we return.)
|
||||
mAdapter.swapCursor(data);
|
||||
|
||||
// The list should now be shown.
|
||||
if (isResumed()) {
|
||||
setListShown(true);
|
||||
} else {
|
||||
setListShownNoAnimation(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
// This is called when the last Cursor provided to onLoadFinished()
|
||||
// above is about to be closed. We need to make sure we are no
|
||||
// longer using it.
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class SelectSecretKeyLayoutFragment extends Fragment {
|
||||
|
||||
private TextView mKeyUserId;
|
||||
private TextView mKeyUserIdRest;
|
||||
private BootstrapButton mSelectKeyButton;
|
||||
|
||||
private SelectSecretKeyCallback mCallback;
|
||||
|
||||
private static final int REQUEST_CODE_SELECT_KEY = 8882;
|
||||
|
||||
public interface SelectSecretKeyCallback {
|
||||
void onKeySelected(long secretKeyId);
|
||||
}
|
||||
|
||||
public void setCallback(SelectSecretKeyCallback callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
public void selectKey(long secretKeyId) {
|
||||
if (secretKeyId == Id.key.none) {
|
||||
mKeyUserId.setText(R.string.api_settings_no_key);
|
||||
mKeyUserIdRest.setText("");
|
||||
} else {
|
||||
String uid = getResources().getString(R.string.unknown_user_id);
|
||||
String uidExtra = "";
|
||||
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
|
||||
getActivity(), secretKeyId);
|
||||
if (keyRing != null) {
|
||||
PGPSecretKey key = PgpKeyHelper.getMasterKey(keyRing);
|
||||
if (key != null) {
|
||||
String userId = PgpKeyHelper.getMainUserIdSafe(getActivity(), key);
|
||||
String chunks[] = userId.split(" <", 2);
|
||||
uid = chunks[0];
|
||||
if (chunks.length > 1) {
|
||||
uidExtra = "<" + chunks[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
mKeyUserId.setText(uid);
|
||||
mKeyUserIdRest.setText(uidExtra);
|
||||
}
|
||||
}
|
||||
|
||||
public void setError(String error) {
|
||||
mKeyUserId.requestFocus();
|
||||
mKeyUserId.setError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the layout for this fragment
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.select_secret_key_layout_fragment, container, false);
|
||||
|
||||
mKeyUserId = (TextView) view.findViewById(R.id.select_secret_key_user_id);
|
||||
mKeyUserIdRest = (TextView) view.findViewById(R.id.select_secret_key_user_id_rest);
|
||||
mSelectKeyButton = (BootstrapButton) view
|
||||
.findViewById(R.id.select_secret_key_select_key_button);
|
||||
mSelectKeyButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
startSelectKeyActivity();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void startSelectKeyActivity() {
|
||||
Intent intent = new Intent(getActivity(), SelectSecretKeyActivity.class);
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode & 0xFFFF) {
|
||||
case REQUEST_CODE_SELECT_KEY: {
|
||||
long secretKeyId;
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Bundle bundle = data.getExtras();
|
||||
secretKeyId = bundle.getLong(SelectSecretKeyActivity.RESULT_EXTRA_MASTER_KEY_ID);
|
||||
|
||||
selectKey(secretKeyId);
|
||||
|
||||
// remove displayed errors
|
||||
mKeyUserId.setError(null);
|
||||
|
||||
// give value back to callback
|
||||
mCallback.onKeySelected(secretKeyId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Senecaso
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSignature;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.Preferences;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.ActionBar;
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
/**
|
||||
* gpg --sign-key
|
||||
*
|
||||
* signs the specified public key with the specified secret master key
|
||||
*/
|
||||
public class SignKeyActivity extends SherlockFragmentActivity implements
|
||||
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback {
|
||||
|
||||
public static final String EXTRA_KEY_ID = "key_id";
|
||||
|
||||
private long mPubKeyId = 0;
|
||||
private long mMasterKeyId = 0;
|
||||
|
||||
private BootstrapButton mSignButton;
|
||||
private CheckBox mUploadKeyCheckbox;
|
||||
private Spinner mSelectKeyserverSpinner;
|
||||
|
||||
private SelectSecretKeyLayoutFragment mSelectKeyFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.sign_key_activity);
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setHomeButtonEnabled(false);
|
||||
|
||||
mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.sign_key_select_key_fragment);
|
||||
mSelectKeyFragment.setCallback(this);
|
||||
|
||||
mSelectKeyserverSpinner = (Spinner) findViewById(R.id.sign_key_keyserver);
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
|
||||
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
|
||||
.getKeyServers());
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
mSelectKeyserverSpinner.setAdapter(adapter);
|
||||
|
||||
mUploadKeyCheckbox = (CheckBox) findViewById(R.id.sign_key_upload_checkbox);
|
||||
if (!mUploadKeyCheckbox.isChecked()) {
|
||||
mSelectKeyserverSpinner.setEnabled(false);
|
||||
} else {
|
||||
mSelectKeyserverSpinner.setEnabled(true);
|
||||
}
|
||||
|
||||
mUploadKeyCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (!isChecked) {
|
||||
mSelectKeyserverSpinner.setEnabled(false);
|
||||
} else {
|
||||
mSelectKeyserverSpinner.setEnabled(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mSignButton = (BootstrapButton) findViewById(R.id.sign_key_sign_button);
|
||||
mSignButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mPubKeyId != 0) {
|
||||
if (mMasterKeyId == 0) {
|
||||
mSelectKeyFragment.setError(getString(R.string.select_key_to_sign));
|
||||
} else {
|
||||
initiateSigning();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mPubKeyId = getIntent().getLongExtra(EXTRA_KEY_ID, 0);
|
||||
if (mPubKeyId == 0) {
|
||||
Log.e(Constants.TAG, "No pub key id given!");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void showPassphraseDialog(final long secretKeyId) {
|
||||
// Message is received after passphrase is cached
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
|
||||
startSigning();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
try {
|
||||
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
|
||||
messenger, secretKeyId);
|
||||
|
||||
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
|
||||
} catch (PgpGeneralException e) {
|
||||
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
|
||||
// send message to handler to start encryption directly
|
||||
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles the UI bits of the signing process on the UI thread
|
||||
*/
|
||||
private void initiateSigning() {
|
||||
PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, mPubKeyId);
|
||||
if (pubring != null) {
|
||||
// if we have already signed this key, dont bother doing it again
|
||||
boolean alreadySigned = false;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSignature> itr = pubring.getPublicKey(mPubKeyId).getSignatures();
|
||||
while (itr.hasNext()) {
|
||||
PGPSignature sig = itr.next();
|
||||
if (sig.getKeyID() == mMasterKeyId) {
|
||||
alreadySigned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadySigned) {
|
||||
/*
|
||||
* get the user's passphrase for this key (if required)
|
||||
*/
|
||||
String passphrase = PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId);
|
||||
if (passphrase == null) {
|
||||
showPassphraseDialog(mMasterKeyId);
|
||||
return; // bail out; need to wait until the user has entered the passphrase
|
||||
// before trying again
|
||||
} else {
|
||||
startSigning();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.key_has_already_been_signed, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* kicks off the actual signing process on a background thread
|
||||
*/
|
||||
private void startSigning() {
|
||||
// Send all information needed to service to sign key in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_SIGN_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
data.putLong(KeychainIntentService.SIGN_KEY_MASTER_KEY_ID, mMasterKeyId);
|
||||
data.putLong(KeychainIntentService.SIGN_KEY_PUB_KEY_ID, mPubKeyId);
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after signing is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_signing, ProgressDialog.STYLE_SPINNER) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
|
||||
Toast.makeText(SignKeyActivity.this, R.string.key_sign_success,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
// check if we need to send the key to the server or not
|
||||
if (mUploadKeyCheckbox.isChecked()) {
|
||||
/*
|
||||
* upload the newly signed key to the key server
|
||||
*/
|
||||
uploadKey();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void uploadKey() {
|
||||
// Send all information needed to service to upload key in other thread
|
||||
Intent intent = new Intent(this, KeychainIntentService.class);
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
data.putLong(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, mPubKeyId);
|
||||
|
||||
Spinner keyServer = (Spinner) findViewById(R.id.sign_key_keyserver);
|
||||
String server = (String) keyServer.getSelectedItem();
|
||||
data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
|
||||
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
// Message is received after uploading is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
|
||||
R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
|
||||
Toast.makeText(SignKeyActivity.this, R.string.key_send_success,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
finish();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
saveHandler.showProgressDialog(this);
|
||||
|
||||
// start service with intent
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* callback from select key fragment
|
||||
*/
|
||||
@Override
|
||||
public void onKeySelected(long secretKeyId) {
|
||||
mMasterKeyId = secretKeyId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2013 Bahtiar 'kalkin' Gadimov
|
||||
*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
|
||||
import org.sufficientlysecure.keychain.helper.ExportHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockFragmentActivity;
|
||||
import com.actionbarsherlock.view.Menu;
|
||||
import com.actionbarsherlock.view.MenuItem;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class ViewKeyActivity extends SherlockFragmentActivity implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
ExportHelper mExportHelper;
|
||||
|
||||
protected Uri mDataUri;
|
||||
|
||||
private TextView mName;
|
||||
private TextView mEmail;
|
||||
private TextView mComment;
|
||||
private TextView mAlgorithm;
|
||||
private TextView mKeyId;
|
||||
private TextView mExpiry;
|
||||
private TextView mCreation;
|
||||
private TextView mFingerprint;
|
||||
private BootstrapButton mActionEncrypt;
|
||||
|
||||
private ListView mUserIds;
|
||||
private ListView mKeys;
|
||||
|
||||
private static final int LOADER_ID_KEYRING = 0;
|
||||
private static final int LOADER_ID_USER_IDS = 1;
|
||||
private static final int LOADER_ID_KEYS = 2;
|
||||
private ViewKeyUserIdsAdapter mUserIdsAdapter;
|
||||
private ViewKeyKeysAdapter mKeysAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mExportHelper = new ExportHelper(this);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setIcon(android.R.color.transparent);
|
||||
getSupportActionBar().setHomeButtonEnabled(true);
|
||||
|
||||
setContentView(R.layout.view_key_activity);
|
||||
|
||||
mName = (TextView) findViewById(R.id.name);
|
||||
mEmail = (TextView) findViewById(R.id.email);
|
||||
mComment = (TextView) findViewById(R.id.comment);
|
||||
mKeyId = (TextView) findViewById(R.id.key_id);
|
||||
mAlgorithm = (TextView) findViewById(R.id.algorithm);
|
||||
mCreation = (TextView) findViewById(R.id.creation);
|
||||
mExpiry = (TextView) findViewById(R.id.expiry);
|
||||
mFingerprint = (TextView) findViewById(R.id.fingerprint);
|
||||
mActionEncrypt = (BootstrapButton) findViewById(R.id.action_encrypt);
|
||||
mUserIds = (ListView) findViewById(R.id.user_ids);
|
||||
mKeys = (ListView) findViewById(R.id.keys);
|
||||
|
||||
Intent intent = getIntent();
|
||||
mDataUri = intent.getData();
|
||||
if (mDataUri == null) {
|
||||
Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
|
||||
finish();
|
||||
return;
|
||||
} else {
|
||||
Log.d(Constants.TAG, "uri: " + mDataUri);
|
||||
loadData(mDataUri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getSupportMenuInflater().inflate(R.menu.key_view, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
Intent homeIntent = new Intent(this, KeyListPublicActivity.class);
|
||||
homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(homeIntent);
|
||||
return true;
|
||||
case R.id.menu_key_view_update:
|
||||
updateFromKeyserver(mDataUri);
|
||||
return true;
|
||||
case R.id.menu_key_view_sign:
|
||||
signKey(mDataUri);
|
||||
return true;
|
||||
case R.id.menu_key_view_export_keyserver:
|
||||
uploadToKeyserver(mDataUri);
|
||||
return true;
|
||||
case R.id.menu_key_view_export_file:
|
||||
mExportHelper.showExportKeysDialog(mDataUri, Id.type.public_key, Constants.path.APP_DIR
|
||||
+ "/pubexport.asc");
|
||||
return true;
|
||||
case R.id.menu_key_view_share_default_fingerprint:
|
||||
shareKey(mDataUri, true);
|
||||
return true;
|
||||
case R.id.menu_key_view_share_default:
|
||||
shareKey(mDataUri, false);
|
||||
return true;
|
||||
case R.id.menu_key_view_share_qr_code_fingerprint:
|
||||
shareKeyQrCode(mDataUri, true);
|
||||
return true;
|
||||
case R.id.menu_key_view_share_qr_code:
|
||||
shareKeyQrCode(mDataUri, false);
|
||||
return true;
|
||||
case R.id.menu_key_view_share_nfc:
|
||||
shareNfc();
|
||||
return true;
|
||||
case R.id.menu_key_view_share_clipboard:
|
||||
copyToClipboard(mDataUri);
|
||||
return true;
|
||||
case R.id.menu_key_view_delete: {
|
||||
// Message is received after key is deleted
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mExportHelper.deleteKey(mDataUri, Id.type.public_key, returnHandler);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void loadData(Uri dataUri) {
|
||||
mActionEncrypt.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// TODO: don't get object here!!! solve this differently!
|
||||
PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(
|
||||
ViewKeyActivity.this, mDataUri);
|
||||
PGPPublicKey publicKey = ring.getPublicKey();
|
||||
|
||||
long[] encryptionKeyIds = new long[] { publicKey.getKeyID() };
|
||||
Intent intent = new Intent(ViewKeyActivity.this, EncryptActivity.class);
|
||||
intent.setAction(EncryptActivity.ACTION_ENCRYPT);
|
||||
intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds);
|
||||
// used instead of startActivity set actionbar based on callingPackage
|
||||
startActivityForResult(intent, 0);
|
||||
}
|
||||
});
|
||||
|
||||
mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0);
|
||||
|
||||
mUserIds.setAdapter(mUserIdsAdapter);
|
||||
// mUserIds.setEmptyView(findViewById(android.R.id.empty));
|
||||
// mUserIds.setClickable(true);
|
||||
// mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
// @Override
|
||||
// public void onItemClick(AdapterView<?> arg0, View arg1, int position, long id) {
|
||||
// }
|
||||
// });
|
||||
|
||||
mKeysAdapter = new ViewKeyKeysAdapter(this, null, 0);
|
||||
|
||||
mKeys.setAdapter(mKeysAdapter);
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this);
|
||||
getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
|
||||
getSupportLoaderManager().initLoader(LOADER_ID_KEYS, null, this);
|
||||
}
|
||||
|
||||
static final String[] KEYRING_PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
|
||||
UserIds.USER_ID };
|
||||
static final int KEYRING_INDEX_ID = 0;
|
||||
static final int KEYRING_INDEX_MASTER_KEY_ID = 1;
|
||||
static final int KEYRING_INDEX_USER_ID = 2;
|
||||
|
||||
static final String[] USER_IDS_PROJECTION = new String[] { UserIds._ID, UserIds.USER_ID,
|
||||
UserIds.RANK, };
|
||||
// not the main user id
|
||||
static final String USER_IDS_SELECTION = UserIds.RANK + " > 0 ";
|
||||
static final String USER_IDS_SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC";
|
||||
|
||||
static final String[] KEYS_PROJECTION = new String[] { Keys._ID, Keys.KEY_ID,
|
||||
Keys.IS_MASTER_KEY, Keys.ALGORITHM, Keys.KEY_SIZE, Keys.CAN_CERTIFY, Keys.CAN_SIGN,
|
||||
Keys.CAN_ENCRYPT, Keys.CREATION, Keys.EXPIRY };
|
||||
static final String KEYS_SORT_ORDER = Keys.RANK + " ASC";
|
||||
static final int KEYS_INDEX_ID = 0;
|
||||
static final int KEYS_INDEX_KEY_ID = 1;
|
||||
static final int KEYS_INDEX_IS_MASTER_KEY = 2;
|
||||
static final int KEYS_INDEX_ALGORITHM = 3;
|
||||
static final int KEYS_INDEX_KEY_SIZE = 4;
|
||||
static final int KEYS_INDEX_CAN_CERTIFY = 5;
|
||||
static final int KEYS_INDEX_CAN_SIGN = 6;
|
||||
static final int KEYS_INDEX_CAN_ENCRYPT = 7;
|
||||
static final int KEYS_INDEX_CREATION = 8;
|
||||
static final int KEYS_INDEX_EXPIRY = 9;
|
||||
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
switch (id) {
|
||||
case LOADER_ID_KEYRING: {
|
||||
Uri baseUri = mDataUri;
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(this, baseUri, KEYRING_PROJECTION, null, null, null);
|
||||
}
|
||||
case LOADER_ID_USER_IDS: {
|
||||
Uri baseUri = UserIds.buildUserIdsUri(mDataUri);
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(this, baseUri, USER_IDS_PROJECTION, USER_IDS_SELECTION, null,
|
||||
USER_IDS_SORT_ORDER);
|
||||
}
|
||||
case LOADER_ID_KEYS: {
|
||||
Uri baseUri = Keys.buildKeysUri(mDataUri);
|
||||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(this, baseUri, KEYS_PROJECTION, null, null, KEYS_SORT_ORDER);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
// Swap the new cursor in. (The framework will take care of closing the
|
||||
// old cursor once we return.)
|
||||
switch (loader.getId()) {
|
||||
case LOADER_ID_KEYRING:
|
||||
if (data.moveToFirst()) {
|
||||
// get name, email, and comment from USER_ID
|
||||
String[] mainUserId = PgpKeyHelper.splitUserId(data
|
||||
.getString(KEYRING_INDEX_USER_ID));
|
||||
setTitle(mainUserId[0]);
|
||||
mName.setText(mainUserId[0]);
|
||||
mEmail.setText(mainUserId[1]);
|
||||
mComment.setText(mainUserId[2]);
|
||||
}
|
||||
|
||||
break;
|
||||
case LOADER_ID_USER_IDS:
|
||||
mUserIdsAdapter.swapCursor(data);
|
||||
break;
|
||||
case LOADER_ID_KEYS:
|
||||
// the first key here is our master key
|
||||
if (data.moveToFirst()) {
|
||||
// get key id from MASTER_KEY_ID
|
||||
long keyId = data.getLong(KEYS_INDEX_KEY_ID);
|
||||
|
||||
String keyIdStr = "0x" + PgpKeyHelper.convertKeyIdToHex(keyId);
|
||||
mKeyId.setText(keyIdStr);
|
||||
|
||||
// get creation date from CREATION
|
||||
if (data.isNull(KEYS_INDEX_CREATION)) {
|
||||
mCreation.setText(R.string.none);
|
||||
} else {
|
||||
Date creationDate = new Date(data.getLong(KEYS_INDEX_CREATION) * 1000);
|
||||
|
||||
mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(
|
||||
creationDate));
|
||||
}
|
||||
|
||||
// get creation date from EXPIRY
|
||||
if (data.isNull(KEYS_INDEX_EXPIRY)) {
|
||||
mExpiry.setText(R.string.none);
|
||||
} else {
|
||||
Date expiryDate = new Date(data.getLong(KEYS_INDEX_EXPIRY) * 1000);
|
||||
|
||||
mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(
|
||||
expiryDate));
|
||||
}
|
||||
|
||||
String algorithmStr = PgpKeyHelper.getAlgorithmInfo(
|
||||
data.getInt(KEYS_INDEX_ALGORITHM), data.getInt(KEYS_INDEX_KEY_SIZE));
|
||||
mAlgorithm.setText(algorithmStr);
|
||||
|
||||
// TODO: Can this be done better? fingerprint in db?
|
||||
String fingerprint = PgpKeyHelper.getFingerPrint(this, keyId);
|
||||
|
||||
fingerprint = fingerprint.replace(" ", "\n");
|
||||
mFingerprint.setText(fingerprint);
|
||||
|
||||
// TODO: get image with getUserAttributes() on key and then
|
||||
// PGPUserAttributeSubpacketVector
|
||||
}
|
||||
|
||||
mKeysAdapter.swapCursor(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the last Cursor provided to onLoadFinished() above is about to be closed.
|
||||
* We need to make sure we are no longer using it.
|
||||
*/
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
switch (loader.getId()) {
|
||||
case LOADER_ID_KEYRING:
|
||||
// TODO?
|
||||
break;
|
||||
case LOADER_ID_USER_IDS:
|
||||
mUserIdsAdapter.swapCursor(null);
|
||||
break;
|
||||
case LOADER_ID_KEYS:
|
||||
mKeysAdapter.swapCursor(null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadToKeyserver(Uri dataUri) {
|
||||
long keyRingRowId = Long.valueOf(dataUri.getLastPathSegment());
|
||||
|
||||
Intent uploadIntent = new Intent(this, KeyServerUploadActivity.class);
|
||||
uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER);
|
||||
uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId);
|
||||
startActivityForResult(uploadIntent, Id.request.export_to_server);
|
||||
}
|
||||
|
||||
private void updateFromKeyserver(Uri dataUri) {
|
||||
long updateKeyId = 0;
|
||||
PGPPublicKeyRing updateRing = (PGPPublicKeyRing) ProviderHelper
|
||||
.getPGPKeyRing(this, dataUri);
|
||||
|
||||
if (updateRing != null) {
|
||||
updateKeyId = PgpKeyHelper.getMasterKey(updateRing).getKeyID();
|
||||
}
|
||||
if (updateKeyId == 0) {
|
||||
Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent queryIntent = new Intent(this, KeyServerQueryActivity.class);
|
||||
queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN);
|
||||
queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId);
|
||||
|
||||
// TODO: lookup??
|
||||
startActivityForResult(queryIntent, Id.request.look_up_key_id);
|
||||
}
|
||||
|
||||
private void signKey(Uri dataUri) {
|
||||
long keyId = 0;
|
||||
PGPPublicKeyRing signKey = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri);
|
||||
|
||||
if (signKey != null) {
|
||||
keyId = PgpKeyHelper.getMasterKey(signKey).getKeyID();
|
||||
}
|
||||
if (keyId == 0) {
|
||||
Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent signIntent = new Intent(this, SignKeyActivity.class);
|
||||
signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId);
|
||||
startActivity(signIntent);
|
||||
}
|
||||
|
||||
private void shareKey(Uri dataUri, boolean fingerprintOnly) {
|
||||
String content = null;
|
||||
if (fingerprintOnly) {
|
||||
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
|
||||
|
||||
// TODO: dublicated in ShareQrCodeDialog
|
||||
content = "openpgp4fpr:";
|
||||
|
||||
String fingerprint = PgpKeyHelper.convertKeyToHex(masterKeyId);
|
||||
|
||||
content = content + fingerprint;
|
||||
} else {
|
||||
// get public keyring as ascii armored string
|
||||
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
|
||||
ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this,
|
||||
dataUri, new long[] { masterKeyId });
|
||||
|
||||
content = keyringArmored.get(0);
|
||||
|
||||
// Android will fail with android.os.TransactionTooLargeException if key is too big
|
||||
// see http://www.lonestarprod.com/?p=34
|
||||
if (content.length() >= 86389) {
|
||||
Toast.makeText(getApplicationContext(), R.string.key_too_big_for_sharing,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// let user choose application
|
||||
Intent sendIntent = new Intent(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, content);
|
||||
sendIntent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(sendIntent,
|
||||
getResources().getText(R.string.action_share_key_with)));
|
||||
}
|
||||
|
||||
private void shareKeyQrCode(Uri dataUri, boolean fingerprintOnly) {
|
||||
ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(dataUri,
|
||||
fingerprintOnly);
|
||||
dialog.show(getSupportFragmentManager(), "shareQrCodeDialog");
|
||||
}
|
||||
|
||||
private void copyToClipboard(Uri dataUri) {
|
||||
// get public keyring as ascii armored string
|
||||
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
|
||||
ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri,
|
||||
new long[] { masterKeyId });
|
||||
|
||||
ClipboardReflection.copyToClipboard(this, keyringArmored.get(0));
|
||||
Toast.makeText(getApplicationContext(), R.string.key_copied_to_clipboard, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void shareNfc() {
|
||||
ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance();
|
||||
dialog.show(getSupportFragmentManager(), "shareNfcDialog");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2013 Bahtiar 'kalkin' Gadimov
|
||||
*
|
||||
* 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 org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.nfc.NdefMessage;
|
||||
import android.nfc.NdefRecord;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.NfcAdapter.CreateNdefMessageCallback;
|
||||
import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
|
||||
import android.nfc.NfcEvent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class ViewKeyActivityJB extends ViewKeyActivity implements CreateNdefMessageCallback,
|
||||
OnNdefPushCompleteCallback, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
// NFC
|
||||
private NfcAdapter mNfcAdapter;
|
||||
private byte[] mSharedKeyringBytes;
|
||||
private static final int NFC_SENT = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
initNfc(mDataUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC: Initialize NFC sharing if OS and device supports it
|
||||
*/
|
||||
private void initNfc(Uri dataUri) {
|
||||
// check if NFC Beam is supported (>= Android 4.1)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
// Check for available NFC Adapter
|
||||
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
if (mNfcAdapter != null) {
|
||||
// init nfc
|
||||
|
||||
// get public keyring as byte array
|
||||
long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
|
||||
mSharedKeyringBytes = ProviderHelper.getKeyRingsAsByteArray(this, dataUri,
|
||||
new long[] { masterKeyId });
|
||||
|
||||
// Register callback to set NDEF message
|
||||
mNfcAdapter.setNdefPushMessageCallback(this, this);
|
||||
// Register callback to listen for message-sent success
|
||||
mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC: Implementation for the CreateNdefMessageCallback interface
|
||||
*/
|
||||
@Override
|
||||
public NdefMessage createNdefMessage(NfcEvent event) {
|
||||
/**
|
||||
* When a device receives a push with an AAR in it, the application specified in the AAR is
|
||||
* guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to
|
||||
* guarantee that this activity starts when receiving a beamed message. For now, this code
|
||||
* uses the tag dispatch system.
|
||||
*/
|
||||
NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME,
|
||||
mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME));
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC: Implementation for the OnNdefPushCompleteCallback interface
|
||||
*/
|
||||
@Override
|
||||
public void onNdefPushComplete(NfcEvent arg0) {
|
||||
// A handler is needed to send messages to the activity when this
|
||||
// callback occurs, because it happens from a binder thread
|
||||
mNfcHandler.obtainMessage(NFC_SENT).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC: This handler receives a message from onNdefPushComplete
|
||||
*/
|
||||
private final Handler mNfcHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case NFC_SENT:
|
||||
Toast.makeText(getApplicationContext(), R.string.nfc_successfull, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.adapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> {
|
||||
protected LayoutInflater mInflater;
|
||||
protected Activity mActivity;
|
||||
|
||||
protected List<ImportKeysListEntry> data;
|
||||
|
||||
public ImportKeysAdapter(Activity activity) {
|
||||
super(activity, -1);
|
||||
mActivity = activity;
|
||||
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public void setData(List<ImportKeysListEntry> data) {
|
||||
clear();
|
||||
if (data != null) {
|
||||
this.data = data;
|
||||
|
||||
// add data to extended ArrayAdapter
|
||||
if (Build.VERSION.SDK_INT >= 11) {
|
||||
addAll(data);
|
||||
} else {
|
||||
for (ImportKeysListEntry entry : data) {
|
||||
add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<ImportKeysListEntry> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
ImportKeysListEntry entry = data.get(position);
|
||||
|
||||
View view = mInflater.inflate(R.layout.import_keys_list_entry, null);
|
||||
|
||||
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
|
||||
mainUserId.setText(R.string.unknown_user_id);
|
||||
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
|
||||
mainUserIdRest.setText("");
|
||||
TextView keyId = (TextView) view.findViewById(R.id.keyId);
|
||||
keyId.setText(R.string.no_key);
|
||||
TextView fingerprint = (TextView) view.findViewById(R.id.fingerprint);
|
||||
TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
|
||||
algorithm.setText("");
|
||||
TextView status = (TextView) view.findViewById(R.id.status);
|
||||
status.setText("");
|
||||
|
||||
String userId = entry.userIds.get(0);
|
||||
if (userId != null) {
|
||||
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
|
||||
|
||||
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
|
||||
// show red user id if it is a secret key
|
||||
if (entry.secretKey) {
|
||||
userId = mActivity.getString(R.string.secret_key) + " " + userId;
|
||||
mainUserId.setTextColor(Color.RED);
|
||||
} else {
|
||||
mainUserId.setText(userIdSplit[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
|
||||
mainUserIdRest.setText(userIdSplit[1]);
|
||||
mainUserIdRest.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mainUserIdRest.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
keyId.setText(entry.hexKeyId);
|
||||
fingerprint.setText(mActivity.getString(R.string.fingerprint) + " " + entry.fingerPrint);
|
||||
|
||||
algorithm.setText("" + entry.bitStrength + "/" + entry.algorithm);
|
||||
|
||||
if (entry.revoked) {
|
||||
status.setText("revoked");
|
||||
} else {
|
||||
status.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
|
||||
if (entry.userIds.size() == 1) {
|
||||
ll.setVisibility(View.GONE);
|
||||
} else {
|
||||
boolean first = true;
|
||||
boolean second = true;
|
||||
for (String uid : entry.userIds) {
|
||||
if (first) {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
if (!second) {
|
||||
View sep = new View(mActivity);
|
||||
sep.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
|
||||
sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
|
||||
ll.addView(sep);
|
||||
}
|
||||
TextView uidView = (TextView) mInflater.inflate(
|
||||
R.layout.import_keys_list_entry_user_id, null);
|
||||
uidView.setText(uid);
|
||||
ll.addView(uidView);
|
||||
second = false;
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox cBox = (CheckBox) view.findViewById(R.id.selected);
|
||||
cBox.setChecked(entry.isSelected());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.adapter;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.util.AlgorithmNames;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
|
||||
public class ImportKeysListEntry implements Serializable {
|
||||
private static final long serialVersionUID = -7797972103284992662L;
|
||||
public ArrayList<String> userIds;
|
||||
public long keyId;
|
||||
|
||||
public boolean revoked;
|
||||
// public Date date;
|
||||
public String fingerPrint;
|
||||
public String hexKeyId;
|
||||
public int bitStrength;
|
||||
public String algorithm;
|
||||
public boolean secretKey;
|
||||
AlgorithmNames algorithmNames;
|
||||
|
||||
private boolean selected;
|
||||
|
||||
/**
|
||||
* Constructor for later querying from keyserver
|
||||
*/
|
||||
public ImportKeysListEntry() {
|
||||
secretKey = false;
|
||||
userIds = new ArrayList<String>();
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor based on key object, used for import from NFC, QR Codes, files
|
||||
*
|
||||
* @param pgpKey
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ImportKeysListEntry(PGPKeyRing pgpKeyRing) {
|
||||
// selected is default
|
||||
this.selected = true;
|
||||
|
||||
if (pgpKeyRing instanceof PGPSecretKeyRing) {
|
||||
secretKey = true;
|
||||
} else {
|
||||
secretKey = false;
|
||||
}
|
||||
|
||||
userIds = new ArrayList<String>();
|
||||
for (String userId : new IterableIterator<String>(pgpKeyRing.getPublicKey().getUserIDs())) {
|
||||
userIds.add(userId);
|
||||
}
|
||||
this.keyId = pgpKeyRing.getPublicKey().getKeyID();
|
||||
|
||||
this.revoked = pgpKeyRing.getPublicKey().isRevoked();
|
||||
this.fingerPrint = PgpKeyHelper.convertFingerprintToHex(pgpKeyRing.getPublicKey()
|
||||
.getFingerprint());
|
||||
this.hexKeyId = PgpKeyHelper.convertKeyIdToHex(keyId);
|
||||
this.bitStrength = pgpKeyRing.getPublicKey().getBitStrength();
|
||||
int algorithm = pgpKeyRing.getPublicKey().getAlgorithm();
|
||||
if (algorithm == PGPPublicKey.RSA_ENCRYPT || algorithm == PGPPublicKey.RSA_GENERAL
|
||||
|| algorithm == PGPPublicKey.RSA_SIGN) {
|
||||
this.algorithm = "RSA";
|
||||
} else if (algorithm == PGPPublicKey.DSA) {
|
||||
this.algorithm = "DSA";
|
||||
} else if (algorithm == PGPPublicKey.ELGAMAL_ENCRYPT
|
||||
|| algorithm == PGPPublicKey.ELGAMAL_GENERAL) {
|
||||
this.algorithm = "ElGamal";
|
||||
} else if (algorithm == PGPPublicKey.EC || algorithm == PGPPublicKey.ECDSA) {
|
||||
this.algorithm = "ECC";
|
||||
} else {
|
||||
// TODO: with resources
|
||||
this.algorithm = "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.adapter;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPObjectFactory;
|
||||
import org.spongycastle.openpgp.PGPUtil;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.util.InputData;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
|
||||
public class ImportKeysListLoader extends AsyncTaskLoader<List<ImportKeysListEntry>> {
|
||||
Context mContext;
|
||||
|
||||
InputData mInputData;
|
||||
|
||||
ArrayList<ImportKeysListEntry> data = new ArrayList<ImportKeysListEntry>();
|
||||
|
||||
public ImportKeysListLoader(Context context, InputData inputData) {
|
||||
super(context);
|
||||
this.mContext = context;
|
||||
this.mInputData = inputData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ImportKeysListEntry> loadInBackground() {
|
||||
if (mInputData == null) {
|
||||
Log.e(Constants.TAG, "Input data is null!");
|
||||
return data;
|
||||
}
|
||||
|
||||
generateListOfKeyrings(mInputData);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
|
||||
// Ensure the loader is stopped
|
||||
onStopLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
forceLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(List<ImportKeysListEntry> data) {
|
||||
super.deliverResult(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all PGPKeyRing objects from input
|
||||
*
|
||||
* @param keyringBytes
|
||||
* @return
|
||||
*/
|
||||
private void generateListOfKeyrings(InputData inputData) {
|
||||
PositionAwareInputStream progressIn = new PositionAwareInputStream(
|
||||
inputData.getInputStream());
|
||||
|
||||
// need to have access to the bufferedInput, so we can reuse it for the possible
|
||||
// PGPObject chunks after the first one, e.g. files with several consecutive ASCII
|
||||
// armour blocks
|
||||
BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
|
||||
try {
|
||||
|
||||
// read all available blocks... (asc files can contain many blocks with BEGIN END)
|
||||
while (bufferedInput.available() > 0) {
|
||||
InputStream in = PGPUtil.getDecoderStream(bufferedInput);
|
||||
PGPObjectFactory objectFactory = new PGPObjectFactory(in);
|
||||
|
||||
// go through all objects in this block
|
||||
Object obj;
|
||||
while ((obj = objectFactory.nextObject()) != null) {
|
||||
Log.d(Constants.TAG, "Found class: " + obj.getClass());
|
||||
|
||||
if (obj instanceof PGPKeyRing) {
|
||||
PGPKeyRing newKeyring = (PGPKeyRing) obj;
|
||||
addToData(newKeyring);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "Exception on parsing key file!", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addToData(PGPKeyRing keyring) {
|
||||
ImportKeysListEntry item = new ImportKeysListEntry(keyring);
|
||||
data.add(item);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.adapter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Implements StickyListHeadersAdapter from library
|
||||
*/
|
||||
public class KeyListPublicAdapter extends CursorAdapter implements StickyListHeadersAdapter {
|
||||
private LayoutInflater mInflater;
|
||||
private int mSectionColumnIndex;
|
||||
private int mIndexUserId;
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
private HashMap<Integer, Boolean> mSelection = new HashMap<Integer, Boolean>();
|
||||
|
||||
public KeyListPublicAdapter(Context context, Cursor c, int flags, int sectionColumnIndex) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
mSectionColumnIndex = sectionColumnIndex;
|
||||
initIndex(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
initIndex(newCursor);
|
||||
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
|
||||
* performance comparison see http://stackoverflow.com/a/17999582
|
||||
*
|
||||
* @param cursor
|
||||
*/
|
||||
private void initIndex(Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind cursor data to the item list view
|
||||
*
|
||||
* NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. Thus
|
||||
* no ViewHolder is required here.
|
||||
*/
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
|
||||
mainUserId.setText(R.string.unknown_user_id);
|
||||
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
|
||||
mainUserIdRest.setText("");
|
||||
|
||||
String userId = cursor.getString(mIndexUserId);
|
||||
if (userId != null) {
|
||||
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
|
||||
|
||||
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
|
||||
mainUserId.setText(userIdSplit[0]);
|
||||
}
|
||||
|
||||
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
|
||||
mainUserIdRest.setText(userIdSplit[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.key_list_item, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new header view and binds the section headers to it. It uses the ViewHolder
|
||||
* pattern. Most functionality is similar to getView() from Android's CursorAdapter.
|
||||
*
|
||||
* NOTE: The variables mDataValid and mCursor are available due to the super class
|
||||
* CursorAdapter.
|
||||
*/
|
||||
@Override
|
||||
public View getHeaderView(int position, View convertView, ViewGroup parent) {
|
||||
HeaderViewHolder holder;
|
||||
if (convertView == null) {
|
||||
holder = new HeaderViewHolder();
|
||||
convertView = mInflater.inflate(R.layout.key_list_public_header, parent, false);
|
||||
holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (HeaderViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
if (!mDataValid) {
|
||||
// no data available at this point
|
||||
Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
|
||||
return convertView;
|
||||
}
|
||||
|
||||
if (!mCursor.moveToPosition(position)) {
|
||||
throw new IllegalStateException("couldn't move cursor to position " + position);
|
||||
}
|
||||
|
||||
// set header text as first char in user id
|
||||
String userId = mCursor.getString(mSectionColumnIndex);
|
||||
String headerText = convertView.getResources().getString(R.string.unknown_user_id);
|
||||
if (userId != null && userId.length() > 0) {
|
||||
headerText = "" + mCursor.getString(mSectionColumnIndex).subSequence(0, 1).charAt(0);
|
||||
}
|
||||
holder.text.setText(headerText);
|
||||
return convertView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header IDs should be static, position=1 should always return the same Id that is.
|
||||
*/
|
||||
@Override
|
||||
public long getHeaderId(int position) {
|
||||
if (!mDataValid) {
|
||||
// no data available at this point
|
||||
Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!mCursor.moveToPosition(position)) {
|
||||
throw new IllegalStateException("couldn't move cursor to position " + position);
|
||||
}
|
||||
|
||||
// return the first character of the name as ID because this is what
|
||||
// headers private HashMap<Integer, Boolean> mSelection = new HashMap<Integer,
|
||||
// Boolean>();are based upon
|
||||
String userId = mCursor.getString(mSectionColumnIndex);
|
||||
if (userId != null && userId.length() > 0) {
|
||||
return userId.subSequence(0, 1).charAt(0);
|
||||
} else {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderViewHolder {
|
||||
TextView text;
|
||||
}
|
||||
|
||||
/** -------------------------- MULTI-SELECTION METHODS -------------- */
|
||||
public void setNewSelection(int position, boolean value) {
|
||||
mSelection.put(position, value);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean isPositionChecked(int position) {
|
||||
Boolean result = mSelection.get(position);
|
||||
return result == null ? false : result;
|
||||
}
|
||||
|
||||
public Set<Integer> getCurrentCheckedPosition() {
|
||||
return mSelection.keySet();
|
||||
}
|
||||
|
||||
public void removeSelection(int position) {
|
||||
mSelection.remove(position);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
mSelection.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
// let the adapter handle setting up the row views
|
||||
View v = super.getView(position, convertView, parent);
|
||||
|
||||
/**
|
||||
* Change color for multi-selection
|
||||
*/
|
||||
// default color
|
||||
v.setBackgroundColor(Color.TRANSPARENT);
|
||||
if (mSelection.get(position) != null) {
|
||||
// this is a selected position, change color!
|
||||
v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.adapter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class KeyListSecretAdapter extends CursorAdapter {
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
private int mIndexUserId;
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
private HashMap<Integer, Boolean> mSelection = new HashMap<Integer, Boolean>();
|
||||
|
||||
public KeyListSecretAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
initIndex(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
initIndex(newCursor);
|
||||
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
|
||||
* performance comparison see http://stackoverflow.com/a/17999582
|
||||
*
|
||||
* @param cursor
|
||||
*/
|
||||
private void initIndex(Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
|
||||
mainUserId.setText(R.string.unknown_user_id);
|
||||
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
|
||||
mainUserIdRest.setText("");
|
||||
|
||||
String userId = cursor.getString(mIndexUserId);
|
||||
if (userId != null) {
|
||||
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
|
||||
|
||||
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
|
||||
mainUserId.setText(userIdSplit[0]);
|
||||
}
|
||||
|
||||
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
|
||||
mainUserIdRest.setText(userIdSplit[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.key_list_item, null);
|
||||
}
|
||||
|
||||
/** -------------------------- MULTI-SELECTION METHODS -------------- */
|
||||
public void setNewSelection(int position, boolean value) {
|
||||
mSelection.put(position, value);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean isPositionChecked(int position) {
|
||||
Boolean result = mSelection.get(position);
|
||||
return result == null ? false : result;
|
||||
}
|
||||
|
||||
public Set<Integer> getCurrentCheckedPosition() {
|
||||
return mSelection.keySet();
|
||||
}
|
||||
|
||||
public void removeSelection(int position) {
|
||||
mSelection.remove(position);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
mSelection.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
// let the adapter handle setting up the row views
|
||||
View v = super.getView(position, convertView, parent);
|
||||
|
||||
/**
|
||||
* Change color for multi-selection
|
||||
*/
|
||||
// default color
|
||||
v.setBackgroundColor(Color.TRANSPARENT);
|
||||
if (mSelection.get(position) != null) {
|
||||
// this is a selected position, change color!
|
||||
v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.adapter;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
public class KeyValueSpinnerAdapter extends ArrayAdapter<String> {
|
||||
private final HashMap<Integer, String> mData;
|
||||
private final int[] mKeys;
|
||||
private final String[] mValues;
|
||||
|
||||
static <K, V extends Comparable<? super V>> SortedSet<Map.Entry<K, V>> entriesSortedByValues(
|
||||
Map<K, V> map) {
|
||||
SortedSet<Map.Entry<K, V>> sortedEntries = new TreeSet<Map.Entry<K, V>>(
|
||||
new Comparator<Map.Entry<K, V>>() {
|
||||
@Override
|
||||
public int compare(Map.Entry<K, V> e1, Map.Entry<K, V> e2) {
|
||||
return e1.getValue().compareTo(e2.getValue());
|
||||
}
|
||||
});
|
||||
sortedEntries.addAll(map.entrySet());
|
||||
return sortedEntries;
|
||||
}
|
||||
|
||||
public KeyValueSpinnerAdapter(Context context, HashMap<Integer, String> objects) {
|
||||
// To make the drop down a simple text box
|
||||
super(context, android.R.layout.simple_spinner_item);
|
||||
mData = objects;
|
||||
|
||||
// To make the drop down view a radio button list
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
SortedSet<Map.Entry<Integer, String>> sorted = entriesSortedByValues(objects);
|
||||
|
||||
// Assign hash keys with a position so that we can present and retrieve them
|
||||
int i = 0;
|
||||
mKeys = new int[mData.size()];
|
||||
mValues = new String[mData.size()];
|
||||
for (Map.Entry<Integer, String> entry : sorted) {
|
||||
mKeys[i] = entry.getKey();
|
||||
mValues[i] = entry.getValue();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return mData.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value
|
||||
*/
|
||||
@Override
|
||||
public String getItem(int position) {
|
||||
// return the value based on the position. This is displayed in the list.
|
||||
return mValues[position];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns item key
|
||||
*/
|
||||
public long getItemId(int position) {
|
||||
// Return an id to represent the item.
|
||||
|
||||
return mKeys[position];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find position from key
|
||||
*/
|
||||
public int getPosition(long itemId) {
|
||||
for (int i = 0; i < mKeys.length; i++) {
|
||||
if ((int) itemId == mKeys[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 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.adapter;
|
||||
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class SelectKeyCursorAdapter extends CursorAdapter {
|
||||
|
||||
protected int mKeyType;
|
||||
|
||||
private LayoutInflater mInflater;
|
||||
private ListView mListView;
|
||||
|
||||
private int mIndexUserId;
|
||||
private int mIndexMasterKeyId;
|
||||
private int mIndexProjectionValid;
|
||||
private int mIndexProjectionAvailable;
|
||||
|
||||
public final static String PROJECTION_ROW_AVAILABLE = "available";
|
||||
public final static String PROJECTION_ROW_VALID = "valid";
|
||||
|
||||
public SelectKeyCursorAdapter(Context context, Cursor c, int flags, ListView listView,
|
||||
int keyType) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
mListView = listView;
|
||||
mKeyType = keyType;
|
||||
|
||||
initIndex(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
initIndex(newCursor);
|
||||
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
|
||||
* performance comparison see http://stackoverflow.com/a/17999582
|
||||
*
|
||||
* @param cursor
|
||||
*/
|
||||
private void initIndex(Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
|
||||
mIndexMasterKeyId = cursor.getColumnIndexOrThrow(KeyRings.MASTER_KEY_ID);
|
||||
mIndexProjectionValid = cursor.getColumnIndexOrThrow(PROJECTION_ROW_VALID);
|
||||
mIndexProjectionAvailable = cursor.getColumnIndexOrThrow(PROJECTION_ROW_AVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserId(int position) {
|
||||
mCursor.moveToPosition(position);
|
||||
return mCursor.getString(mIndexUserId);
|
||||
}
|
||||
|
||||
public long getMasterKeyId(int position) {
|
||||
mCursor.moveToPosition(position);
|
||||
return mCursor.getLong(mIndexMasterKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
boolean valid = cursor.getInt(mIndexProjectionValid) > 0;
|
||||
|
||||
TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
|
||||
mainUserId.setText(R.string.unknown_user_id);
|
||||
TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
|
||||
mainUserIdRest.setText("");
|
||||
TextView keyId = (TextView) view.findViewById(R.id.keyId);
|
||||
keyId.setText(R.string.no_key);
|
||||
TextView status = (TextView) view.findViewById(R.id.status);
|
||||
status.setText(R.string.unknown_status);
|
||||
|
||||
String userId = cursor.getString(mIndexUserId);
|
||||
if (userId != null) {
|
||||
String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
|
||||
|
||||
if (userIdSplit[0] != null && userIdSplit[0].length() > 0) {
|
||||
mainUserId.setText(userIdSplit[0]);
|
||||
}
|
||||
|
||||
if (userIdSplit[1] != null && userIdSplit[1].length() > 0) {
|
||||
mainUserIdRest.setText(userIdSplit[1]);
|
||||
}
|
||||
}
|
||||
|
||||
long masterKeyId = cursor.getLong(mIndexMasterKeyId);
|
||||
keyId.setText(PgpKeyHelper.convertKeyIdToHex(masterKeyId));
|
||||
|
||||
if (valid) {
|
||||
if (mKeyType == Id.type.public_key) {
|
||||
status.setText(R.string.can_encrypt);
|
||||
} else {
|
||||
status.setText(R.string.can_sign);
|
||||
}
|
||||
} else {
|
||||
if (cursor.getInt(mIndexProjectionAvailable) > 0) {
|
||||
// has some CAN_ENCRYPT keys, but col(ROW_VALID) = 0, so must be revoked or
|
||||
// expired
|
||||
status.setText(R.string.expired);
|
||||
} else {
|
||||
status.setText(R.string.no_key);
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
|
||||
if (mKeyType == Id.type.public_key) {
|
||||
selected.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!valid) {
|
||||
mListView.setItemChecked(cursor.getPosition(), false);
|
||||
}
|
||||
|
||||
selected.setChecked(mListView.isItemChecked(cursor.getPosition()));
|
||||
selected.setEnabled(valid);
|
||||
} else {
|
||||
selected.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
status.setText(status.getText() + " ");
|
||||
|
||||
view.setEnabled(valid);
|
||||
mainUserId.setEnabled(valid);
|
||||
mainUserIdRest.setEnabled(valid);
|
||||
keyId.setEnabled(valid);
|
||||
status.setEnabled(valid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.select_key_item, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.adapter;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class ViewKeyKeysAdapter extends CursorAdapter {
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
private int mIndexKeyId;
|
||||
private int mIndexAlgorithm;
|
||||
private int mIndexKeySize;
|
||||
private int mIndexIsMasterKey;
|
||||
private int mIndexCanCertify;
|
||||
private int mIndexCanEncrypt;
|
||||
private int mIndexCanSign;
|
||||
|
||||
public ViewKeyKeysAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
|
||||
initIndex(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
initIndex(newCursor);
|
||||
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
|
||||
* performance comparison see http://stackoverflow.com/a/17999582
|
||||
*
|
||||
* @param cursor
|
||||
*/
|
||||
private void initIndex(Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
mIndexKeyId = cursor.getColumnIndexOrThrow(Keys.KEY_ID);
|
||||
mIndexAlgorithm = cursor.getColumnIndexOrThrow(Keys.ALGORITHM);
|
||||
mIndexKeySize = cursor.getColumnIndexOrThrow(Keys.KEY_SIZE);
|
||||
mIndexIsMasterKey = cursor.getColumnIndexOrThrow(Keys.IS_MASTER_KEY);
|
||||
mIndexCanCertify = cursor.getColumnIndexOrThrow(Keys.CAN_CERTIFY);
|
||||
mIndexCanEncrypt = cursor.getColumnIndexOrThrow(Keys.CAN_ENCRYPT);
|
||||
mIndexCanSign = cursor.getColumnIndexOrThrow(Keys.CAN_SIGN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
String keyIdStr = "0x" + PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexKeyId));
|
||||
String algorithmStr = PgpKeyHelper.getAlgorithmInfo(cursor.getInt(mIndexAlgorithm),
|
||||
cursor.getInt(mIndexKeySize));
|
||||
|
||||
TextView keyId = (TextView) view.findViewById(R.id.keyId);
|
||||
keyId.setText(keyIdStr);
|
||||
|
||||
TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
|
||||
keyDetails.setText("(" + algorithmStr + ")");
|
||||
|
||||
ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey);
|
||||
if (cursor.getInt(mIndexIsMasterKey) != 1) {
|
||||
masterKeyIcon.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
masterKeyIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ImageView certifyIcon = (ImageView) view.findViewById(R.id.ic_certifyKey);
|
||||
if (cursor.getInt(mIndexCanCertify) != 1) {
|
||||
certifyIcon.setVisibility(View.GONE);
|
||||
} else {
|
||||
certifyIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
|
||||
if (cursor.getInt(mIndexCanEncrypt) != 1) {
|
||||
encryptIcon.setVisibility(View.GONE);
|
||||
} else {
|
||||
encryptIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
|
||||
if (cursor.getInt(mIndexCanSign) != 1) {
|
||||
signIcon.setVisibility(View.GONE);
|
||||
} else {
|
||||
signIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.view_key_keys_item, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.adapter;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class ViewKeyUserIdsAdapter extends CursorAdapter {
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
private int mIndexUserId;
|
||||
|
||||
public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
|
||||
initIndex(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
initIndex(newCursor);
|
||||
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column indexes for performance reasons just once in constructor and swapCursor. For a
|
||||
* performance comparison see http://stackoverflow.com/a/17999582
|
||||
*
|
||||
* @param cursor
|
||||
*/
|
||||
private void initIndex(Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
String userIdStr = cursor.getString(mIndexUserId);
|
||||
|
||||
TextView userId = (TextView) view.findViewById(R.id.userId);
|
||||
userId.setText(userIdStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return mInflater.inflate(R.layout.view_key_userids_item, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentService;
|
||||
import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
public class DeleteFileDialogFragment extends SherlockDialogFragment {
|
||||
private static final String ARG_DELETE_FILE = "delete_file";
|
||||
|
||||
/**
|
||||
* Creates new instance of this delete file dialog fragment
|
||||
*/
|
||||
public static DeleteFileDialogFragment newInstance(String deleteFile) {
|
||||
DeleteFileDialogFragment frag = new DeleteFileDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putString(ARG_DELETE_FILE, deleteFile);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = getActivity();
|
||||
|
||||
final String deleteFile = getArguments().getString(ARG_DELETE_FILE);
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
alert.setTitle(R.string.warning);
|
||||
alert.setMessage(this.getString(R.string.file_delete_confirmation, deleteFile));
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
|
||||
// Send all information needed to service to edit key in other thread
|
||||
Intent intent = new Intent(activity, KeychainIntentService.class);
|
||||
|
||||
// fill values for this action
|
||||
Bundle data = new Bundle();
|
||||
|
||||
intent.setAction(KeychainIntentService.ACTION_DELETE_FILE_SECURELY);
|
||||
data.putString(KeychainIntentService.DELETE_FILE, deleteFile);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
|
||||
|
||||
ProgressDialogFragment deletingDialog = ProgressDialogFragment.newInstance(
|
||||
R.string.progress_deleting_securely, ProgressDialog.STYLE_HORIZONTAL);
|
||||
|
||||
// Message is received after deleting is done in ApgService
|
||||
KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(activity, deletingDialog) {
|
||||
public void handleMessage(Message message) {
|
||||
// handle messages by standard ApgHandler first
|
||||
super.handleMessage(message);
|
||||
|
||||
if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
|
||||
Toast.makeText(activity, R.string.file_delete_successful,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
Messenger messenger = new Messenger(saveHandler);
|
||||
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
|
||||
|
||||
// show progress dialog
|
||||
deletingDialog.show(activity.getSupportFragmentManager(), "deletingDialog");
|
||||
|
||||
// start service with intent
|
||||
activity.startService(intent);
|
||||
}
|
||||
});
|
||||
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
alert.setCancelable(true);
|
||||
|
||||
return alert.create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
public class DeleteKeyDialogFragment extends SherlockDialogFragment {
|
||||
private static final String ARG_MESSENGER = "messenger";
|
||||
private static final String ARG_DELETE_KEY_RING_ROW_IDS = "delete_file";
|
||||
private static final String ARG_KEY_TYPE = "key_type";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
|
||||
private Messenger mMessenger;
|
||||
|
||||
/**
|
||||
* Creates new instance of this delete file dialog fragment
|
||||
*/
|
||||
public static DeleteKeyDialogFragment newInstance(Messenger messenger, long[] keyRingRowIds,
|
||||
int keyType) {
|
||||
DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putParcelable(ARG_MESSENGER, messenger);
|
||||
args.putLongArray(ARG_DELETE_KEY_RING_ROW_IDS, keyRingRowIds);
|
||||
args.putInt(ARG_KEY_TYPE, keyType);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = getActivity();
|
||||
mMessenger = getArguments().getParcelable(ARG_MESSENGER);
|
||||
|
||||
final long[] keyRingRowIds = getArguments().getLongArray(ARG_DELETE_KEY_RING_ROW_IDS);
|
||||
final int keyType = getArguments().getInt(ARG_KEY_TYPE);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.warning);
|
||||
|
||||
if (keyRingRowIds.length == 1) {
|
||||
// TODO: better way to do this?
|
||||
String userId = activity.getString(R.string.unknown_user_id);
|
||||
|
||||
if (keyType == Id.type.public_key) {
|
||||
PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByRowId(activity,
|
||||
keyRingRowIds[0]);
|
||||
userId = PgpKeyHelper.getMainUserIdSafe(activity,
|
||||
PgpKeyHelper.getMasterKey(keyRing));
|
||||
} else {
|
||||
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByRowId(activity,
|
||||
keyRingRowIds[0]);
|
||||
userId = PgpKeyHelper.getMainUserIdSafe(activity,
|
||||
PgpKeyHelper.getMasterKey(keyRing));
|
||||
}
|
||||
|
||||
builder.setMessage(getString(
|
||||
keyType == Id.type.public_key ? R.string.key_deletion_confirmation
|
||||
: R.string.secret_key_deletion_confirmation, userId));
|
||||
} else {
|
||||
builder.setMessage(R.string.key_deletion_confirmation_multi);
|
||||
}
|
||||
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
if (keyType == Id.type.public_key) {
|
||||
for (long keyRowId : keyRingRowIds) {
|
||||
ProviderHelper.deletePublicKeyRing(activity, keyRowId);
|
||||
}
|
||||
} else {
|
||||
for (long keyRowId : keyRingRowIds) {
|
||||
ProviderHelper.deleteSecretKeyRing(activity, keyRowId);
|
||||
}
|
||||
}
|
||||
|
||||
dismiss();
|
||||
|
||||
sendMessageToHandler(MESSAGE_OKAY);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
* @param what
|
||||
* Message integer you want to send
|
||||
*/
|
||||
private void sendMessageToHandler(Integer what) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = what;
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 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.dialog;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.helper.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
import com.beardedhen.androidbootstrap.BootstrapButton;
|
||||
|
||||
public class FileDialogFragment extends SherlockDialogFragment {
|
||||
private static final String ARG_MESSENGER = "messenger";
|
||||
private static final String ARG_TITLE = "title";
|
||||
private static final String ARG_MESSAGE = "message";
|
||||
private static final String ARG_DEFAULT_FILE = "default_file";
|
||||
private static final String ARG_CHECKBOX_TEXT = "checkbox_text";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
|
||||
public static final String MESSAGE_DATA_FILENAME = "filename";
|
||||
public static final String MESSAGE_DATA_CHECKED = "checked";
|
||||
|
||||
private Messenger mMessenger;
|
||||
|
||||
private EditText mFilename;
|
||||
private BootstrapButton mBrowse;
|
||||
private CheckBox mCheckBox;
|
||||
|
||||
private static final int REQUEST_CODE = 0x00007004;
|
||||
|
||||
/**
|
||||
* Creates new instance of this file dialog fragment
|
||||
*/
|
||||
public static FileDialogFragment newInstance(Messenger messenger, String title, String message,
|
||||
String defaultFile, String checkboxText) {
|
||||
FileDialogFragment frag = new FileDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ARG_MESSENGER, messenger);
|
||||
|
||||
args.putString(ARG_TITLE, title);
|
||||
args.putString(ARG_MESSAGE, message);
|
||||
args.putString(ARG_DEFAULT_FILE, defaultFile);
|
||||
args.putString(ARG_CHECKBOX_TEXT, checkboxText);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
mMessenger = getArguments().getParcelable(ARG_MESSENGER);
|
||||
|
||||
String title = getArguments().getString(ARG_TITLE);
|
||||
String message = getArguments().getString(ARG_MESSAGE);
|
||||
String defaultFile = getArguments().getString(ARG_DEFAULT_FILE);
|
||||
String checkboxText = getArguments().getString(ARG_CHECKBOX_TEXT);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) activity
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setTitle(title);
|
||||
alert.setMessage(message);
|
||||
|
||||
View view = inflater.inflate(R.layout.file_dialog, null);
|
||||
|
||||
mFilename = (EditText) view.findViewById(R.id.input);
|
||||
mFilename.setText(defaultFile);
|
||||
mBrowse = (BootstrapButton) view.findViewById(R.id.btn_browse);
|
||||
mBrowse.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
// only .asc or .gpg files
|
||||
// setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
|
||||
// or gpg types!
|
||||
FileHelper.openFile(FileDialogFragment.this, mFilename.getText().toString(), "*/*",
|
||||
REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
|
||||
mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
|
||||
if (checkboxText == null) {
|
||||
mCheckBox.setEnabled(false);
|
||||
mCheckBox.setVisibility(View.GONE);
|
||||
} else {
|
||||
mCheckBox.setEnabled(true);
|
||||
mCheckBox.setVisibility(View.VISIBLE);
|
||||
mCheckBox.setText(checkboxText);
|
||||
}
|
||||
|
||||
alert.setView(view);
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
|
||||
boolean checked = false;
|
||||
if (mCheckBox.isEnabled()) {
|
||||
checked = mCheckBox.isChecked();
|
||||
}
|
||||
|
||||
// return resulting data back to activity
|
||||
Bundle data = new Bundle();
|
||||
data.putString(MESSAGE_DATA_FILENAME, mFilename.getText().toString());
|
||||
data.putBoolean(MESSAGE_DATA_CHECKED, checked);
|
||||
|
||||
sendMessageToHandler(MESSAGE_OKAY, data);
|
||||
}
|
||||
});
|
||||
|
||||
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return alert.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates filename in dialog, normally called in onActivityResult in activity using the
|
||||
* FileDialog
|
||||
*
|
||||
* @param messageId
|
||||
* @param progress
|
||||
* @param max
|
||||
*/
|
||||
private void setFilename(String filename) {
|
||||
AlertDialog dialog = (AlertDialog) getDialog();
|
||||
EditText filenameEditText = (EditText) dialog.findViewById(R.id.input);
|
||||
|
||||
if (filenameEditText != null) {
|
||||
filenameEditText.setText(filename);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode & 0xFFFF) {
|
||||
case REQUEST_CODE: {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
try {
|
||||
String path = data.getData().getPath();
|
||||
Log.d(Constants.TAG, "path=" + path);
|
||||
|
||||
// set filename used in export/import dialogs
|
||||
setFilename(path);
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
* @param what
|
||||
* Message integer you want to send
|
||||
*/
|
||||
private void sendMessageToHandler(Integer what, Bundle data) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = what;
|
||||
if (data != null) {
|
||||
msg.setData(data);
|
||||
}
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.ui.KeyServerQueryActivity;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnCancelListener;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
|
||||
public class LookupUnknownKeyDialogFragment extends SherlockDialogFragment {
|
||||
private static final String ARG_MESSENGER = "messenger";
|
||||
private static final String ARG_UNKNOWN_KEY_ID = "unknown_key_id";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
public static final int MESSAGE_CANCEL = 2;
|
||||
|
||||
private Messenger mMessenger;
|
||||
|
||||
/**
|
||||
* Creates new instance of this dialog fragment
|
||||
*
|
||||
* @param messenger
|
||||
* @param unknownKeyId
|
||||
* @return
|
||||
*/
|
||||
public static LookupUnknownKeyDialogFragment newInstance(Messenger messenger, long unknownKeyId) {
|
||||
LookupUnknownKeyDialogFragment frag = new LookupUnknownKeyDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(ARG_UNKNOWN_KEY_ID, unknownKeyId);
|
||||
args.putParcelable(ARG_MESSENGER, messenger);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
final long unknownKeyId = getArguments().getLong(ARG_UNKNOWN_KEY_ID);
|
||||
mMessenger = getArguments().getParcelable(ARG_MESSENGER);
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
alert.setTitle(R.string.title_unknown_signature_key);
|
||||
alert.setMessage(getString(R.string.lookup_unknown_key,
|
||||
PgpKeyHelper.convertKeyIdToHex(unknownKeyId)));
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
|
||||
sendMessageToHandler(MESSAGE_OKAY);
|
||||
|
||||
Intent intent = new Intent(activity, KeyServerQueryActivity.class);
|
||||
intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
|
||||
intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, unknownKeyId);
|
||||
startActivityForResult(intent, Id.request.look_up_key_id);
|
||||
}
|
||||
});
|
||||
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
|
||||
sendMessageToHandler(MESSAGE_CANCEL);
|
||||
}
|
||||
});
|
||||
alert.setCancelable(true);
|
||||
alert.setOnCancelListener(new OnCancelListener() {
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
sendMessageToHandler(MESSAGE_CANCEL);
|
||||
}
|
||||
});
|
||||
|
||||
return alert.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
* @param what
|
||||
* Message integer you want to send
|
||||
*/
|
||||
private void sendMessageToHandler(Integer what) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = what;
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import org.spongycastle.openpgp.PGPException;
|
||||
import org.spongycastle.openpgp.PGPPrivateKey;
|
||||
import org.spongycastle.openpgp.PGPSecretKey;
|
||||
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Id;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
public class PassphraseDialogFragment extends SherlockDialogFragment implements OnEditorActionListener {
|
||||
private static final String ARG_MESSENGER = "messenger";
|
||||
private static final String ARG_SECRET_KEY_ID = "secret_key_id";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
public static final int MESSAGE_CANCEL = 2;
|
||||
|
||||
private Messenger mMessenger;
|
||||
private EditText mPassphraseEditText;
|
||||
private boolean canKB;
|
||||
|
||||
/**
|
||||
* Creates new instance of this dialog fragment
|
||||
*
|
||||
* @param secretKeyId
|
||||
* secret key id you want to use
|
||||
* @param messenger
|
||||
* to communicate back after caching the passphrase
|
||||
* @return
|
||||
* @throws PgpGeneralException
|
||||
*/
|
||||
public static PassphraseDialogFragment newInstance(Context context, Messenger messenger,
|
||||
long secretKeyId) throws PgpGeneralException {
|
||||
// check if secret key has a passphrase
|
||||
if (!(secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none)) {
|
||||
if (!PassphraseCacheService.hasPassphrase(context, secretKeyId)) {
|
||||
throw new PgpGeneralException("No passphrase! No passphrase dialog needed!");
|
||||
}
|
||||
}
|
||||
|
||||
PassphraseDialogFragment frag = new PassphraseDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(ARG_SECRET_KEY_ID, secretKeyId);
|
||||
args.putParcelable(ARG_MESSENGER, messenger);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
final long secretKeyId = getArguments().getLong(ARG_SECRET_KEY_ID);
|
||||
mMessenger = getArguments().getParcelable(ARG_MESSENGER);
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setTitle(R.string.title_authentication);
|
||||
|
||||
final PGPSecretKey secretKey;
|
||||
|
||||
if (secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none) {
|
||||
secretKey = null;
|
||||
alert.setMessage(R.string.passphrase_for_symmetric_encryption);
|
||||
} else {
|
||||
// TODO: by master key id???
|
||||
secretKey = PgpKeyHelper.getMasterKey(ProviderHelper.getPGPSecretKeyRingByKeyId(activity,
|
||||
secretKeyId));
|
||||
// secretKey = PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId));
|
||||
|
||||
if (secretKey == null) {
|
||||
alert.setTitle(R.string.title_key_not_found);
|
||||
alert.setMessage(getString(R.string.key_not_found, secretKeyId));
|
||||
alert.setPositiveButton(android.R.string.ok, new OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
alert.setCancelable(false);
|
||||
canKB = false;
|
||||
return alert.create();
|
||||
}
|
||||
String userId = PgpKeyHelper.getMainUserIdSafe(activity, secretKey);
|
||||
|
||||
Log.d(Constants.TAG, "User id: '" + userId + "'");
|
||||
alert.setMessage(getString(R.string.passphrase_for, userId));
|
||||
}
|
||||
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.passphrase_dialog, null);
|
||||
alert.setView(view);
|
||||
|
||||
mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase);
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
long curKeyIndex = 1;
|
||||
boolean keyOK = true;
|
||||
String passPhrase = mPassphraseEditText.getText().toString();
|
||||
long keyId;
|
||||
PGPSecretKey clickSecretKey = secretKey;
|
||||
|
||||
if (clickSecretKey != null) {
|
||||
while (keyOK == true) {
|
||||
if (clickSecretKey != null) { // check again for loop
|
||||
try {
|
||||
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
|
||||
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
|
||||
passPhrase.toCharArray());
|
||||
PGPPrivateKey testKey = clickSecretKey
|
||||
.extractPrivateKey(keyDecryptor);
|
||||
if (testKey == null) {
|
||||
if (!clickSecretKey.isMasterKey()) {
|
||||
Toast.makeText(activity,
|
||||
R.string.error_could_not_extract_private_key,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
sendMessageToHandler(MESSAGE_CANCEL);
|
||||
return;
|
||||
} else {
|
||||
clickSecretKey = PgpKeyHelper.getKeyNum(ProviderHelper
|
||||
.getPGPSecretKeyRingByKeyId(activity, secretKeyId),
|
||||
curKeyIndex);
|
||||
curKeyIndex++; // does post-increment work like C?
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
keyOK = false;
|
||||
}
|
||||
} catch (PGPException e) {
|
||||
Toast.makeText(activity, R.string.wrong_passphrase,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
sendMessageToHandler(MESSAGE_CANCEL);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.error_could_not_extract_private_key,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
sendMessageToHandler(MESSAGE_CANCEL);
|
||||
return; // ran out of keys to try
|
||||
}
|
||||
}
|
||||
keyId = secretKey.getKeyID();
|
||||
} else {
|
||||
keyId = Id.key.symmetric;
|
||||
}
|
||||
|
||||
// cache the new passphrase
|
||||
Log.d(Constants.TAG, "Everything okay! Caching entered passphrase");
|
||||
PassphraseCacheService.addCachedPassphrase(activity, keyId, passPhrase);
|
||||
if (keyOK == false && clickSecretKey.getKeyID() != keyId) {
|
||||
PassphraseCacheService.addCachedPassphrase(activity, clickSecretKey.getKeyID(),
|
||||
passPhrase);
|
||||
}
|
||||
|
||||
sendMessageToHandler(MESSAGE_OKAY);
|
||||
}
|
||||
});
|
||||
|
||||
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
canKB = true;
|
||||
return alert.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle arg0) {
|
||||
super.onActivityCreated(arg0);
|
||||
if (canKB) {
|
||||
// request focus and open soft keyboard
|
||||
mPassphraseEditText.requestFocus();
|
||||
getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
|
||||
mPassphraseEditText.setOnEditorActionListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
super.onCancel(dialog);
|
||||
|
||||
dismiss();
|
||||
sendMessageToHandler(MESSAGE_CANCEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the "done" button on the soft keyboard with the okay button in the view
|
||||
*/
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (EditorInfo.IME_ACTION_DONE == actionId) {
|
||||
AlertDialog dialog = ((AlertDialog) getDialog());
|
||||
Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
|
||||
bt.performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
* @param what
|
||||
* Message integer you want to send
|
||||
*/
|
||||
private void sendMessageToHandler(Integer what) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = what;
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnKeyListener;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
public class ProgressDialogFragment extends SherlockDialogFragment {
|
||||
private static final String ARG_MESSAGE_ID = "message_id";
|
||||
private static final String ARG_STYLE = "style";
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public static ProgressDialogFragment newInstance(int messageId, int style) {
|
||||
ProgressDialogFragment frag = new ProgressDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_MESSAGE_ID, messageId);
|
||||
args.putInt(ARG_STYLE, style);
|
||||
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates progress of dialog
|
||||
*
|
||||
* @param messageId
|
||||
* @param progress
|
||||
* @param max
|
||||
*/
|
||||
public void setProgress(int messageId, int progress, int max) {
|
||||
setProgress(getString(messageId), progress, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates progress of dialog
|
||||
*
|
||||
* @param messageId
|
||||
* @param progress
|
||||
* @param max
|
||||
*/
|
||||
public void setProgress(int progress, int max) {
|
||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
|
||||
dialog.setProgress(progress);
|
||||
dialog.setMax(max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates progress of dialog
|
||||
*
|
||||
* @param messageId
|
||||
* @param progress
|
||||
* @param max
|
||||
*/
|
||||
public void setProgress(String message, int progress, int max) {
|
||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
|
||||
dialog.setMessage(message);
|
||||
dialog.setProgress(progress);
|
||||
dialog.setMax(max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Activity activity = getActivity();
|
||||
|
||||
ProgressDialog dialog = new ProgressDialog(activity);
|
||||
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
dialog.setCancelable(false);
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
|
||||
int messageId = getArguments().getInt(ARG_MESSAGE_ID);
|
||||
int style = getArguments().getInt(ARG_STYLE);
|
||||
|
||||
dialog.setMessage(getString(messageId));
|
||||
dialog.setProgressStyle(style);
|
||||
|
||||
// Disable the back button
|
||||
OnKeyListener keyListener = new OnKeyListener() {
|
||||
|
||||
@Override
|
||||
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
dialog.setOnKeyListener(keyListener);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
public class SetPassphraseDialogFragment extends SherlockDialogFragment implements OnEditorActionListener {
|
||||
private static final String ARG_MESSENGER = "messenger";
|
||||
private static final String ARG_TITLE = "title";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
|
||||
public static final String MESSAGE_NEW_PASSPHRASE = "new_passphrase";
|
||||
|
||||
private Messenger mMessenger;
|
||||
private EditText mPassphraseEditText;
|
||||
private EditText mPassphraseAgainEditText;
|
||||
|
||||
/**
|
||||
* Creates new instance of this dialog fragment
|
||||
*
|
||||
* @param title
|
||||
* title of dialog
|
||||
* @param messenger
|
||||
* to communicate back after setting the passphrase
|
||||
* @return
|
||||
*/
|
||||
public static SetPassphraseDialogFragment newInstance(Messenger messenger, int title) {
|
||||
SetPassphraseDialogFragment frag = new SetPassphraseDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_TITLE, title);
|
||||
args.putParcelable(ARG_MESSENGER, messenger);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
int title = getArguments().getInt(ARG_TITLE);
|
||||
mMessenger = getArguments().getParcelable(ARG_MESSENGER);
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setTitle(title);
|
||||
alert.setMessage(R.string.enter_passphrase_twice);
|
||||
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.passphrase_repeat_dialog, null);
|
||||
alert.setView(view);
|
||||
|
||||
mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase);
|
||||
mPassphraseAgainEditText = (EditText) view.findViewById(R.id.passphrase_passphrase_again);
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
|
||||
String passPhrase1 = mPassphraseEditText.getText().toString();
|
||||
String passPhrase2 = mPassphraseAgainEditText.getText().toString();
|
||||
if (!passPhrase1.equals(passPhrase2)) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
getString(R.string.error_message,
|
||||
getString(R.string.passphrases_do_not_match)), Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (passPhrase1.equals("")) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
getString(R.string.error_message,
|
||||
getString(R.string.passphrase_must_not_be_empty)),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// return resulting data back to activity
|
||||
Bundle data = new Bundle();
|
||||
data.putString(MESSAGE_NEW_PASSPHRASE, passPhrase1);
|
||||
|
||||
sendMessageToHandler(MESSAGE_OKAY, data);
|
||||
}
|
||||
});
|
||||
|
||||
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
return alert.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle arg0) {
|
||||
super.onActivityCreated(arg0);
|
||||
|
||||
// request focus and open soft keyboard
|
||||
mPassphraseEditText.requestFocus();
|
||||
getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
|
||||
mPassphraseAgainEditText.setOnEditorActionListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the "done" button on the soft keyboard with the okay button in the view
|
||||
*/
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (EditorInfo.IME_ACTION_DONE == actionId) {
|
||||
AlertDialog dialog = ((AlertDialog) getDialog());
|
||||
Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
|
||||
bt.performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
* @param what
|
||||
* Message integer you want to send
|
||||
*/
|
||||
private void sendMessageToHandler(Integer what, Bundle data) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = what;
|
||||
if (data != null) {
|
||||
msg.setData(data);
|
||||
}
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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.dialog;
|
||||
|
||||
import org.sufficientlysecure.htmltextview.HtmlTextView;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class ShareNfcDialogFragment extends SherlockDialogFragment {
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static ShareNfcDialogFragment newInstance() {
|
||||
ShareNfcDialogFragment frag = new ShareNfcDialogFragment();
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = getActivity();
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setIcon(android.R.drawable.ic_dialog_info);
|
||||
alert.setTitle(R.string.share_nfc_dialog);
|
||||
alert.setCancelable(true);
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
HtmlTextView textView = new HtmlTextView(getActivity());
|
||||
textView.setPadding(8, 8, 8, 8);
|
||||
alert.setView(textView);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
||||
textView.setText(getString(R.string.error) + ": "
|
||||
+ getString(R.string.error_jelly_bean_needed));
|
||||
} else {
|
||||
// check if NFC Adapter is available
|
||||
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
|
||||
if (nfcAdapter == null) {
|
||||
textView.setText(getString(R.string.error) + ": "
|
||||
+ getString(R.string.error_nfc_needed));
|
||||
} else {
|
||||
// nfc works...
|
||||
textView.setHtmlFromRawResource(getActivity(), R.raw.nfc_beam_share);
|
||||
|
||||
alert.setNegativeButton(R.string.menu_beam_preferences,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent intentSettings = new Intent(
|
||||
Settings.ACTION_NFCSHARING_SETTINGS);
|
||||
startActivity(intentSettings);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// no flickering when clicking textview for Android < 4
|
||||
// aboutTextView.setTextColor(getResources().getColor(android.R.color.black));
|
||||
|
||||
return alert.create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 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.dialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.QrCodeUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.actionbarsherlock.app.SherlockDialogFragment;
|
||||
|
||||
public class ShareQrCodeDialogFragment extends SherlockDialogFragment {
|
||||
private static final String ARG_URI = "uri";
|
||||
private static final String ARG_FINGERPRINT_ONLY = "fingerprint_only";
|
||||
|
||||
private ImageView mImage;
|
||||
private TextView mText;
|
||||
|
||||
private ArrayList<String> mContentList;
|
||||
private int mCounter;
|
||||
|
||||
private static final int QR_CODE_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* Creates new instance of this dialog fragment
|
||||
*
|
||||
* @param content
|
||||
* Content to be shared via QR Codes
|
||||
* @return
|
||||
*/
|
||||
public static ShareQrCodeDialogFragment newInstance(Uri dataUri, boolean fingerprintOnly) {
|
||||
ShareQrCodeDialogFragment frag = new ShareQrCodeDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ARG_URI, dataUri);
|
||||
args.putBoolean(ARG_FINGERPRINT_ONLY, fingerprintOnly);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
Uri dataUri = getArguments().getParcelable(ARG_URI);
|
||||
boolean fingerprintOnly = getArguments().getBoolean(ARG_FINGERPRINT_ONLY);
|
||||
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
|
||||
alert.setTitle(R.string.share_qr_code_dialog_title);
|
||||
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.share_qr_code_dialog, null);
|
||||
alert.setView(view);
|
||||
|
||||
mImage = (ImageView) view.findViewById(R.id.share_qr_code_dialog_image);
|
||||
mText = (TextView) view.findViewById(R.id.share_qr_code_dialog_text);
|
||||
|
||||
// TODO
|
||||
long masterKeyId = ProviderHelper.getMasterKeyId(getActivity(), dataUri);
|
||||
|
||||
String content = null;
|
||||
if (fingerprintOnly) {
|
||||
content = "openpgp4fpr:";
|
||||
|
||||
String fingerprint = PgpKeyHelper.convertKeyToHex(masterKeyId);
|
||||
|
||||
mText.setText(getString(R.string.share_qr_code_dialog_fingerprint_text) + " "
|
||||
+ fingerprint);
|
||||
|
||||
content = content + fingerprint;
|
||||
|
||||
Log.d(Constants.TAG, "content: " + content);
|
||||
|
||||
alert.setPositiveButton(R.string.btn_okay, null);
|
||||
} else {
|
||||
mText.setText(R.string.share_qr_code_dialog_start);
|
||||
|
||||
// get public keyring as ascii armored string
|
||||
ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(
|
||||
getActivity(), dataUri, new long[] { masterKeyId });
|
||||
|
||||
// TODO: binary?
|
||||
|
||||
content = keyringArmored.get(0);
|
||||
|
||||
// OnClickListener are set in onResume to prevent automatic dismissing of Dialogs
|
||||
// http://stackoverflow.com/questions/2620444/how-to-prevent-a-dialog-from-closing-when-a-button-is-clicked
|
||||
alert.setPositiveButton(R.string.btn_next, null);
|
||||
alert.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
mContentList = splitString(content, 1000);
|
||||
|
||||
// start with first
|
||||
mCounter = 0;
|
||||
updateQrCode();
|
||||
|
||||
return alert.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
AlertDialog alertDialog = (AlertDialog) getDialog();
|
||||
final Button backButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
|
||||
final Button nextButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
|
||||
backButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mCounter > 0) {
|
||||
mCounter--;
|
||||
updateQrCode();
|
||||
updateDialog(backButton, nextButton);
|
||||
} else {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
nextButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
if (mCounter < mContentList.size() - 1) {
|
||||
mCounter++;
|
||||
updateQrCode();
|
||||
updateDialog(backButton, nextButton);
|
||||
} else {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateQrCode() {
|
||||
// Content: <counter>,<size>,<content>
|
||||
mImage.setImageBitmap(QrCodeUtils.getQRCodeBitmap(mCounter + "," + mContentList.size()
|
||||
+ "," + mContentList.get(mCounter), QR_CODE_SIZE));
|
||||
}
|
||||
|
||||
private void updateDialog(Button backButton, Button nextButton) {
|
||||
if (mCounter == 0) {
|
||||
backButton.setText(android.R.string.cancel);
|
||||
} else {
|
||||
backButton.setText(R.string.btn_back);
|
||||
}
|
||||
if (mCounter == mContentList.size() - 1) {
|
||||
nextButton.setText(android.R.string.ok);
|
||||
} else {
|
||||
nextButton.setText(R.string.btn_next);
|
||||
}
|
||||
|
||||
mText.setText(getResources().getString(R.string.share_qr_code_dialog_progress,
|
||||
mCounter + 1, mContentList.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split String by number of characters
|
||||
*
|
||||
* @param text
|
||||
* @param size
|
||||
* @return
|
||||
*/
|
||||
private ArrayList<String> splitString(String text, int size) {
|
||||
ArrayList<String> strings = new ArrayList<String>();
|
||||
int index = 0;
|
||||
while (index < text.length()) {
|
||||
strings.add(text.substring(index, Math.min(index + size, text.length())));
|
||||
index += size;
|
||||
}
|
||||
|
||||
return strings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.widget;
|
||||
|
||||
public interface Editor {
|
||||
public interface EditorListener {
|
||||
public void onDeleted(Editor editor);
|
||||
}
|
||||
|
||||
public void setEditorListener(EditorListener listener);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user