summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArne Schwabe <arne@rfc2549.org>2012-06-28 19:33:05 +0200
committerArne Schwabe <arne@rfc2549.org>2012-06-28 19:33:05 +0200
commit78172a10165a969b8c002b6bdcf9dc47fa6cd5f3 (patch)
treeaa4f487db9426822dd005b2fb8b5b677139ec6f9
parent7cb22c98cc326aceb6a9672ebddc6988703dc1c8 (diff)
The 'be ready for Jelly Beans' commit
- fix concurrentaccess occuring on JB - JB does not allow to extract the private keys from the key storage, rewrite using the key storage to use JAVA API and the external-key management interface
-rw-r--r--openvpn/src/openvpn/options.c4
-rw-r--r--res/values/strings.xml1
-rw-r--r--src/de/blinkt/openvpn/ConfigConverter.java1
-rw-r--r--src/de/blinkt/openvpn/FileSelect.java2
-rw-r--r--src/de/blinkt/openvpn/OpenVPN.java6
-rw-r--r--src/de/blinkt/openvpn/OpenVpnManagementThread.java40
-rw-r--r--src/de/blinkt/openvpn/VpnProfile.java100
-rw-r--r--src/org/spongycastle/util/encoders/Base64.java121
-rw-r--r--src/org/spongycastle/util/encoders/Base64Encoder.java298
-rw-r--r--src/org/spongycastle/util/encoders/Encoder.java17
-rw-r--r--src/org/spongycastle/util/io/pem/PemGenerationException.java26
-rw-r--r--src/org/spongycastle/util/io/pem/PemHeader.java66
-rw-r--r--src/org/spongycastle/util/io/pem/PemObject.java62
-rw-r--r--src/org/spongycastle/util/io/pem/PemObjectGenerator.java7
-rw-r--r--src/org/spongycastle/util/io/pem/PemWriter.java138
15 files changed, 839 insertions, 50 deletions
diff --git a/openvpn/src/openvpn/options.c b/openvpn/src/openvpn/options.c
index 7b7fc684..b83c1de6 100644
--- a/openvpn/src/openvpn/options.c
+++ b/openvpn/src/openvpn/options.c
@@ -2730,6 +2730,10 @@ options_postprocess_filechecks (struct options *options)
errs |= check_file_access (CHKACC_FILE|CHKACC_INLINE, options->cert_file, R_OK, "--cert");
errs |= check_file_access (CHKACC_FILE|CHKACC_INLINE, options->extra_certs_file, R_OK,
"--extra-certs");
+
+#ifdef MANAGMENT_EXTERNAL_KEY
+ if(!(options->management_flags | MF_EXTERNAL_KEY))
+#endif
errs |= check_file_access (CHKACC_FILE|CHKACC_INLINE, options->priv_key_file, R_OK,
"--key");
errs |= check_file_access (CHKACC_FILE|CHKACC_INLINE, options->pkcs12_file, R_OK,
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 64b8e28b..ba4f6b02 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -220,5 +220,6 @@
<string name="keppstatus_summary">Keep the notification displayed after the connection is established to show traffic statistics.</string>
<string name="keepstatus">Show Traffic Statistics</string>
<string name="mobile_info">Running on %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Error signing with Android keystore key %s</string>
</resources>
diff --git a/src/de/blinkt/openvpn/ConfigConverter.java b/src/de/blinkt/openvpn/ConfigConverter.java
index 5e0a6eb3..3ccda053 100644
--- a/src/de/blinkt/openvpn/ConfigConverter.java
+++ b/src/de/blinkt/openvpn/ConfigConverter.java
@@ -134,6 +134,7 @@ public class ConfigConverter extends ListActivity {
filedata += new String(buf,0,len);
len = fis.read(buf);
}
+ fis.close();
return filedata;
} catch (FileNotFoundException e) {
log(e.getLocalizedMessage());
diff --git a/src/de/blinkt/openvpn/FileSelect.java b/src/de/blinkt/openvpn/FileSelect.java
index c235594e..bbad5cfd 100644
--- a/src/de/blinkt/openvpn/FileSelect.java
+++ b/src/de/blinkt/openvpn/FileSelect.java
@@ -113,7 +113,7 @@ public class FileSelect extends Activity {
data += new String(buf,0,len);
len=fis.read(buf);
}
-
+ fis.close();
mData =data;
mInlineFragment.setData(data);
getActionBar().selectTab(inlineFileTab);
diff --git a/src/de/blinkt/openvpn/OpenVPN.java b/src/de/blinkt/openvpn/OpenVPN.java
index 64ecf17c..db980200 100644
--- a/src/de/blinkt/openvpn/OpenVPN.java
+++ b/src/de/blinkt/openvpn/OpenVPN.java
@@ -147,7 +147,7 @@ public class OpenVPN {
}
- public static void updateStateString(String state, String msg) {
+ public synchronized static void updateStateString(String state, String msg) {
for (StateListener sl : stateListener) {
sl.updateState(state,msg);
}
@@ -179,7 +179,9 @@ public class OpenVPN {
public static void logError(int ressourceId) {
newlogItem(new LogItem(LogItem.ERROR, ressourceId));
}
-
+ public static void logError(int ressourceId, Object... args) {
+ newlogItem(new LogItem(LogItem.ERROR, ressourceId,args));
+ }
}
diff --git a/src/de/blinkt/openvpn/OpenVpnManagementThread.java b/src/de/blinkt/openvpn/OpenVpnManagementThread.java
index f23d9d9b..e1b37342 100644
--- a/src/de/blinkt/openvpn/OpenVpnManagementThread.java
+++ b/src/de/blinkt/openvpn/OpenVpnManagementThread.java
@@ -5,11 +5,20 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
import java.util.LinkedList;
import java.util.Vector;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
import android.net.LocalSocket;
import android.os.ParcelFileDescriptor;
+import android.util.Base64;
import android.util.Log;
public class OpenVpnManagementThread implements Runnable {
@@ -173,6 +182,8 @@ public class OpenVpnManagementThread implements Runnable {
// 1 log level N,I,E etc.
// 2 log message
OpenVPN.logMessage(0, "", args[2]);
+ } else if (cmd.equals("RSA_SIGN")) {
+ processSignCommand(argument);
} else {
OpenVPN.logMessage(0, "MGMT:", "Got unrecognized command" + command);
Log.i(TAG, "Got unrecognized command" + command);
@@ -389,4 +400,33 @@ public class OpenVpnManagementThread implements Runnable {
}
+ private void processSignCommand(String b64data) {
+ PrivateKey privkey = mProfile.getKeystoreKey();
+ Exception err =null;
+ try{
+ byte[] data = Base64.decode(b64data, Base64.DEFAULT);
+ Cipher rsasinger = javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1PADDING");
+ rsasinger.init(Cipher.ENCRYPT_MODE, privkey);
+
+ byte[] signed_bytes = rsasinger.doFinal(data);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+ } catch (NoSuchAlgorithmException e){
+ err =e;
+ } catch (InvalidKeyException e) {
+ err =e;
+ } catch (NoSuchPaddingException e) {
+ err =e;
+ } catch (IllegalBlockSizeException e) {
+ err =e;
+ } catch (BadPaddingException e) {
+ err =e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getLocalizedMessage());
+ }
+ }
+
}
diff --git a/src/de/blinkt/openvpn/VpnProfile.java b/src/de/blinkt/openvpn/VpnProfile.java
index dd729a06..8b758b3b 100644
--- a/src/de/blinkt/openvpn/VpnProfile.java
+++ b/src/de/blinkt/openvpn/VpnProfile.java
@@ -4,25 +4,23 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
-import java.util.Random;
import java.util.UUID;
import java.util.Vector;
+import org.spongycastle.util.io.pem.PemObject;
+import org.spongycastle.util.io.pem.PemWriter;
+
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -51,8 +49,7 @@ public class VpnProfile implements Serializable{
protected transient String mTransientPW=null;
protected transient String mTransientPCKS12PW=null;
-
- private static transient String mTempPKCS12Password;
+ private transient PrivateKey mPrivateKey;
public static String DEFAULT_DNS1="131.234.137.23";
public static String DEFAULT_DNS2="131.234.137.24";
@@ -100,6 +97,7 @@ public class VpnProfile implements Serializable{
public boolean mUseDefaultRoutev6=true;
public String mCustomRoutesv6="";
public String mKeyPassword="";
+
public void clearDefaults() {
@@ -122,7 +120,8 @@ public class VpnProfile implements Serializable{
}
- static final String OVPNCONFIGPKCS12 = "android.pkcs12";
+ static final String OVPNCONFIGCA = "android-ca.pem";
+ static final String OVPNCONFIGUSERCERT = "android-user.pem";
public VpnProfile(String name) {
@@ -223,9 +222,10 @@ public class VpnProfile implements Serializable{
case VpnProfile.TYPE_USERPASS_KEYSTORE:
cfg+="auth-user-pass\n";
case VpnProfile.TYPE_KEYSTORE:
- cfg+="pkcs12 ";
- cfg+=cacheDir.getAbsolutePath() + "/" + OVPNCONFIGPKCS12;
- cfg+="\n";
+ cfg+="ca " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGCA + "\n";
+ cfg+="cert " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGUSERCERT + "\n";
+ cfg+="management-external-key\n";
+
break;
case VpnProfile.TYPE_USERPASS:
cfg+="auth-user-pass\n";
@@ -447,7 +447,7 @@ public class VpnProfile implements Serializable{
Intent intent = new Intent(context,OpenVpnService.class);
if(mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
- savePKCS12(context);
+ saveCertificates(context);
}
intent.putExtra(prefix + ".ARGV" , buildOpenvpnArgv(context.getCacheDir()));
@@ -468,27 +468,13 @@ public class VpnProfile implements Serializable{
return intent;
}
- private String getTemporaryPKCS12Password() {
- if(mTempPKCS12Password!=null)
- return mTempPKCS12Password;
-
- String pw= "";
- // Put enough digits togher to make a password :)
- Random r = new Random();
- for(int i=0;i < 4;i++) {
- pw += Integer.valueOf(r.nextInt(1000)).toString();
- }
-
- mTempPKCS12Password=pw;
- return mTempPKCS12Password;
-
- }
-
- private void savePKCS12(Context context) {
+ private void saveCertificates(Context context) {
PrivateKey privateKey = null;
X509Certificate[] cachain=null;
try {
privateKey = KeyChain.getPrivateKey(context,mAlias);
+ mPrivateKey = privateKey;
+
cachain = KeyChain.getCertificateChain(context, mAlias);
if(cachain.length <= 1 && !nonNull(mCaFilename))
OpenVPN.logMessage(0, "", context.getString(R.string.keychain_nocacert));
@@ -496,32 +482,50 @@ public class VpnProfile implements Serializable{
for(X509Certificate cert:cachain) {
OpenVPN.logInfo(R.string.cert_from_keystore,cert.getSubjectDN());
}
+
+
- KeyStore ks = KeyStore.getInstance("PKCS12");
- ks.load(null, null);
+
if(nonNull(mCaFilename)) {
try {
- Certificate cacert = getCacertFromFile();
-
- ks.setCertificateEntry("cacert", cacert);
+ Certificate cacert = getCacertFromFile();
+ X509Certificate[] newcachain = new X509Certificate[cachain.length+1];
+ for(int i=0;i<cachain.length;i++)
+ newcachain[i]=cachain[i];
+
+ newcachain[cachain.length-1]=(X509Certificate) cacert;
+
} catch (Exception e) {
OpenVPN.logError("Could not read CA certificate" + e.getLocalizedMessage());
}
}
- ks.setKeyEntry("usercert", privateKey, null, cachain);
- String mypw = getTemporaryPKCS12Password();
- FileOutputStream fout = new FileOutputStream(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGPKCS12);
- ks.store(fout,mypw.toCharArray());
- fout.flush(); fout.close();
+
+
+ FileWriter fout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGCA);
+ PemWriter pw = new PemWriter(fout);
+ for(X509Certificate cert:cachain) {
+ pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
+ }
+
+ pw.close();
+
+
+ if(cachain.length>= 1){
+ X509Certificate usercert = cachain[0];
+
+ FileWriter userout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGUSERCERT);
+
+ PemWriter upw = new PemWriter(userout);
+ upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded()));
+ upw.close();
+
+ }
+
return;
} catch (InterruptedException e) {
e.printStackTrace();
- } catch (KeyStoreException e) {
- e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
@@ -574,10 +578,6 @@ public class VpnProfile implements Serializable{
return pwcopy;
}
switch (mAuthenticationType) {
- case TYPE_KEYSTORE:
- case TYPE_USERPASS_KEYSTORE:
- return getTemporaryPKCS12Password();
-
case TYPE_PKCS12:
case TYPE_USERPASS_PKCS12:
return mPKCS12Password;
@@ -623,6 +623,7 @@ public class VpnProfile implements Serializable{
data += new String(buf,0,len);
len = fr.read(buf);
}
+ fr.close();
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) {
@@ -684,6 +685,11 @@ public class VpnProfile implements Serializable{
}
+ public PrivateKey getKeystoreKey() {
+ return mPrivateKey;
+ }
+
+
}
diff --git a/src/org/spongycastle/util/encoders/Base64.java b/src/org/spongycastle/util/encoders/Base64.java
new file mode 100644
index 00000000..87bd80a0
--- /dev/null
+++ b/src/org/spongycastle/util/encoders/Base64.java
@@ -0,0 +1,121 @@
+package org.spongycastle.util.encoders;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class Base64
+{
+ private static final Encoder encoder = new Base64Encoder();
+
+ /**
+ * encode the input data producing a base 64 encoded byte array.
+ *
+ * @return a byte array containing the base 64 encoded data.
+ */
+ public static byte[] encode(
+ byte[] data)
+ {
+ int len = (data.length + 2) / 3 * 4;
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream(len);
+
+ try
+ {
+ encoder.encode(data, 0, data.length, bOut);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("exception encoding base64 string: " + e);
+ }
+
+ return bOut.toByteArray();
+ }
+
+ /**
+ * Encode the byte data to base 64 writing it to the given output stream.
+ *
+ * @return the number of bytes produced.
+ */
+ public static int encode(
+ byte[] data,
+ OutputStream out)
+ throws IOException
+ {
+ return encoder.encode(data, 0, data.length, out);
+ }
+
+ /**
+ * Encode the byte data to base 64 writing it to the given output stream.
+ *
+ * @return the number of bytes produced.
+ */
+ public static int encode(
+ byte[] data,
+ int off,
+ int length,
+ OutputStream out)
+ throws IOException
+ {
+ return encoder.encode(data, off, length, out);
+ }
+
+ /**
+ * decode the base 64 encoded input data. It is assumed the input data is valid.
+ *
+ * @return a byte array representing the decoded data.
+ */
+ public static byte[] decode(
+ byte[] data)
+ {
+ int len = data.length / 4 * 3;
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream(len);
+
+ try
+ {
+ encoder.decode(data, 0, data.length, bOut);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("exception decoding base64 string: " + e);
+ }
+
+ return bOut.toByteArray();
+ }
+
+ /**
+ * decode the base 64 encoded String data - whitespace will be ignored.
+ *
+ * @return a byte array representing the decoded data.
+ */
+ public static byte[] decode(
+ String data)
+ {
+ int len = data.length() / 4 * 3;
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream(len);
+
+ try
+ {
+ encoder.decode(data, bOut);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("exception decoding base64 string: " + e);
+ }
+
+ return bOut.toByteArray();
+ }
+
+ /**
+ * decode the base 64 encoded String data writing it to the given output stream,
+ * whitespace characters will be ignored.
+ *
+ * @return the number of bytes produced.
+ */
+ public static int decode(
+ String data,
+ OutputStream out)
+ throws IOException
+ {
+ return encoder.decode(data, out);
+ }
+}
diff --git a/src/org/spongycastle/util/encoders/Base64Encoder.java b/src/org/spongycastle/util/encoders/Base64Encoder.java
new file mode 100644
index 00000000..84060707
--- /dev/null
+++ b/src/org/spongycastle/util/encoders/Base64Encoder.java
@@ -0,0 +1,298 @@
+package org.spongycastle.util.encoders;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class Base64Encoder
+ implements Encoder
+{
+ protected final byte[] encodingTable =
+ {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v',
+ (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6',
+ (byte)'7', (byte)'8', (byte)'9',
+ (byte)'+', (byte)'/'
+ };
+
+ protected byte padding = (byte)'=';
+
+ /*
+ * set up the decoding table.
+ */
+ protected final byte[] decodingTable = new byte[128];
+
+ protected void initialiseDecodingTable()
+ {
+ for (int i = 0; i < encodingTable.length; i++)
+ {
+ decodingTable[encodingTable[i]] = (byte)i;
+ }
+ }
+
+ public Base64Encoder()
+ {
+ initialiseDecodingTable();
+ }
+
+ /**
+ * encode the input data producing a base 64 output stream.
+ *
+ * @return the number of bytes produced.
+ */
+ public int encode(
+ byte[] data,
+ int off,
+ int length,
+ OutputStream out)
+ throws IOException
+ {
+ int modulus = length % 3;
+ int dataLength = (length - modulus);
+ int a1, a2, a3;
+
+ for (int i = off; i < off + dataLength; i += 3)
+ {
+ a1 = data[i] & 0xff;
+ a2 = data[i + 1] & 0xff;
+ a3 = data[i + 2] & 0xff;
+
+ out.write(encodingTable[(a1 >>> 2) & 0x3f]);
+ out.write(encodingTable[((a1 << 4) | (a2 >>> 4)) & 0x3f]);
+ out.write(encodingTable[((a2 << 2) | (a3 >>> 6)) & 0x3f]);
+ out.write(encodingTable[a3 & 0x3f]);
+ }
+
+ /*
+ * process the tail end.
+ */
+ int b1, b2, b3;
+ int d1, d2;
+
+ switch (modulus)
+ {
+ case 0: /* nothing left to do */
+ break;
+ case 1:
+ d1 = data[off + dataLength] & 0xff;
+ b1 = (d1 >>> 2) & 0x3f;
+ b2 = (d1 << 4) & 0x3f;
+
+ out.write(encodingTable[b1]);
+ out.write(encodingTable[b2]);
+ out.write(padding);
+ out.write(padding);
+ break;
+ case 2:
+ d1 = data[off + dataLength] & 0xff;
+ d2 = data[off + dataLength + 1] & 0xff;
+
+ b1 = (d1 >>> 2) & 0x3f;
+ b2 = ((d1 << 4) | (d2 >>> 4)) & 0x3f;
+ b3 = (d2 << 2) & 0x3f;
+
+ out.write(encodingTable[b1]);
+ out.write(encodingTable[b2]);
+ out.write(encodingTable[b3]);
+ out.write(padding);
+ break;
+ }
+
+ return (dataLength / 3) * 4 + ((modulus == 0) ? 0 : 4);
+ }
+
+ private boolean ignore(
+ char c)
+ {
+ return (c == '\n' || c =='\r' || c == '\t' || c == ' ');
+ }
+
+ /**
+ * decode the base 64 encoded byte data writing it to the given output stream,
+ * whitespace characters will be ignored.
+ *
+ * @return the number of bytes produced.
+ */
+ public int decode(
+ byte[] data,
+ int off,
+ int length,
+ OutputStream out)
+ throws IOException
+ {
+ byte b1, b2, b3, b4;
+ int outLen = 0;
+
+ int end = off + length;
+
+ while (end > off)
+ {
+ if (!ignore((char)data[end - 1]))
+ {
+ break;
+ }
+
+ end--;
+ }
+
+ int i = off;
+ int finish = end - 4;
+
+ i = nextI(data, i, finish);
+
+ while (i < finish)
+ {
+ b1 = decodingTable[data[i++]];
+
+ i = nextI(data, i, finish);
+
+ b2 = decodingTable[data[i++]];
+
+ i = nextI(data, i, finish);
+
+ b3 = decodingTable[data[i++]];
+
+ i = nextI(data, i, finish);
+
+ b4 = decodingTable[data[i++]];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+ out.write((b3 << 6) | b4);
+
+ outLen += 3;
+
+ i = nextI(data, i, finish);
+ }
+
+ outLen += decodeLastBlock(out, (char)data[end - 4], (char)data[end - 3], (char)data[end - 2], (char)data[end - 1]);
+
+ return outLen;
+ }
+
+ private int nextI(byte[] data, int i, int finish)
+ {
+ while ((i < finish) && ignore((char)data[i]))
+ {
+ i++;
+ }
+ return i;
+ }
+
+ /**
+ * decode the base 64 encoded String data writing it to the given output stream,
+ * whitespace characters will be ignored.
+ *
+ * @return the number of bytes produced.
+ */
+ public int decode(
+ String data,
+ OutputStream out)
+ throws IOException
+ {
+ byte b1, b2, b3, b4;
+ int length = 0;
+
+ int end = data.length();
+
+ while (end > 0)
+ {
+ if (!ignore(data.charAt(end - 1)))
+ {
+ break;
+ }
+
+ end--;
+ }
+
+ int i = 0;
+ int finish = end - 4;
+
+ i = nextI(data, i, finish);
+
+ while (i < finish)
+ {
+ b1 = decodingTable[data.charAt(i++)];
+
+ i = nextI(data, i, finish);
+
+ b2 = decodingTable[data.charAt(i++)];
+
+ i = nextI(data, i, finish);
+
+ b3 = decodingTable[data.charAt(i++)];
+
+ i = nextI(data, i, finish);
+
+ b4 = decodingTable[data.charAt(i++)];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+ out.write((b3 << 6) | b4);
+
+ length += 3;
+
+ i = nextI(data, i, finish);
+ }
+
+ length += decodeLastBlock(out, data.charAt(end - 4), data.charAt(end - 3), data.charAt(end - 2), data.charAt(end - 1));
+
+ return length;
+ }
+
+ private int decodeLastBlock(OutputStream out, char c1, char c2, char c3, char c4)
+ throws IOException
+ {
+ byte b1, b2, b3, b4;
+
+ if (c3 == padding)
+ {
+ b1 = decodingTable[c1];
+ b2 = decodingTable[c2];
+
+ out.write((b1 << 2) | (b2 >> 4));
+
+ return 1;
+ }
+ else if (c4 == padding)
+ {
+ b1 = decodingTable[c1];
+ b2 = decodingTable[c2];
+ b3 = decodingTable[c3];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+
+ return 2;
+ }
+ else
+ {
+ b1 = decodingTable[c1];
+ b2 = decodingTable[c2];
+ b3 = decodingTable[c3];
+ b4 = decodingTable[c4];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+ out.write((b3 << 6) | b4);
+
+ return 3;
+ }
+ }
+
+ private int nextI(String data, int i, int finish)
+ {
+ while ((i < finish) && ignore(data.charAt(i)))
+ {
+ i++;
+ }
+ return i;
+ }
+}
diff --git a/src/org/spongycastle/util/encoders/Encoder.java b/src/org/spongycastle/util/encoders/Encoder.java
new file mode 100644
index 00000000..106c36b7
--- /dev/null
+++ b/src/org/spongycastle/util/encoders/Encoder.java
@@ -0,0 +1,17 @@
+package org.spongycastle.util.encoders;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Encode and decode byte arrays (typically from binary to 7-bit ASCII
+ * encodings).
+ */
+public interface Encoder
+{
+ int encode(byte[] data, int off, int length, OutputStream out) throws IOException;
+
+ int decode(byte[] data, int off, int length, OutputStream out) throws IOException;
+
+ int decode(String data, OutputStream out) throws IOException;
+}
diff --git a/src/org/spongycastle/util/io/pem/PemGenerationException.java b/src/org/spongycastle/util/io/pem/PemGenerationException.java
new file mode 100644
index 00000000..0127ca0c
--- /dev/null
+++ b/src/org/spongycastle/util/io/pem/PemGenerationException.java
@@ -0,0 +1,26 @@
+package org.spongycastle.util.io.pem;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+public class PemGenerationException
+ extends IOException
+{
+ private Throwable cause;
+
+ public PemGenerationException(String message, Throwable cause)
+ {
+ super(message);
+ this.cause = cause;
+ }
+
+ public PemGenerationException(String message)
+ {
+ super(message);
+ }
+
+ public Throwable getCause()
+ {
+ return cause;
+ }
+}
diff --git a/src/org/spongycastle/util/io/pem/PemHeader.java b/src/org/spongycastle/util/io/pem/PemHeader.java
new file mode 100644
index 00000000..4adb815e
--- /dev/null
+++ b/src/org/spongycastle/util/io/pem/PemHeader.java
@@ -0,0 +1,66 @@
+package org.spongycastle.util.io.pem;
+
+public class PemHeader
+{
+ private String name;
+ private String value;
+
+ public PemHeader(String name, String value)
+ {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+
+ public int hashCode()
+ {
+ return getHashCode(this.name) + 31 * getHashCode(this.value);
+ }
+
+ public boolean equals(Object o)
+ {
+ if (!(o instanceof PemHeader))
+ {
+ return false;
+ }
+
+ PemHeader other = (PemHeader)o;
+
+ return other == this || (isEqual(this.name, other.name) && isEqual(this.value, other.value));
+ }
+
+ private int getHashCode(String s)
+ {
+ if (s == null)
+ {
+ return 1;
+ }
+
+ return s.hashCode();
+ }
+
+ private boolean isEqual(String s1, String s2)
+ {
+ if (s1 == s2)
+ {
+ return true;
+ }
+
+ if (s1 == null || s2 == null)
+ {
+ return false;
+ }
+
+ return s1.equals(s2);
+ }
+
+}
diff --git a/src/org/spongycastle/util/io/pem/PemObject.java b/src/org/spongycastle/util/io/pem/PemObject.java
new file mode 100644
index 00000000..6f7c79c5
--- /dev/null
+++ b/src/org/spongycastle/util/io/pem/PemObject.java
@@ -0,0 +1,62 @@
+package org.spongycastle.util.io.pem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@SuppressWarnings("all")
+public class PemObject
+ implements PemObjectGenerator
+{
+ private static final List EMPTY_LIST = Collections.unmodifiableList(new ArrayList());
+
+ private String type;
+ private List headers;
+ private byte[] content;
+
+ /**
+ * Generic constructor for object without headers.
+ *
+ * @param type pem object type.
+ * @param content the binary content of the object.
+ */
+ public PemObject(String type, byte[] content)
+ {
+ this(type, EMPTY_LIST, content);
+ }
+
+ /**
+ * Generic constructor for object with headers.
+ *
+ * @param type pem object type.
+ * @param headers a list of PemHeader objects.
+ * @param content the binary content of the object.
+ */
+ public PemObject(String type, List headers, byte[] content)
+ {
+ this.type = type;
+ this.headers = Collections.unmodifiableList(headers);
+ this.content = content;
+ }
+
+ public String getType()
+ {
+ return type;
+ }
+
+ public List getHeaders()
+ {
+ return headers;
+ }
+
+ public byte[] getContent()
+ {
+ return content;
+ }
+
+ public PemObject generate()
+ throws PemGenerationException
+ {
+ return this;
+ }
+}
diff --git a/src/org/spongycastle/util/io/pem/PemObjectGenerator.java b/src/org/spongycastle/util/io/pem/PemObjectGenerator.java
new file mode 100644
index 00000000..1a8cea6d
--- /dev/null
+++ b/src/org/spongycastle/util/io/pem/PemObjectGenerator.java
@@ -0,0 +1,7 @@
+package org.spongycastle.util.io.pem;
+
+public interface PemObjectGenerator
+{
+ PemObject generate()
+ throws PemGenerationException;
+}
diff --git a/src/org/spongycastle/util/io/pem/PemWriter.java b/src/org/spongycastle/util/io/pem/PemWriter.java
new file mode 100644
index 00000000..f5a6a363
--- /dev/null
+++ b/src/org/spongycastle/util/io/pem/PemWriter.java
@@ -0,0 +1,138 @@
+package org.spongycastle.util.io.pem;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+
+import org.spongycastle.util.encoders.Base64;
+
+/**
+ * A generic PEM writer, based on RFC 1421
+ */
+@SuppressWarnings("all")
+public class PemWriter
+ extends BufferedWriter
+{
+ private static final int LINE_LENGTH = 64;
+
+ private final int nlLength;
+ private char[] buf = new char[LINE_LENGTH];
+
+ /**
+ * Base constructor.
+ *
+ * @param out output stream to use.
+ */
+ public PemWriter(Writer out)
+ {
+ super(out);
+
+ String nl = System.getProperty("line.separator");
+ if (nl != null)
+ {
+ nlLength = nl.length();
+ }
+ else
+ {
+ nlLength = 2;
+ }
+ }
+
+ /**
+ * Return the number of bytes or characters required to contain the
+ * passed in object if it is PEM encoded.
+ *
+ * @param obj pem object to be output
+ * @return an estimate of the number of bytes
+ */
+ public int getOutputSize(PemObject obj)
+ {
+ // BEGIN and END boundaries.
+ int size = (2 * (obj.getType().length() + 10 + nlLength)) + 6 + 4;
+
+ if (!obj.getHeaders().isEmpty())
+ {
+ for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
+ {
+ PemHeader hdr = (PemHeader)it.next();
+
+ size += hdr.getName().length() + ": ".length() + hdr.getValue().length() + nlLength;
+ }
+
+ size += nlLength;
+ }
+
+ // base64 encoding
+ int dataLen = ((obj.getContent().length + 2) / 3) * 4;
+
+ size += dataLen + (((dataLen + LINE_LENGTH - 1) / LINE_LENGTH) * nlLength);
+
+ return size;
+ }
+
+ public void writeObject(PemObjectGenerator objGen)
+ throws IOException
+ {
+ PemObject obj = objGen.generate();
+
+ writePreEncapsulationBoundary(obj.getType());
+
+ if (!obj.getHeaders().isEmpty())
+ {
+ for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
+ {
+ PemHeader hdr = (PemHeader)it.next();
+
+ this.write(hdr.getName());
+ this.write(": ");
+ this.write(hdr.getValue());
+ this.newLine();
+ }
+
+ this.newLine();
+ }
+
+ writeEncoded(obj.getContent());
+ writePostEncapsulationBoundary(obj.getType());
+ }
+
+ private void writeEncoded(byte[] bytes)
+ throws IOException
+ {
+ bytes = Base64.encode(bytes);
+
+ for (int i = 0; i < bytes.length; i += buf.length)
+ {
+ int index = 0;
+
+ while (index != buf.length)
+ {
+ if ((i + index) >= bytes.length)
+ {
+ break;
+ }
+ buf[index] = (char)bytes[i + index];
+ index++;
+ }
+ this.write(buf, 0, index);
+ this.newLine();
+ }
+ }
+
+ private void writePreEncapsulationBoundary(
+ String type)
+ throws IOException
+ {
+ this.write("-----BEGIN " + type + "-----");
+ this.newLine();
+ }
+
+ private void writePostEncapsulationBoundary(
+ String type)
+ throws IOException
+ {
+ this.write("-----END " + type + "-----");
+ this.newLine();
+ }
+}