rewrote sign-only code, also finally recognize sign-only emails in the list and allow opening them for verification

This commit is contained in:
Thialfihar 2010-04-15 16:37:32 +00:00
parent acd71a45c0
commit c212f28c44
6 changed files with 310 additions and 33 deletions

View File

@ -25,7 +25,7 @@
android:layout_width="fill_parent"> android:layout_width="fill_parent">
<ImageView <ImageView
android:id="@+id/ic_encrypted" android:id="@+id/ic_status"
android:src="@drawable/encrypted" android:src="@drawable/encrypted"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -32,6 +32,7 @@
<string name="btn_send">Send via Email</string> <string name="btn_send">Send via Email</string>
<string name="btn_decrypt">Decrypt</string> <string name="btn_decrypt">Decrypt</string>
<string name="btn_verify">Verify</string>
<string name="btn_selectEncryptKeys">Select Recipients</string> <string name="btn_selectEncryptKeys">Select Recipients</string>
<string name="btn_reply">Reply</string> <string name="btn_reply">Reply</string>
<string name="btn_encryptMessage">Encrypt Message</string> <string name="btn_encryptMessage">Encrypt Message</string>

View File

@ -16,7 +16,9 @@
package org.thialfihar.android.apg; package org.thialfihar.android.apg;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -42,6 +44,7 @@ import java.util.HashMap;
import java.util.Vector; import java.util.Vector;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bouncycastle2.bcpg.ArmoredInputStream;
import org.bouncycastle2.bcpg.ArmoredOutputStream; import org.bouncycastle2.bcpg.ArmoredOutputStream;
import org.bouncycastle2.bcpg.BCPGOutputStream; import org.bouncycastle2.bcpg.BCPGOutputStream;
import org.bouncycastle2.bcpg.CompressionAlgorithmTags; import org.bouncycastle2.bcpg.CompressionAlgorithmTags;
@ -125,6 +128,10 @@ public class Apg {
Pattern.compile(".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.compile(".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*",
Pattern.DOTALL); Pattern.DOTALL);
public static Pattern PGP_SIGNED_MESSAGE =
Pattern.compile(".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
protected static boolean mInitialized = false; protected static boolean mInitialized = false;
protected static final int RETURN_NO_MASTER_KEY = -2; protected static final int RETURN_NO_MASTER_KEY = -2;
@ -1247,8 +1254,9 @@ public class Apg {
progress.setProgress("done.", 100, 100); progress.setProgress("done.", 100, 100);
} }
public static void sign(InputStream inStream, OutputStream outStream, public static void signText(InputStream inStream, OutputStream outStream,
long signatureKeyId, String signaturePassPhrase, long signatureKeyId, String signaturePassPhrase,
int hashAlgorithm,
ProgressDialogUpdater progress) ProgressDialogUpdater progress)
throws GeneralException, PGPException, IOException, NoSuchAlgorithmException, throws GeneralException, PGPException, IOException, NoSuchAlgorithmException,
SignatureException { SignatureException {
@ -1256,8 +1264,6 @@ public class Apg {
ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream); ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream);
armorOut.setHeader("Version", FULL_VERSION); armorOut.setHeader("Version", FULL_VERSION);
OutputStream out = armorOut;
OutputStream signOut = out;
PGPSecretKey signingKey = null; PGPSecretKey signingKey = null;
PGPSecretKeyRing signingKeyRing = null; PGPSecretKeyRing signingKeyRing = null;
@ -1286,7 +1292,7 @@ public class Apg {
progress.setProgress("preparing signature...", 30, 100); progress.setProgress("preparing signature...", 30, 100);
signatureGenerator = signatureGenerator =
new PGPSignatureGenerator(signingKey.getPublicKey().getAlgorithm(), new PGPSignatureGenerator(signingKey.getPublicKey().getAlgorithm(),
HashAlgorithmTags.SHA1, hashAlgorithm,
new BouncyCastleProvider()); new BouncyCastleProvider());
signatureGenerator.initSign(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey); signatureGenerator.initSign(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey);
String userId = getMainUserId(getMasterKey(signingKeyRing)); String userId = getMainUserId(getMasterKey(signingKeyRing));
@ -1296,15 +1302,31 @@ public class Apg {
signatureGenerator.setHashedSubpackets(spGen.generate()); signatureGenerator.setHashedSubpackets(spGen.generate());
progress.setProgress("signing...", 40, 100); progress.setProgress("signing...", 40, 100);
int n = 0;
byte[] buffer = new byte[1 << 16]; armorOut.beginClearText(hashAlgorithm);
while ((n = inStream.read(buffer)) > 0) {
signatureGenerator.update(buffer, 0, n); ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
int lookAhead = readInputLine(lineOut, inStream);
processLine(armorOut, signatureGenerator, lineOut.toByteArray());
if (lookAhead != -1) {
do {
lookAhead = readInputLine(lineOut, lookAhead, inStream);
signatureGenerator.update((byte)'\r');
signatureGenerator.update((byte)'\n');
processLine(armorOut, signatureGenerator, lineOut.toByteArray());
}
while (lookAhead != -1);
} }
signatureGenerator.generate().encode(signOut); armorOut.endClearText();
signOut.close();
out.close(); BCPGOutputStream bOut = new BCPGOutputStream(armorOut);
signatureGenerator.generate().encode(bOut);
armorOut.close();
progress.setProgress("done.", 100, 100); progress.setProgress("done.", 100, 100);
} }
@ -1492,6 +1514,108 @@ public class Apg {
return returnData; return returnData;
} }
public static Bundle verifyText(InputStream inStream, OutputStream outStream,
ProgressDialogUpdater progress)
throws IOException, GeneralException, PGPException, SignatureException {
Bundle returnData = new Bundle();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ArmoredInputStream aIn = new ArmoredInputStream(inStream);
progress.setProgress("reading data...", 0, 100);
// mostly taken from CLearSignedFileProcessor
ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
int lookAhead = readInputLine(lineOut, aIn);
byte[] lineSep = getLineSeparator();
if (lookAhead != -1 && aIn.isClearText())
{
byte[] line = lineOut.toByteArray();
out.write(line, 0, getLengthWithoutSeparator(line));
out.write(lineSep);
while (lookAhead != -1 && aIn.isClearText())
{
lookAhead = readInputLine(lineOut, lookAhead, aIn);
line = lineOut.toByteArray();
out.write(line, 0, getLengthWithoutSeparator(line));
out.write(lineSep);
}
}
out.close();
byte[] clearText = out.toByteArray();
outStream.write(clearText);
returnData.putBoolean("signature", true);
progress.setProgress("processing signature...", 60, 100);
PGPObjectFactory pgpFact = new PGPObjectFactory(aIn);
PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject();
if (sigList == null) {
throw new GeneralException("corrupt data");
}
PGPSignature signature = null;
long signatureKeyId = 0;
PGPPublicKey signatureKey = null;
for (int i = 0; i < sigList.size(); ++i) {
signature = sigList.get(i);
signatureKey = findPublicKey(signature.getKeyID());
if (signatureKeyId == 0) {
signatureKeyId = signature.getKeyID();
}
if (signatureKey == null) {
signature = null;
} else {
signatureKeyId = signature.getKeyID();
String userId = null;
PGPPublicKeyRing sigKeyRing = findPublicKeyRing(signatureKeyId);
if (sigKeyRing != null) {
userId = getMainUserId(getMasterKey(sigKeyRing));
}
returnData.putString("signatureUserId", userId);
break;
}
}
returnData.putLong("signatureKeyId", signatureKeyId);
if (signature == null) {
returnData.putBoolean("signatureUnknown", true);
progress.setProgress("done.", 100, 100);
return returnData;
}
signature.initVerify(signatureKey, new BouncyCastleProvider());
InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText));
lookAhead = readInputLine(lineOut, sigIn);
processLine(signature, lineOut.toByteArray());
if (lookAhead != -1) {
do {
lookAhead = readInputLine(lineOut, lookAhead, sigIn);
signature.update((byte)'\r');
signature.update((byte)'\n');
processLine(signature, lineOut.toByteArray());
}
while (lookAhead != -1);
}
returnData.putBoolean("signatureSuccess", signature.verify());
progress.setProgress("done.", 100, 100);
return returnData;
}
public static Vector<PGPPublicKeyRing> getPublicKeyRings() { public static Vector<PGPPublicKeyRing> getPublicKeyRings() {
return mPublicKeyRings; return mPublicKeyRings;
} }
@ -1499,4 +1623,120 @@ public class Apg {
public static Vector<PGPSecretKeyRing> getSecretKeyRings() { public static Vector<PGPSecretKeyRing> getSecretKeyRings() {
return mSecretKeyRings; return mSecretKeyRings;
} }
// taken from ClearSignedFileProcessor in BC
private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
throws IOException
{
bOut.reset();
int lookAhead = -1;
int ch;
while ((ch = fIn.read()) >= 0)
{
bOut.write(ch);
if (ch == '\r' || ch == '\n')
{
lookAhead = readPassedEOL(bOut, ch, fIn);
break;
}
}
return lookAhead;
}
private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn)
throws IOException {
bOut.reset();
int ch = lookAhead;
do {
bOut.write(ch);
if (ch == '\r' || ch == '\n') {
lookAhead = readPassedEOL(bOut, ch, fIn);
break;
}
}
while ((ch = fIn.read()) >= 0);
if (ch < 0) {
lookAhead = -1;
}
return lookAhead;
}
private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
throws IOException {
int lookAhead = fIn.read();
if (lastCh == '\r' && lookAhead == '\n') {
bOut.write(lookAhead);
lookAhead = fIn.read();
}
return lookAhead;
}
private static void processLine(PGPSignature sig, byte[] line)
throws SignatureException, IOException {
int length = getLengthWithoutWhiteSpace(line);
if (length > 0)
{
sig.update(line, 0, length);
}
}
private static void processLine(OutputStream aOut, PGPSignatureGenerator sGen, byte[] line)
throws SignatureException, IOException {
int length = getLengthWithoutWhiteSpace(line);
if (length > 0)
{
sGen.update(line, 0, length);
}
aOut.write(line, 0, line.length);
}
private static int getLengthWithoutSeparator(byte[] line) {
int end = line.length - 1;
while (end >= 0 && isLineEnding(line[end])) {
end--;
}
return end + 1;
}
private static boolean isLineEnding(byte b) {
return b == '\r' || b == '\n';
}
private static int getLengthWithoutWhiteSpace(byte[] line) {
int end = line.length - 1;
while (end >= 0 && isWhiteSpace(line[end])) {
end--;
}
return end + 1;
}
private static boolean isWhiteSpace(byte b) {
return b == '\r' || b == '\n' || b == '\t' || b == ' ';
}
private static byte[] getLineSeparator() {
String nl = System.getProperty("line.separator");
byte[] nlBytes = new byte[nl.length()];
for (int i = 0; i != nlBytes.length; i++) {
nlBytes[i] = (byte)nl.charAt(i);
}
return nlBytes;
}
} }

View File

@ -63,6 +63,7 @@ public class DecryptMessageActivity extends Activity
private String mReplyTo = null; private String mReplyTo = null;
private String mSubject = null; private String mSubject = null;
private boolean mSignedOnly = false;
private ProgressDialog mProgressDialog = null; private ProgressDialog mProgressDialog = null;
private Thread mRunningThread = null; private Thread mRunningThread = null;
@ -193,6 +194,15 @@ public class DecryptMessageActivity extends Activity
// replace non breakable spaces // replace non breakable spaces
data = data.replaceAll("\\xa0", " "); data = data.replaceAll("\\xa0", " ");
mMessage.setText(data); mMessage.setText(data);
} else {
matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data);
if (matcher.matches()) {
data = matcher.group(1);
// replace non breakable spaces
data = data.replaceAll("\\xa0", " ");
mMessage.setText(data);
mDecryptButton.setText(R.string.btn_verify);
}
} }
} }
mReplyTo = intent.getExtras().getString("replyTo"); mReplyTo = intent.getExtras().getString("replyTo");
@ -266,8 +276,18 @@ public class DecryptMessageActivity extends Activity
private void decryptClicked() { private void decryptClicked() {
String error = null; String error = null;
String messageData = mMessage.getText().toString();
Matcher matcher = Apg.PGP_SIGNED_MESSAGE.matcher(messageData);
if (matcher.matches()) {
mSignedOnly = true;
decryptStart();
return;
}
// else treat it as an encrypted message
mSignedOnly = false;
ByteArrayInputStream in = ByteArrayInputStream in =
new ByteArrayInputStream(mMessage.getText().toString().getBytes()); new ByteArrayInputStream(messageData.getBytes());
try { try {
mDecryptionKeyId = Apg.getDecryptionKeyId(in); mDecryptionKeyId = Apg.getDecryptionKeyId(in);
showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE); showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE);
@ -320,7 +340,11 @@ public class DecryptMessageActivity extends Activity
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
try { try {
if (mSignedOnly) {
data = Apg.verifyText(in, out, this);
} else {
data = Apg.decrypt(in, out, Apg.getPassPhrase(), this); data = Apg.decrypt(in, out, Apg.getPassPhrase(), this);
}
} catch (PGPException e) { } catch (PGPException e) {
error = e.getMessage(); error = e.getMessage();
} catch (IOException e) { } catch (IOException e) {

View File

@ -24,6 +24,7 @@ import java.security.NoSuchProviderException;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.Vector; import java.util.Vector;
import org.bouncycastle2.bcpg.HashAlgorithmTags;
import org.bouncycastle2.openpgp.PGPException; import org.bouncycastle2.openpgp.PGPException;
import org.bouncycastle2.openpgp.PGPPublicKey; import org.bouncycastle2.openpgp.PGPPublicKey;
import org.bouncycastle2.openpgp.PGPPublicKeyRing; import org.bouncycastle2.openpgp.PGPPublicKeyRing;
@ -104,16 +105,9 @@ public class EncryptMessageActivity extends Activity
return; return;
} else { } else {
String message = data.getString("message"); String message = data.getString("message");
String signature = data.getString("signature");
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("text/plain; charset=utf-8"); emailIntent.setType("text/plain; charset=utf-8");
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message); emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message);
if (signature != null) {
String fullText = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Hash: SHA256\n" + "\n" +
message + "\n" + signature;
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, fullText);
}
if (mSubject != null) { if (mSubject != null) {
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
mSubject); mSubject);
@ -305,7 +299,9 @@ public class EncryptMessageActivity extends Activity
message = message.replaceAll(" +\n", "\n"); message = message.replaceAll(" +\n", "\n");
message = message.replaceAll("\n\n+", "\n\n"); message = message.replaceAll("\n\n+", "\n\n");
message = message.replaceFirst("^\n+", ""); message = message.replaceFirst("^\n+", "");
message = message.replaceFirst("\n+$", ""); // make sure there'll be exactly one newline at the end
message = message.replaceFirst("\n*$", "\n");
ByteArrayInputStream in = ByteArrayInputStream in =
new ByteArrayInputStream(Strings.toUTF8ByteArray(message)); new ByteArrayInputStream(Strings.toUTF8ByteArray(message));
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
@ -316,9 +312,9 @@ public class EncryptMessageActivity extends Activity
Apg.getPassPhrase(), this); Apg.getPassPhrase(), this);
data.putString("message", new String(out.toByteArray())); data.putString("message", new String(out.toByteArray()));
} else { } else {
Apg.sign(in, out, mSignatureKeyId, Apg.getPassPhrase(), this); Apg.signText(in, out, mSignatureKeyId,
data.putString("message", message); Apg.getPassPhrase(), HashAlgorithmTags.SHA256, this);
data.putString("signature", new String(out.toByteArray())); data.putString("message", new String(out.toByteArray()));
} }
} catch (IOException e) { } catch (IOException e) {
error = e.getMessage(); error = e.getMessage();

View File

@ -57,9 +57,11 @@ public class MailListActivity extends ListActivity {
public String fromAddress; public String fromAddress;
public String data; public String data;
public String replyTo; public String replyTo;
public boolean signedOnly;
public Message(Conversation parent, long id, String subject, public Message(Conversation parent, long id, String subject,
String fromAddress, String replyTo, String data) { String fromAddress, String replyTo,
String data, boolean signedOnly) {
this.parent = parent; this.parent = parent;
this.id = id; this.id = id;
this.subject = subject; this.subject = subject;
@ -69,6 +71,7 @@ public class MailListActivity extends ListActivity {
if (this.replyTo == null || this.replyTo.equals("")) { if (this.replyTo == null || this.replyTo.equals("")) {
this.replyTo = this.fromAddress; this.replyTo = this.fromAddress;
} }
this.signedOnly = signedOnly;
} }
} }
@ -115,18 +118,26 @@ public class MailListActivity extends ListActivity {
int bodyIndex = messageCursor.getColumnIndex("body"); int bodyIndex = messageCursor.getColumnIndex("body");
String data = messageCursor.getString(bodyIndex); String data = messageCursor.getString(bodyIndex);
data = Html.fromHtml(data).toString(); data = Html.fromHtml(data).toString();
boolean signedOnly = false;
Matcher matcher = Apg.PGP_MESSAGE.matcher(data); Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
if (matcher.matches()) { if (matcher.matches()) {
data = matcher.group(1); data = matcher.group(1);
} else {
matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data);
if (matcher.matches()) {
data = matcher.group(1);
signedOnly = true;
} else { } else {
data = null; data = null;
} }
}
Message message = Message message =
new Message(conversation, new Message(conversation,
messageCursor.getLong(idIndex), messageCursor.getLong(idIndex),
messageCursor.getString(subjectIndex), messageCursor.getString(subjectIndex),
messageCursor.getString(fromAddressIndex), messageCursor.getString(fromAddressIndex),
messageCursor.getString(replyToIndex), data); messageCursor.getString(replyToIndex),
data, signedOnly);
messages.add(message); messages.add(message);
mmessages.add(message); mmessages.add(message);
@ -186,14 +197,19 @@ public class MailListActivity extends ListActivity {
TextView subject = (TextView) view.findViewById(R.id.subject); TextView subject = (TextView) view.findViewById(R.id.subject);
TextView email = (TextView) view.findViewById(R.id.email_address); TextView email = (TextView) view.findViewById(R.id.email_address);
ImageView encrypted = (ImageView) view.findViewById(R.id.ic_encrypted); ImageView status = (ImageView) view.findViewById(R.id.ic_status);
subject.setText(message.subject); subject.setText(message.subject);
email.setText(message.fromAddress); email.setText(message.fromAddress);
if (message.data != null) { if (message.data != null) {
encrypted.setVisibility(View.VISIBLE); if (message.signedOnly) {
status.setImageResource(R.drawable.signed);
} else { } else {
encrypted.setVisibility(View.INVISIBLE); status.setImageResource(R.drawable.encrypted);
}
status.setVisibility(View.VISIBLE);
} else {
status.setVisibility(View.INVISIBLE);
} }
return view; return view;