-implementation of "--send-key"

-implementation of "--sign-key"
-partial implementation of exchanging/verifying keys via QR Code
This commit is contained in:
senecaso 2011-10-17 10:07:37 +09:00
parent 6f2333b7d3
commit debb90409a
18 changed files with 923 additions and 112 deletions

View File

@ -183,6 +183,11 @@
android:label="@string/title_keyServerQuery" android:label="@string/title_keyServerQuery"
android:configChanges="keyboardHidden|orientation|keyboard"/> android:configChanges="keyboardHidden|orientation|keyboard"/>
<activity
android:name=".SendKeyActivity"
android:label="@string/title_sendKey"
android:configChanges="keyboardHidden|orientation|keyboard"/>
<activity <activity
android:name=".PreferencesActivity" android:name=".PreferencesActivity"
android:label="@string/title_preferences" android:label="@string/title_preferences"
@ -193,6 +198,17 @@
android:label="@string/title_keyServerPreference" android:label="@string/title_keyServerPreference"
android:configChanges="keyboardHidden|orientation|keyboard"/> android:configChanges="keyboardHidden|orientation|keyboard"/>
<activity
android:name=".SignKeyActivity"
android:label="@string/title_signKey"
android:configChanges="keyboardHidden|orientation|keyboard"/>
<activity
android:name=".ImportFromQRCodeActivity"
android:label="@string/title_importFromQRCode"
android:configChanges="keyboardHidden|orientation|keyboard"/>
<service android:name=".Service" /> <service android:name=".Service" />
<provider <provider

View File

@ -27,7 +27,12 @@
<artifactId>scprov-jdk15</artifactId> <artifactId>scprov-jdk15</artifactId>
<version>1.46.99.4-UNOFFICIAL-ROBERTO-RELEASE-SNAPSHOT</version> <version>1.46.99.4-UNOFFICIAL-ROBERTO-RELEASE-SNAPSHOT</version>
<type>jar</type> <type>jar</type>
<scope>provided</scope> </dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>android-integration</artifactId>
<version>1.6-SNAPSHOT</version>
<type>jar</type>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Spinner
android:id="@+id/keyServer"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:id="@+id/btn_export_to_server"
android:text="@string/btn_export_to_server"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:orientation="horizontal">
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:orientation="horizontal">
<TextView android:id="@+id/textView1" android:layout_height="wrap_content"
android:text="@string/label_sendKey" android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"></TextView>
<CheckBox android:text="" android:id="@+id/sendKey"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:checked="true"></CheckBox>
</LinearLayout>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:orientation="horizontal">
<Spinner android:id="@+id/keyServer" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_weight="1" />
</LinearLayout>
<Button android:layout_height="wrap_content"
android:layout_width="wrap_content" android:text="@string/btn_sign"
android:id="@+id/sign"></Button>
</LinearLayout>

View File

@ -42,7 +42,10 @@
<string name="title_keyNotFound">Key Not Found</string> <string name="title_keyNotFound">Key Not Found</string>
<string name="title_help">Getting Started</string> <string name="title_help">Getting Started</string>
<string name="title_keyServerQuery">Query Key Server</string> <string name="title_keyServerQuery">Query Key Server</string>
<string name="title_sendKey">Export to Key Server</string>
<string name="title_unknownSignatureKey">Unknown Signature Key</string> <string name="title_unknownSignatureKey">Unknown Signature Key</string>
<string name="title_importFromQRCode">Import from QR Code</string>
<string name="title_signKey">Sign Key</string>
<!-- section_lowerCase: capitalized words, no punctuation --> <!-- section_lowerCase: capitalized words, no punctuation -->
<string name="section_userIds">User IDs</string> <string name="section_userIds">User IDs</string>
@ -74,6 +77,7 @@
<string name="btn_changePassPhrase">Change Pass Phrase</string> <string name="btn_changePassPhrase">Change Pass Phrase</string>
<string name="btn_setPassPhrase">Set Pass Phrase</string> <string name="btn_setPassPhrase">Set Pass Phrase</string>
<string name="btn_search">Search</string> <string name="btn_search">Search</string>
<string name="btn_export_to_server">Export To Server</string>
<!-- menu_lowerCase: capitalized words, no punctuation --> <!-- menu_lowerCase: capitalized words, no punctuation -->
<string name="menu_about">About</string> <string name="menu_about">About</string>
@ -92,6 +96,10 @@
<string name="menu_help">Help</string> <string name="menu_help">Help</string>
<string name="menu_keyServer">Key Server</string> <string name="menu_keyServer">Key Server</string>
<string name="menu_updateKey">Update</string> <string name="menu_updateKey">Update</string>
<string name="menu_exportKeyToServer">Export To Server</string>
<string name="menu_share">Share</string>
<string name="menu_scanQRCode">Scan QR Code</string>
<string name="menu_signKey">Sign Key</string>
<!-- label_lowerCase: capitalized words, no punctuation --> <!-- label_lowerCase: capitalized words, no punctuation -->
<string name="label_sign">Sign</string> <string name="label_sign">Sign</string>
@ -124,6 +132,7 @@
<string name="label_name">Name</string> <string name="label_name">Name</string>
<string name="label_comment">Comment</string> <string name="label_comment">Comment</string>
<string name="label_email">Email</string> <string name="label_email">Email</string>
<string name="label_sendKey">Send Key to Server?</string>
<string name="noKeysSelected">Select</string> <string name="noKeysSelected">Select</string>
<string name="oneKeySelected">1 Selected</string> <string name="oneKeySelected">1 Selected</string>
@ -217,6 +226,9 @@
<string name="keyEditingIsBeta">Key editing is still kind of beta.</string> <string name="keyEditingIsBeta">Key editing is still kind of beta.</string>
<string name="badKeysEncountered">%s bad secret key(s) ignored. Perhaps you exported with the option\n --export-secret-subkeys\nMake sure you export with\n --export-secret-keys\ninstead.</string> <string name="badKeysEncountered">%s bad secret key(s) ignored. Perhaps you exported with the option\n --export-secret-subkeys\nMake sure you export with\n --export-secret-keys\ninstead.</string>
<string name="lookupUnknownKey">Unknown key %s, do you want to try finding it on a keyserver?</string> <string name="lookupUnknownKey">Unknown key %s, do you want to try finding it on a keyserver?</string>
<string name="keySendSuccess">Successfully sent key to server</string>
<string name="keySignSuccess">Successfully signed key</string>
<string name="qrScanImportSuccess">Successfully validated and imported key</string>
<!-- error_lowerCase: phrases, no punctuation, all lowercase, <!-- error_lowerCase: phrases, no punctuation, all lowercase,
they will be put after "errorMessage", e.g. "Error: file not found" --> they will be put after "errorMessage", e.g. "Error: file not found" -->

View File

@ -61,6 +61,7 @@ import org.spongycastle.openpgp.PGPEncryptedDataGenerator;
import org.spongycastle.openpgp.PGPEncryptedDataList; import org.spongycastle.openpgp.PGPEncryptedDataList;
import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPKeyPair; import org.spongycastle.openpgp.PGPKeyPair;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPKeyRingGenerator; import org.spongycastle.openpgp.PGPKeyRingGenerator;
import org.spongycastle.openpgp.PGPLiteralData; import org.spongycastle.openpgp.PGPLiteralData;
import org.spongycastle.openpgp.PGPLiteralDataGenerator; import org.spongycastle.openpgp.PGPLiteralDataGenerator;
@ -81,6 +82,7 @@ import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.spongycastle.openpgp.PGPSignatureSubpacketVector; import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
import org.spongycastle.openpgp.PGPUtil; import org.spongycastle.openpgp.PGPUtil;
import org.spongycastle.openpgp.PGPV3SignatureGenerator; import org.spongycastle.openpgp.PGPV3SignatureGenerator;
import org.thialfihar.android.apg.KeyServer.AddKeyException;
import org.thialfihar.android.apg.provider.DataProvider; import org.thialfihar.android.apg.provider.DataProvider;
import org.thialfihar.android.apg.provider.Database; import org.thialfihar.android.apg.provider.Database;
import org.thialfihar.android.apg.provider.KeyRings; import org.thialfihar.android.apg.provider.KeyRings;
@ -119,6 +121,8 @@ public class Apg {
public static final String LOOK_UP_KEY_ID = "org.thialfihar.android.apg.intent.LOOK_UP_KEY_ID"; public static final String LOOK_UP_KEY_ID = "org.thialfihar.android.apg.intent.LOOK_UP_KEY_ID";
public static final String LOOK_UP_KEY_ID_AND_RETURN = "org.thialfihar.android.apg.intent.LOOK_UP_KEY_ID_AND_RETURN"; public static final String LOOK_UP_KEY_ID_AND_RETURN = "org.thialfihar.android.apg.intent.LOOK_UP_KEY_ID_AND_RETURN";
public static final String GENERATE_SIGNATURE = "org.thialfihar.android.apg.intent.GENERATE_SIGNATURE"; public static final String GENERATE_SIGNATURE = "org.thialfihar.android.apg.intent.GENERATE_SIGNATURE";
public static final String EXPORT_KEY_TO_SERVER = "org.thialfihar.android.apg.intent.EXPORT_KEY_TO_SERVER";
public static final String IMPORT_FROM_QR_CODE = "org.thialfihar.android.apg.intent.IMPORT_FROM_QR_CODE";
} }
public static final String EXTRA_TEXT = "text"; public static final String EXTRA_TEXT = "text";
@ -147,6 +151,7 @@ public class Apg {
public static final String EXTRA_ASCII_ARMOUR = "asciiArmour"; public static final String EXTRA_ASCII_ARMOUR = "asciiArmour";
public static final String EXTRA_BINARY = "binary"; public static final String EXTRA_BINARY = "binary";
public static final String EXTRA_KEY_SERVERS = "keyServers"; public static final String EXTRA_KEY_SERVERS = "keyServers";
public static final String EXTRA_EXPECTED_FINGERPRINT = "org.thialfihar.android.apg.EXPECTED_FINGERPRINT";
public static final String AUTHORITY = DataProvider.AUTHORITY; public static final String AUTHORITY = DataProvider.AUTHORITY;
@ -590,6 +595,75 @@ public class Apg {
progress.setProgress(R.string.progress_done, 100, 100); progress.setProgress(R.string.progress_done, 100, 100);
} }
public static PGPKeyRing decodeKeyRing(InputStream is) throws IOException {
InputStream in = PGPUtil.getDecoderStream(is);
PGPObjectFactory objectFactory = new PGPObjectFactory(in);
Object obj = objectFactory.nextObject();
if (obj instanceof PGPKeyRing) {
return (PGPKeyRing) obj;
}
return null;
}
public static int storeKeyRingInCache(PGPKeyRing keyring) {
int status = Integer.MIN_VALUE; // out of bounds value (Id.retrun_value.*)
try {
if (keyring instanceof PGPSecretKeyRing) {
PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring;
boolean save = true;
try {
PGPPrivateKey testKey = secretKeyRing.getSecretKey().extractPrivateKey(new char[] {}, new BouncyCastleProvider());
if (testKey == null) {
// this is bad, something is very wrong... likely a --export-secret-subkeys export
save = false;
status = Id.return_value.bad;
}
} catch (PGPException e) {
// all good if this fails, we likely didn't use the right password
}
if (save) {
status = mDatabase.saveKeyRing(secretKeyRing);
}
} else if (keyring instanceof PGPPublicKeyRing) {
PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
status = mDatabase.saveKeyRing(publicKeyRing);
}
} catch (IOException e) {
status = Id.return_value.error;
} catch (Database.GeneralException e) {
status = Id.return_value.error;
}
return status;
}
public static 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) {
}
}
}
public static Bundle importKeyRings(Activity context, int type, public static Bundle importKeyRings(Activity context, int type,
InputData data, InputData data,
@ -616,64 +690,32 @@ public class Apg {
int oldKeys = 0; int oldKeys = 0;
int badKeys = 0; int badKeys = 0;
try { try {
while (true) { PGPKeyRing keyring = decodeKeyRing(bufferedInput);
InputStream in = PGPUtil.getDecoderStream(bufferedInput); while (keyring != null) {
PGPObjectFactory objectFactory = new PGPObjectFactory(in); int status = Integer.MIN_VALUE; // out of bounds value
Object obj = objectFactory.nextObject();
// if the first is already a null object, then we can stop trying // if this key is what we expect it to be, save it
if (obj == null) { if ((type == Id.type.secret_key && keyring instanceof PGPSecretKeyRing) ||
break; (type == Id.type.public_key && keyring instanceof PGPPublicKeyRing)) {
status = storeKeyRingInCache(keyring);
} }
while (obj != null) {
PGPPublicKeyRing publicKeyRing;
PGPSecretKeyRing secretKeyRing;
// a return value that doesn't match any Id.return_value.* values, in case
// saveKeyRing is never called
int retValue = 2107;
try { if (status == Id.return_value.error) {
if (type == Id.type.secret_key && obj instanceof PGPSecretKeyRing) { throw new GeneralException(context.getString(R.string.error_savingKeys));
secretKeyRing = (PGPSecretKeyRing) obj;
boolean save = true;
try {
PGPPrivateKey testKey = secretKeyRing.getSecretKey()
.extractPrivateKey(new char[] {}, new BouncyCastleProvider());
if (testKey == null) {
// this is bad, something is very wrong... likely a
// --export-secret-subkeys export
retValue = Id.return_value.bad;
save = false;
}
} catch (PGPException e) {
// all good if this fails, we likely didn't use the right password
}
if (save) {
retValue = mDatabase.saveKeyRing(secretKeyRing);
}
} else if (type == Id.type.public_key && obj instanceof PGPPublicKeyRing) {
publicKeyRing = (PGPPublicKeyRing) obj;
retValue = mDatabase.saveKeyRing(publicKeyRing);
}
} catch (IOException e) {
retValue = Id.return_value.error;
} catch (Database.GeneralException e) {
retValue = Id.return_value.error;
}
if (retValue == Id.return_value.error) {
throw new GeneralException(context.getString(R.string.error_savingKeys));
}
if (retValue == Id.return_value.updated) {
++oldKeys;
} else if (retValue == Id.return_value.ok) {
++newKeys;
} else if (retValue == Id.return_value.bad) {
++badKeys;
}
progress.setProgress((int)(100 * progressIn.position() / data.getSize()), 100);
obj = objectFactory.nextObject();
} }
// 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;
}
progress.setProgress((int)(100 * progressIn.position() / data.getSize()), 100);
keyring = decodeKeyRing(bufferedInput);
} }
} catch (EOFException e) { } catch (EOFException e) {
// nothing to do, we are done // nothing to do, we are done
@ -1038,18 +1080,8 @@ public class Apg {
return algorithmStr + ", " + keySize + "bit"; return algorithmStr + ", " + keySize + "bit";
} }
public static String getFingerPrint(long keyId) { public static String convertToHex(byte[] fp) {
PGPPublicKey key = Apg.getPublicKey(keyId);
if (key == null) {
PGPSecretKey secretKey = Apg.getSecretKey(keyId);
if (secretKey == null) {
return "";
}
key = secretKey.getPublicKey();
}
String fingerPrint = ""; String fingerPrint = "";
byte fp[] = key.getFingerprint();
for (int i = 0; i < fp.length; ++i) { for (int i = 0; i < fp.length; ++i) {
if (i != 0 && i % 10 == 0) { if (i != 0 && i % 10 == 0) {
fingerPrint += " "; fingerPrint += " ";
@ -1064,6 +1096,20 @@ public class Apg {
} }
return fingerPrint; return fingerPrint;
}
public static String getFingerPrint(long keyId) {
PGPPublicKey key = Apg.getPublicKey(keyId);
if (key == null) {
PGPSecretKey secretKey = Apg.getSecretKey(keyId);
if (secretKey == null) {
return "";
}
key = secretKey.getPublicKey();
}
return convertToHex(key.getFingerprint());
} }
public static String getSmallFingerPrint(long keyId) { public static String getSmallFingerPrint(long keyId) {
@ -1089,8 +1135,8 @@ public class Apg {
mDatabase.deleteKeyRing(keyRingId); mDatabase.deleteKeyRing(keyRingId);
} }
public static Object getKeyRing(int keyRingId) { public static PGPKeyRing getKeyRing(int keyRingId) {
return mDatabase.getKeyRing(keyRingId); return (PGPKeyRing) mDatabase.getKeyRing(keyRingId);
} }
public static PGPSecretKeyRing getSecretKeyRing(long keyId) { public static PGPSecretKeyRing getSecretKeyRing(long keyId) {
@ -1109,7 +1155,7 @@ public class Apg {
} }
return null; return null;
} }
public static PGPPublicKeyRing getPublicKeyRing(long keyId) { public static PGPPublicKeyRing getPublicKeyRing(long keyId) {
byte[] data = mDatabase.getKeyRingDataFromKeyId(Id.database.type_public, keyId); byte[] data = mDatabase.getKeyRingDataFromKeyId(Id.database.type_public, keyId);
if (data == null) { if (data == null) {

View File

@ -163,6 +163,13 @@ public class BaseActivity extends Activity
mProgressDialog.setCancelable(false); mProgressDialog.setCancelable(false);
return mProgressDialog; return mProgressDialog;
} }
case Id.dialog.signing: {
mProgressDialog.setMessage(this.getString(R.string.progress_signing));
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCancelable(false);
return mProgressDialog;
}
default: { default: {
break; break;

View File

@ -10,12 +10,25 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List; import java.util.List;
import java.util.Vector; import java.util.Vector;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import android.text.Html; import android.text.Html;
public class HkpKeyServer extends KeyServer { public class HkpKeyServer extends KeyServer {
@ -38,16 +51,14 @@ public class HkpKeyServer extends KeyServer {
return mData; return mData;
} }
} }
private String mHost; private String mHost;
private short mPort = 11371; private short mPort = 11371;
// example: // example:
// pub 2048R/<a href="/pks/lookup?op=get&search=0x887DF4BE9F5C9090">9F5C9090</a> 2009-08-17 <a href="/pks/lookup?op=vindex&search=0x887DF4BE9F5C9090">Jörg Runge &lt;joerg@joergrunge.de&gt;</a> // pub 2048R/<a href="/pks/lookup?op=get&search=0x887DF4BE9F5C9090">9F5C9090</a> 2009-08-17 <a href="/pks/lookup?op=vindex&search=0x887DF4BE9F5C9090">Jörg Runge &lt;joerg@joergrunge.de&gt;</a>
public static Pattern PUB_KEY_LINE = public static Pattern PUB_KEY_LINE = Pattern.compile("pub +([0-9]+)([a-z]+)/.*?0x([0-9a-z]+).*? +([0-9-]+) +(.+)[\n\r]+((?: +.+[\n\r]+)*)", Pattern.CASE_INSENSITIVE);
Pattern.compile("pub +([0-9]+)([a-z]+)/.*?0x([0-9a-z]+).*? +([0-9-]+) +(.+)[\n\r]+((?: +.+[\n\r]+)*)", public static Pattern USER_ID_LINE = Pattern.compile("^ +(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
Pattern.CASE_INSENSITIVE);
public static Pattern USER_ID_LINE =
Pattern.compile("^ +(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
public HkpKeyServer(String host) { public HkpKeyServer(String host) {
mHost = host; mHost = host;
@ -58,8 +69,7 @@ public class HkpKeyServer extends KeyServer {
mPort = port; mPort = port;
} }
static private String readAll(InputStream in, String encoding) static private String readAll(InputStream in, String encoding) throws IOException {
throws IOException {
ByteArrayOutputStream raw = new ByteArrayOutputStream(); ByteArrayOutputStream raw = new ByteArrayOutputStream();
byte buffer[] = new byte[1 << 16]; byte buffer[] = new byte[1 << 16];
@ -74,8 +84,8 @@ public class HkpKeyServer extends KeyServer {
return raw.toString(encoding); return raw.toString(encoding);
} }
private String query(String request) // TODO: replace this with httpclient
throws QueryException, HttpError { private String query(String request) throws QueryException, HttpError {
InetAddress ips[]; InetAddress ips[];
try { try {
ips = InetAddress.getAllByName(mHost); ips = InetAddress.getAllByName(mHost);
@ -93,8 +103,7 @@ public class HkpKeyServer extends KeyServer {
int response = conn.getResponseCode(); int response = conn.getResponseCode();
if (response >= 200 && response < 300) { if (response >= 200 && response < 300) {
return readAll(conn.getInputStream(), conn.getContentEncoding()); return readAll(conn.getInputStream(), conn.getContentEncoding());
} } else {
else {
String data = readAll(conn.getErrorStream(), conn.getContentEncoding()); String data = readAll(conn.getErrorStream(), conn.getContentEncoding());
throw new HttpError(response, data); throw new HttpError(response, data);
} }
@ -108,9 +117,9 @@ public class HkpKeyServer extends KeyServer {
throw new QueryException("querying server(s) for '" + mHost + "' failed"); throw new QueryException("querying server(s) for '" + mHost + "' failed");
} }
// TODO: replace this with httpclient
@Override @Override
List<KeyInfo> search(String query) List<KeyInfo> search(String query) throws QueryException, TooManyResponses, InsufficientQuery {
throws QueryException, TooManyResponses, InsufficientQuery {
Vector<KeyInfo> results = new Vector<KeyInfo>(); Vector<KeyInfo> results = new Vector<KeyInfo>();
if (query.length() < 3) { if (query.length() < 3) {
@ -151,9 +160,7 @@ public class HkpKeyServer extends KeyServer {
info.keyId = Apg.keyFromHex(matcher.group(3)); info.keyId = Apg.keyFromHex(matcher.group(3));
info.fingerPrint = Apg.getSmallFingerPrint(info.keyId); info.fingerPrint = Apg.getSmallFingerPrint(info.keyId);
String chunks[] = matcher.group(4).split("-"); String chunks[] = matcher.group(4).split("-");
info.date = new GregorianCalendar(Integer.parseInt(chunks[0]), info.date = new GregorianCalendar(Integer.parseInt(chunks[0]), Integer.parseInt(chunks[1]), Integer.parseInt(chunks[2])).getTime();
Integer.parseInt(chunks[1]),
Integer.parseInt(chunks[2])).getTime();
info.userIds = new Vector<String>(); info.userIds = new Vector<String>();
if (matcher.group(5).startsWith("*** KEY")) { if (matcher.group(5).startsWith("*** KEY")) {
info.revoked = matcher.group(5); info.revoked = matcher.group(5);
@ -177,21 +184,50 @@ public class HkpKeyServer extends KeyServer {
} }
@Override @Override
String get(long keyId) String get(long keyId) throws QueryException {
throws QueryException { HttpClient client = new DefaultHttpClient();
String request = "/pks/lookup?op=get&search=0x" + Apg.keyToHex(keyId);
String data = null;
try { try {
data = query(request); HttpGet get = new HttpGet("http://" + mHost + ":" + mPort + "/pks/lookup?op=get&search=0x" + Apg.keyToHex(keyId));
} catch (HttpError e) {
throw new QueryException("not found"); HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new QueryException("not found");
}
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
String data = readAll(is, EntityUtils.getContentCharSet(entity));
Matcher matcher = Apg.PGP_PUBLIC_KEY.matcher(data);
if (matcher.find()) {
return matcher.group(1);
}
} catch (IOException e) {
// nothing to do, better luck on the next keyserver
} finally {
client.getConnectionManager().shutdown();
} }
Matcher matcher = Apg.PGP_PUBLIC_KEY.matcher(data);
if (matcher.find()) {
return matcher.group(1);
}
return null; return null;
} }
@Override
void add(String armouredText) throws AddKeyException {
HttpClient client = new DefaultHttpClient();
try {
HttpPost post = new HttpPost("http://" + mHost + ":" + mPort + "/pks/add");
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
nameValuePairs.add(new BasicNameValuePair("keytext", armouredText));
post.setEntity(new UrlEncodedFormEntity(nameValuePairs));
HttpResponse response = client.execute(post);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new AddKeyException();
}
} catch (IOException e) {
// nothing to do, better luck on the next keyserver
} finally {
client.getConnectionManager().shutdown();
}
}
} }

View File

@ -18,12 +18,16 @@ package org.thialfihar.android.apg;
import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.spongycastle.bcpg.CompressionAlgorithmTags;
public final class Id { public final class Id {
public static final class menu { public static final class menu {
public static final int export = 0x21070001; public static final int export = 0x21070001;
public static final int delete = 0x21070002; public static final int delete = 0x21070002;
public static final int edit = 0x21070003; public static final int edit = 0x21070003;
public static final int update = 0x21070004; public static final int update = 0x21070004;
public static final int exportToServer = 0x21070005;
public static final int share = 0x21070006;
public static final int signKey = 0x21070007;
public static final class option { public static final class option {
public static final int new_pass_phrase = 0x21070001; public static final int new_pass_phrase = 0x21070001;
@ -37,6 +41,7 @@ public final class Id {
public static final int search = 0x21070009; public static final int search = 0x21070009;
public static final int help = 0x21070010; public static final int help = 0x21070010;
public static final int key_server = 0x21070011; public static final int key_server = 0x21070011;
public static final int scanQRCode = 0x21070012;
} }
} }
@ -61,6 +66,9 @@ public final class Id {
public static final int output_filename = 0x21070004; public static final int output_filename = 0x21070004;
public static final int key_server_preference = 0x21070005; public static final int key_server_preference = 0x21070005;
public static final int look_up_key_id = 0x21070006; public static final int look_up_key_id = 0x21070006;
public static final int export_to_server = 0x21070007;
public static final int import_from_qr_code = 0x21070008;
public static final int sign_key = 0x21070009;
} }
public static final class dialog { public static final class dialog {
@ -86,6 +94,7 @@ public final class Id {
public static final int help = 0x21070014; public static final int help = 0x21070014;
public static final int querying = 0x21070015; public static final int querying = 0x21070015;
public static final int lookup_unknown_key = 0x21070016; public static final int lookup_unknown_key = 0x21070016;
public static final int signing = 0x21070017;
} }
public static final class task { public static final class task {
@ -166,8 +175,9 @@ public final class Id {
public static final int keys = 2; public static final int keys = 2;
} }
public static final class query { public static final class keyserver {
public static final int search = 0x21070001; public static final int search = 0x21070001;
public static final int get = 0x21070002; public static final int get = 0x21070002;
public static final int add = 0x21070003;
} }
} }

View File

@ -0,0 +1,133 @@
package org.thialfihar.android.apg;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.thialfihar.android.apg.KeyServer.QueryException;
import android.content.Intent;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
public class ImportFromQRCodeActivity extends BaseActivity {
private static final String TAG = "ImportFromQRCodeActivity";
private final Bundle status = new Bundle();
private final Message msg = new Message();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
IntentIntegrator.initiateScan(this);
}
private void importAndSign(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
HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]); // TODO: there should be only 1
String encodedKey = server.get(keyId);
PGPKeyRing keyring = Apg.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 = Apg.convertToHex(publicKeyRing.getPublicKey().getFingerprint());
if (expectedFingerprint.equals(actualFingerprint)) {
// store the signed key in our local cache
int retval = Apg.storeKeyRingInCache(publicKeyRing);
if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
status.putString(Apg.EXTRA_ERROR, "Failed to store signed key in local cache");
} else {
Intent intent = new Intent(ImportFromQRCodeActivity.this, SignKeyActivity.class);
intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
startActivityForResult(intent, Id.request.sign_key);
}
} else {
status.putString(Apg.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(Apg.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(Apg.EXTRA_ERROR, "Failed to query KeyServer");
status.putInt(Constants.extras.status, Id.message.done);
}
}
};
t.setName("KeyExchange Download Thread");
t.setDaemon(true);
t.start();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case IntentIntegrator.REQUEST_CODE: {
boolean debug = true; // TODO: remove this!!!
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (debug || (scanResult != null && scanResult.getFormatName() != null)) {
String[] bits = debug ? new String[] { "5993515643896327656", "0816 F68A 6816 68FB 01BF 2CA5 532D 3EB9 1E2F EDE8" } : scanResult.getContents().split(",");
if (bits.length != 2) {
return; // dont know how to handle this. Not a valid code
}
long keyId = Long.parseLong(bits[0]);
String expectedFingerprint = bits[1];
importAndSign(keyId, expectedFingerprint);
break;
}
}
case Id.request.sign_key: {
// signals the end of processing. Signature was either applied, or it wasnt
status.putInt(Constants.extras.status, Id.message.done);
msg.setData(status);
sendMessage(msg);
break;
}
default: {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
@Override
public void doneCallback(Message msg) {
super.doneCallback(msg);
Bundle data = msg.getData();
String error = data.getString(Apg.EXTRA_ERROR);
if (error != null) {
Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(this, R.string.keySignSuccess, Toast.LENGTH_SHORT).show(); // TODO
finish();
}
}

View File

@ -33,6 +33,9 @@ import org.thialfihar.android.apg.provider.KeyRings;
import org.thialfihar.android.apg.provider.Keys; import org.thialfihar.android.apg.provider.Keys;
import org.thialfihar.android.apg.provider.UserIds; import org.thialfihar.android.apg.provider.UserIds;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.SearchManager; import android.app.SearchManager;
@ -150,7 +153,7 @@ public class KeyListActivity extends BaseActivity {
showDialog(Id.dialog.export_keys); showDialog(Id.dialog.export_keys);
return true; return true;
} }
default: { default: {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -792,7 +795,7 @@ public class KeyListActivity extends BaseActivity {
} }
return; return;
} }
default: { default: {
break; break;
} }

View File

@ -12,12 +12,19 @@ public abstract class KeyServer {
super(message); super(message);
} }
} }
static public class TooManyResponses extends Exception { static public class TooManyResponses extends Exception {
private static final long serialVersionUID = 2703768928624654513L; private static final long serialVersionUID = 2703768928624654513L;
} }
static public class InsufficientQuery extends Exception { static public class InsufficientQuery extends Exception {
private static final long serialVersionUID = 2703768928624654514L; private static final long serialVersionUID = 2703768928624654514L;
} }
static public class AddKeyException extends Exception {
private static final long serialVersionUID = -507574859137295530L;
}
static public class KeyInfo implements Serializable { static public class KeyInfo implements Serializable {
private static final long serialVersionUID = -7797972113284992662L; private static final long serialVersionUID = -7797972113284992662L;
Vector<String> userIds; Vector<String> userIds;
@ -28,6 +35,8 @@ public abstract class KeyServer {
int size; int size;
String algorithm; String algorithm;
} }
abstract List<KeyInfo> search(String query) throws QueryException, TooManyResponses, InsufficientQuery; abstract List<KeyInfo> search(String query) throws QueryException, TooManyResponses, InsufficientQuery;
abstract String get(long keyId) throws QueryException; abstract String get(long keyId) throws QueryException;
abstract void add(String armouredText) throws AddKeyException;
} }

View File

@ -99,7 +99,7 @@ public class KeyServerQueryActivity extends BaseActivity {
private void search(String query) { private void search(String query) {
showDialog(Id.dialog.querying); showDialog(Id.dialog.querying);
mQueryType = Id.query.search; mQueryType = Id.keyserver.search;
mQueryString = query; mQueryString = query;
mAdapter.setKeys(new Vector<KeyInfo>()); mAdapter.setKeys(new Vector<KeyInfo>());
startThread(); startThread();
@ -107,11 +107,11 @@ public class KeyServerQueryActivity extends BaseActivity {
private void get(long keyId) { private void get(long keyId) {
showDialog(Id.dialog.querying); showDialog(Id.dialog.querying);
mQueryType = Id.query.get; mQueryType = Id.keyserver.get;
mQueryId = keyId; mQueryId = keyId;
startThread(); startThread();
} }
protected Dialog onCreateDialog(int id) { protected Dialog onCreateDialog(int id) {
ProgressDialog progress = (ProgressDialog) super.onCreateDialog(id); ProgressDialog progress = (ProgressDialog) super.onCreateDialog(id);
progress.setMessage(this.getString(R.string.progress_queryingServer, progress.setMessage(this.getString(R.string.progress_queryingServer,
@ -127,9 +127,9 @@ public class KeyServerQueryActivity extends BaseActivity {
try { try {
HkpKeyServer server = new HkpKeyServer((String)mKeyServer.getSelectedItem()); HkpKeyServer server = new HkpKeyServer((String)mKeyServer.getSelectedItem());
if (mQueryType == Id.query.search) { if (mQueryType == Id.keyserver.search) {
mSearchResult = server.search(mQueryString); mSearchResult = server.search(mQueryString);
} else if (mQueryType == Id.query.get) { } else if (mQueryType == Id.keyserver.get) {
mKeyData = server.get(mQueryId); mKeyData = server.get(mQueryId);
} }
} catch (QueryException e) { } catch (QueryException e) {
@ -163,12 +163,12 @@ public class KeyServerQueryActivity extends BaseActivity {
return; return;
} }
if (mQueryType == Id.query.search) { if (mQueryType == Id.keyserver.search) {
if (mSearchResult != null) { if (mSearchResult != null) {
Toast.makeText(this, getString(R.string.keysFound, mSearchResult.size()), Toast.LENGTH_SHORT).show(); Toast.makeText(this, getString(R.string.keysFound, mSearchResult.size()), Toast.LENGTH_SHORT).show();
mAdapter.setKeys(mSearchResult); mAdapter.setKeys(mSearchResult);
} }
} else if (mQueryType == Id.query.get) { } else if (mQueryType == Id.keyserver.get) {
Intent orgIntent = getIntent(); Intent orgIntent = getIntent();
if (Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) { if (Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
if (mKeyData != null) { if (mKeyData != null) {

View File

@ -16,9 +16,11 @@
package org.thialfihar.android.apg; package org.thialfihar.android.apg;
import java.security.Security;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.thialfihar.android.apg.provider.Accounts; import org.thialfihar.android.apg.provider.Accounts;
import android.app.AlertDialog; import android.app.AlertDialog;
@ -51,6 +53,10 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class MainActivity extends BaseActivity { public class MainActivity extends BaseActivity {
static {
Security.addProvider(new BouncyCastleProvider());
}
private ListView mAccounts = null; private ListView mAccounts = null;
private AccountListAdapter mListAdapter = null; private AccountListAdapter mListAdapter = null;
private Cursor mAccountCursor; private Cursor mAccountCursor;

View File

@ -18,6 +18,9 @@ package org.thialfihar.android.apg;
import org.spongycastle.openpgp.PGPPublicKeyRing; import org.spongycastle.openpgp.PGPPublicKeyRing;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu; import android.view.ContextMenu;
@ -48,6 +51,8 @@ public class PublicKeyListActivity extends KeyListActivity {
.setIcon(android.R.drawable.ic_menu_preferences); .setIcon(android.R.drawable.ic_menu_preferences);
menu.add(1, Id.menu.option.about, 4, R.string.menu_about) menu.add(1, Id.menu.option.about, 4, R.string.menu_about)
.setIcon(android.R.drawable.ic_menu_info_details); .setIcon(android.R.drawable.ic_menu_info_details);
menu.add(1, Id.menu.option.scanQRCode, 5, R.string.menu_scanQRCode)
.setIcon(android.R.drawable.ic_menu_add);
return true; return true;
} }
@ -63,6 +68,8 @@ public class PublicKeyListActivity extends KeyListActivity {
menu.add(0, Id.menu.export, 0, R.string.menu_exportKey); menu.add(0, Id.menu.export, 0, R.string.menu_exportKey);
menu.add(0, Id.menu.delete, 1, R.string.menu_deleteKey); menu.add(0, Id.menu.delete, 1, R.string.menu_deleteKey);
menu.add(0, Id.menu.update, 1, R.string.menu_updateKey); menu.add(0, Id.menu.update, 1, R.string.menu_updateKey);
menu.add(0, Id.menu.exportToServer, 1, R.string.menu_exportKeyToServer);
menu.add(0, Id.menu.signKey, 1, R.string.menu_signKey);
} }
} }
@ -94,11 +101,62 @@ public class PublicKeyListActivity extends KeyListActivity {
intent.setAction(Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN); intent.setAction(Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN);
intent.putExtra(Apg.EXTRA_KEY_ID, keyId); intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
startActivityForResult(intent, Id.request.look_up_key_id); startActivityForResult(intent, Id.request.look_up_key_id);
return true;
}
case Id.menu.exportToServer: {
mSelectedItem = groupPosition;
final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
Intent intent = new Intent(this, SendKeyActivity.class);
intent.setAction(Apg.Intent.EXPORT_KEY_TO_SERVER);
intent.putExtra(Apg.EXTRA_KEY_ID, keyRingId);
startActivityForResult(intent, Id.request.export_to_server);
return true;
}
case Id.menu.signKey: {
mSelectedItem = groupPosition;
final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
long keyId = 0;
Object keyRing = Apg.getKeyRing(keyRingId);
if (keyRing != null && keyRing instanceof PGPPublicKeyRing) {
keyId = Apg.getMasterKey((PGPPublicKeyRing) keyRing).getKeyID();
}
if (keyId == 0) {
// this shouldn't happen
return true;
}
Intent intent = new Intent(this, SignKeyActivity.class);
intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
startActivity(intent);
return true;
}
default: {
return super.onContextItemSelected(menuItem);
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case Id.menu.option.scanQRCode: {
Intent intent = new Intent(this, ImportFromQRCodeActivity.class);
intent.setAction(Apg.Intent.IMPORT_FROM_QR_CODE);
startActivityForResult(intent, Id.request.import_from_qr_code);
return true; return true;
} }
default: { default: {
return super.onContextItemSelected(menuItem); return super.onOptionsItemSelected(item);
} }
} }
} }
@ -118,7 +176,7 @@ public class PublicKeyListActivity extends KeyListActivity {
handleIntent(intent); handleIntent(intent);
break; break;
} }
default: { default: {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
break; break;

View File

@ -28,6 +28,8 @@ import android.widget.ExpandableListView;
import android.widget.ExpandableListView.ExpandableListContextMenuInfo; import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnChildClickListener;
import com.google.zxing.integration.android.IntentIntegrator;
public class SecretKeyListActivity extends KeyListActivity implements OnChildClickListener { public class SecretKeyListActivity extends KeyListActivity implements OnChildClickListener {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -80,6 +82,7 @@ public class SecretKeyListActivity extends KeyListActivity implements OnChildCli
menu.add(0, Id.menu.edit, 0, R.string.menu_editKey); menu.add(0, Id.menu.edit, 0, R.string.menu_editKey);
menu.add(0, Id.menu.export, 1, R.string.menu_exportKey); menu.add(0, Id.menu.export, 1, R.string.menu_exportKey);
menu.add(0, Id.menu.delete, 2, R.string.menu_deleteKey); menu.add(0, Id.menu.delete, 2, R.string.menu_deleteKey);
menu.add(0, Id.menu.share, 2, R.string.menu_share);
} }
} }
@ -100,6 +103,14 @@ public class SecretKeyListActivity extends KeyListActivity implements OnChildCli
return true; return true;
} }
case Id.menu.share: {
mSelectedItem = groupPosition;
long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
String msg = keyId + "," + Apg.getFingerPrint(keyId);;
IntentIntegrator.shareText(this, msg);
}
default: { default: {
return super.onContextItemSelected(menuItem); return super.onContextItemSelected(menuItem);
} }
@ -169,7 +180,7 @@ public class SecretKeyListActivity extends KeyListActivity implements OnChildCli
} }
break; break;
} }
default: { default: {
break; break;
} }

View File

@ -0,0 +1,93 @@
package org.thialfihar.android.apg;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import android.os.Bundle;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;
/**
* gpg --send-key activity
*
* Sends the selected public key to a key server
*/
public class SendKeyActivity extends BaseActivity {
private Button export;
private Spinner keyServer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.key_server_export_layout);
export = (Button) findViewById(R.id.btn_export_to_server);
keyServer = (Spinner) findViewById(R.id.keyServer);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mPreferences.getKeyServers());
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
keyServer.setAdapter(adapter);
if (adapter.getCount() > 0) {
keyServer.setSelection(0);
} else {
export.setEnabled(false);
}
export.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startThread();
}
});
}
@Override
public void run() {
String error = null;
Bundle data = new Bundle();
Message msg = new Message();
HkpKeyServer server = new HkpKeyServer((String) keyServer.getSelectedItem());
int keyRingId = getIntent().getIntExtra(Apg.EXTRA_KEY_ID, -1);
PGPKeyRing keyring = Apg.getKeyRing(keyRingId);
if (keyring != null && keyring instanceof PGPPublicKeyRing) {
boolean uploaded = Apg.uploadKeyRingToServer(server, (PGPPublicKeyRing) keyring);
if (!uploaded) {
error = "Unable to export key to selected server";
}
}
data.putInt(Constants.extras.status, Id.message.export_done);
if (error != null) {
data.putString(Apg.EXTRA_ERROR, error);
}
msg.setData(data);
sendMessage(msg);
}
@Override
public void doneCallback(Message msg) {
super.doneCallback(msg);
Bundle data = msg.getData();
String error = data.getString(Apg.EXTRA_ERROR);
if (error != null) {
Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(this, R.string.keySendSuccess, Toast.LENGTH_SHORT).show();
finish();
}
}

View File

@ -0,0 +1,284 @@
package org.thialfihar.android.apg;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.util.Iterator;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
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 android.content.Intent;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Spinner;
import android.widget.Toast;
/**
* gpg --sign-key
*
* signs the specified public key with the specified secret master key
*/
public class SignKeyActivity extends BaseActivity {
private static final String TAG = "SignKeyActivity";
private long pubKeyId = 0;
private long masterKeyId = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// check we havent already signed it
setContentView(R.layout.sign_key_layout);
final Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mPreferences.getKeyServers());
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
keyServer.setAdapter(adapter);
final CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
if (!sendKey.isChecked()) {
keyServer.setEnabled(false);
} else {
keyServer.setEnabled(true);
}
sendKey.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (!isChecked) {
keyServer.setEnabled(false);
} else {
keyServer.setEnabled(true);
}
}
});
Button sign = (Button) findViewById(R.id.sign);
sign.setEnabled(false); // disabled until the user selects a key to sign with
sign.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (pubKeyId != 0) {
initiateSigning();
}
}
});
pubKeyId = getIntent().getLongExtra(Apg.EXTRA_KEY_ID, 0);
if (pubKeyId == 0) {
finish(); // nothing to do if we dont know what key to sign
} else {
// kick off the SecretKey selection activity so the user chooses which key to sign with first
Intent intent = new Intent(this, SelectSecretKeyListActivity.class);
startActivityForResult(intent, Id.request.secret_keys);
}
}
/**
* handles the UI bits of the signing process on the UI thread
*/
private void initiateSigning() {
PGPPublicKeyRing pubring = Apg.getPublicKeyRing(pubKeyId);
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(pubKeyId).getSignatures();
while (itr.hasNext()) {
PGPSignature sig = itr.next();
if (sig.getKeyID() == masterKeyId) {
alreadySigned = true;
break;
}
}
if (!alreadySigned) {
/*
* get the user's passphrase for this key (if required)
*/
String passphrase = Apg.getCachedPassPhrase(masterKeyId);
if (passphrase == null) {
showDialog(Id.dialog.pass_phrase);
return; // bail out; need to wait until the user has entered the passphrase before trying again
} else {
startSigning();
}
} else {
final Bundle status = new Bundle();
Message msg = new Message();
status.putString(Apg.EXTRA_ERROR, "Key has already been signed");
status.putInt(Constants.extras.status, Id.message.done);
msg.setData(status);
sendMessage(msg);
setResult(Id.return_value.error);
finish();
}
}
}
@Override
public long getSecretKeyId() {
return masterKeyId;
}
@Override
public void passPhraseCallback(long keyId, String passPhrase) {
super.passPhraseCallback(keyId, passPhrase);
startSigning();
}
/**
* kicks off the actual signing process on a background thread
*/
private void startSigning() {
showDialog(Id.dialog.signing);
startThread();
}
@Override
public void run() {
final Bundle status = new Bundle();
Message msg = new Message();
try {
String passphrase = Apg.getCachedPassPhrase(masterKeyId);
if (passphrase == null || passphrase.length() <= 0) {
status.putString(Apg.EXTRA_ERROR, "Unable to obtain passphrase");
} else {
PGPPublicKeyRing pubring = Apg.getPublicKeyRing(pubKeyId);
/*
* sign the incoming key
*/
PGPSecretKey secretKey = Apg.getSecretKey(masterKeyId);
PGPPrivateKey signingKey = secretKey.extractPrivateKey(passphrase.toCharArray(), BouncyCastleProvider.PROVIDER_NAME);
PGPSignatureGenerator sGen = new PGPSignatureGenerator(secretKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256, BouncyCastleProvider.PROVIDER_NAME);
sGen.initSign(PGPSignature.DIRECT_KEY, signingKey);
PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
PGPSignatureSubpacketVector packetVector = spGen.generate();
sGen.setHashedSubpackets(packetVector);
PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId), sGen.generate());
pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
// check if we need to send the key to the server or not
CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
if (sendKey.isChecked()) {
Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
HkpKeyServer server = new HkpKeyServer((String) keyServer.getSelectedItem());
/*
* upload the newly signed key to the key server
*/
Apg.uploadKeyRingToServer(server, pubring);
}
// store the signed key in our local cache
int retval = Apg.storeKeyRingInCache(pubring);
if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
status.putString(Apg.EXTRA_ERROR, "Failed to store signed key in local cache");
}
}
} catch (PGPException e) {
Log.e(TAG, "Failed to sign key", e);
status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
status.putInt(Constants.extras.status, Id.message.done);
return;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "Failed to sign key", e);
status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
status.putInt(Constants.extras.status, Id.message.done);
return;
} catch (NoSuchProviderException e) {
Log.e(TAG, "Failed to sign key", e);
status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
status.putInt(Constants.extras.status, Id.message.done);
return;
} catch (SignatureException e) {
Log.e(TAG, "Failed to sign key", e);
status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
status.putInt(Constants.extras.status, Id.message.done);
return;
}
status.putInt(Constants.extras.status, Id.message.done);
msg.setData(status);
sendMessage(msg);
if (status.containsKey(Apg.EXTRA_ERROR)) {
setResult(Id.return_value.error);
} else {
setResult(Id.return_value.ok);
}
finish();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case Id.request.secret_keys: {
if (resultCode == RESULT_OK) {
masterKeyId = data.getLongExtra(Apg.EXTRA_KEY_ID, 0);
// re-enable the sign button so the user can initiate the sign process
Button sign = (Button) findViewById(R.id.sign);
sign.setEnabled(true);
}
break;
}
default: {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
@Override
public void doneCallback(Message msg) {
super.doneCallback(msg);
removeDialog(Id.dialog.signing);
Bundle data = msg.getData();
String error = data.getString(Apg.EXTRA_ERROR);
if (error != null) {
Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(this, R.string.keySignSuccess, Toast.LENGTH_SHORT).show();
finish();
}
}