From c212f28c446044acb5fc7ef7487b95b777b39c44 Mon Sep 17 00:00:00 2001 From: Thialfihar Date: Thu, 15 Apr 2010 16:37:32 +0000 Subject: [PATCH] rewrote sign-only code, also finally recognize sign-only emails in the list and allow opening them for verification --- res/layout/mailbox_message_item.xml | 2 +- res/values/strings.xml | 1 + src/org/thialfihar/android/apg/Apg.java | 266 +++++++++++++++++- .../android/apg/DecryptMessageActivity.java | 28 +- .../android/apg/EncryptMessageActivity.java | 18 +- .../android/apg/MailListActivity.java | 28 +- 6 files changed, 310 insertions(+), 33 deletions(-) diff --git a/res/layout/mailbox_message_item.xml b/res/layout/mailbox_message_item.xml index a5858fba3..6026909d3 100644 --- a/res/layout/mailbox_message_item.xml +++ b/res/layout/mailbox_message_item.xml @@ -25,7 +25,7 @@ android:layout_width="fill_parent"> Send via Email Decrypt + Verify Select Recipients Reply Encrypt Message diff --git a/src/org/thialfihar/android/apg/Apg.java b/src/org/thialfihar/android/apg/Apg.java index affc78cd7..91bfb3b44 100644 --- a/src/org/thialfihar/android/apg/Apg.java +++ b/src/org/thialfihar/android/apg/Apg.java @@ -16,7 +16,9 @@ package org.thialfihar.android.apg; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -42,6 +44,7 @@ import java.util.HashMap; import java.util.Vector; import java.util.regex.Pattern; +import org.bouncycastle2.bcpg.ArmoredInputStream; import org.bouncycastle2.bcpg.ArmoredOutputStream; import org.bouncycastle2.bcpg.BCPGOutputStream; import org.bouncycastle2.bcpg.CompressionAlgorithmTags; @@ -125,6 +128,10 @@ public class Apg { 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); + protected static boolean mInitialized = false; protected static final int RETURN_NO_MASTER_KEY = -2; @@ -1247,17 +1254,16 @@ public class Apg { progress.setProgress("done.", 100, 100); } - public static void sign(InputStream inStream, OutputStream outStream, - long signatureKeyId, String signaturePassPhrase, - ProgressDialogUpdater progress) + public static void signText(InputStream inStream, OutputStream outStream, + long signatureKeyId, String signaturePassPhrase, + int hashAlgorithm, + ProgressDialogUpdater progress) throws GeneralException, PGPException, IOException, NoSuchAlgorithmException, SignatureException { Security.addProvider(new BouncyCastleProvider()); ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream); armorOut.setHeader("Version", FULL_VERSION); - OutputStream out = armorOut; - OutputStream signOut = out; PGPSecretKey signingKey = null; PGPSecretKeyRing signingKeyRing = null; @@ -1286,7 +1292,7 @@ public class Apg { progress.setProgress("preparing signature...", 30, 100); signatureGenerator = new PGPSignatureGenerator(signingKey.getPublicKey().getAlgorithm(), - HashAlgorithmTags.SHA1, + hashAlgorithm, new BouncyCastleProvider()); signatureGenerator.initSign(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey); String userId = getMainUserId(getMasterKey(signingKeyRing)); @@ -1296,15 +1302,31 @@ public class Apg { signatureGenerator.setHashedSubpackets(spGen.generate()); progress.setProgress("signing...", 40, 100); - int n = 0; - byte[] buffer = new byte[1 << 16]; - while ((n = inStream.read(buffer)) > 0) { - signatureGenerator.update(buffer, 0, n); + + armorOut.beginClearText(hashAlgorithm); + + 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); - signOut.close(); - out.close(); + armorOut.endClearText(); + + BCPGOutputStream bOut = new BCPGOutputStream(armorOut); + signatureGenerator.generate().encode(bOut); + armorOut.close(); progress.setProgress("done.", 100, 100); } @@ -1492,6 +1514,108 @@ public class Apg { 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 getPublicKeyRings() { return mPublicKeyRings; } @@ -1499,4 +1623,120 @@ public class Apg { public static Vector getSecretKeyRings() { 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; + } } diff --git a/src/org/thialfihar/android/apg/DecryptMessageActivity.java b/src/org/thialfihar/android/apg/DecryptMessageActivity.java index 055c8256c..8b7985c77 100644 --- a/src/org/thialfihar/android/apg/DecryptMessageActivity.java +++ b/src/org/thialfihar/android/apg/DecryptMessageActivity.java @@ -63,6 +63,7 @@ public class DecryptMessageActivity extends Activity private String mReplyTo = null; private String mSubject = null; + private boolean mSignedOnly = false; private ProgressDialog mProgressDialog = null; private Thread mRunningThread = null; @@ -193,6 +194,15 @@ public class DecryptMessageActivity extends Activity // replace non breakable spaces data = data.replaceAll("\\xa0", " "); 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"); @@ -266,8 +276,18 @@ public class DecryptMessageActivity extends Activity private void decryptClicked() { 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 = - new ByteArrayInputStream(mMessage.getText().toString().getBytes()); + new ByteArrayInputStream(messageData.getBytes()); try { mDecryptionKeyId = Apg.getDecryptionKeyId(in); showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE); @@ -320,7 +340,11 @@ public class DecryptMessageActivity extends Activity ByteArrayOutputStream out = new ByteArrayOutputStream(); try { - data = Apg.decrypt(in, out, Apg.getPassPhrase(), this); + if (mSignedOnly) { + data = Apg.verifyText(in, out, this); + } else { + data = Apg.decrypt(in, out, Apg.getPassPhrase(), this); + } } catch (PGPException e) { error = e.getMessage(); } catch (IOException e) { diff --git a/src/org/thialfihar/android/apg/EncryptMessageActivity.java b/src/org/thialfihar/android/apg/EncryptMessageActivity.java index af2eac82d..b954f31a1 100644 --- a/src/org/thialfihar/android/apg/EncryptMessageActivity.java +++ b/src/org/thialfihar/android/apg/EncryptMessageActivity.java @@ -24,6 +24,7 @@ import java.security.NoSuchProviderException; import java.security.SignatureException; import java.util.Vector; +import org.bouncycastle2.bcpg.HashAlgorithmTags; import org.bouncycastle2.openpgp.PGPException; import org.bouncycastle2.openpgp.PGPPublicKey; import org.bouncycastle2.openpgp.PGPPublicKeyRing; @@ -104,16 +105,9 @@ public class EncryptMessageActivity extends Activity return; } else { String message = data.getString("message"); - String signature = data.getString("signature"); Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); emailIntent.setType("text/plain; charset=utf-8"); 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) { emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, mSubject); @@ -305,7 +299,9 @@ public class EncryptMessageActivity extends Activity message = message.replaceAll(" +\n", "\n"); message = message.replaceAll("\n\n+", "\n\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 = new ByteArrayInputStream(Strings.toUTF8ByteArray(message)); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -316,9 +312,9 @@ public class EncryptMessageActivity extends Activity Apg.getPassPhrase(), this); data.putString("message", new String(out.toByteArray())); } else { - Apg.sign(in, out, mSignatureKeyId, Apg.getPassPhrase(), this); - data.putString("message", message); - data.putString("signature", new String(out.toByteArray())); + Apg.signText(in, out, mSignatureKeyId, + Apg.getPassPhrase(), HashAlgorithmTags.SHA256, this); + data.putString("message", new String(out.toByteArray())); } } catch (IOException e) { error = e.getMessage(); diff --git a/src/org/thialfihar/android/apg/MailListActivity.java b/src/org/thialfihar/android/apg/MailListActivity.java index 570e761df..ed207d4cd 100644 --- a/src/org/thialfihar/android/apg/MailListActivity.java +++ b/src/org/thialfihar/android/apg/MailListActivity.java @@ -57,9 +57,11 @@ public class MailListActivity extends ListActivity { public String fromAddress; public String data; public String replyTo; + public boolean signedOnly; 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.id = id; this.subject = subject; @@ -69,6 +71,7 @@ public class MailListActivity extends ListActivity { if (this.replyTo == null || this.replyTo.equals("")) { this.replyTo = this.fromAddress; } + this.signedOnly = signedOnly; } } @@ -115,18 +118,26 @@ public class MailListActivity extends ListActivity { int bodyIndex = messageCursor.getColumnIndex("body"); String data = messageCursor.getString(bodyIndex); data = Html.fromHtml(data).toString(); + boolean signedOnly = false; Matcher matcher = Apg.PGP_MESSAGE.matcher(data); if (matcher.matches()) { data = matcher.group(1); } else { - data = null; + matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data); + if (matcher.matches()) { + data = matcher.group(1); + signedOnly = true; + } else { + data = null; + } } Message message = new Message(conversation, messageCursor.getLong(idIndex), messageCursor.getString(subjectIndex), messageCursor.getString(fromAddressIndex), - messageCursor.getString(replyToIndex), data); + messageCursor.getString(replyToIndex), + data, signedOnly); messages.add(message); mmessages.add(message); @@ -186,14 +197,19 @@ public class MailListActivity extends ListActivity { TextView subject = (TextView) view.findViewById(R.id.subject); 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); email.setText(message.fromAddress); if (message.data != null) { - encrypted.setVisibility(View.VISIBLE); + if (message.signedOnly) { + status.setImageResource(R.drawable.signed); + } else { + status.setImageResource(R.drawable.encrypted); + } + status.setVisibility(View.VISIBLE); } else { - encrypted.setVisibility(View.INVISIBLE); + status.setVisibility(View.INVISIBLE); } return view;