From 78172a10165a969b8c002b6bdcf9dc47fa6cd5f3 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Thu, 28 Jun 2012 19:33:05 +0200 Subject: 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 --- openvpn/src/openvpn/options.c | 4 + res/values/strings.xml | 1 + src/de/blinkt/openvpn/ConfigConverter.java | 1 + src/de/blinkt/openvpn/FileSelect.java | 2 +- src/de/blinkt/openvpn/OpenVPN.java | 6 +- src/de/blinkt/openvpn/OpenVpnManagementThread.java | 40 +++ src/de/blinkt/openvpn/VpnProfile.java | 100 +++---- src/org/spongycastle/util/encoders/Base64.java | 121 +++++++++ .../spongycastle/util/encoders/Base64Encoder.java | 298 +++++++++++++++++++++ src/org/spongycastle/util/encoders/Encoder.java | 17 ++ .../util/io/pem/PemGenerationException.java | 26 ++ src/org/spongycastle/util/io/pem/PemHeader.java | 66 +++++ src/org/spongycastle/util/io/pem/PemObject.java | 62 +++++ .../util/io/pem/PemObjectGenerator.java | 7 + src/org/spongycastle/util/io/pem/PemWriter.java | 138 ++++++++++ 15 files changed, 839 insertions(+), 50 deletions(-) create mode 100644 src/org/spongycastle/util/encoders/Base64.java create mode 100644 src/org/spongycastle/util/encoders/Base64Encoder.java create mode 100644 src/org/spongycastle/util/encoders/Encoder.java create mode 100644 src/org/spongycastle/util/io/pem/PemGenerationException.java create mode 100644 src/org/spongycastle/util/io/pem/PemHeader.java create mode 100644 src/org/spongycastle/util/io/pem/PemObject.java create mode 100644 src/org/spongycastle/util/io/pem/PemObjectGenerator.java create mode 100644 src/org/spongycastle/util/io/pem/PemWriter.java 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 @@ Keep the notification displayed after the connection is established to show traffic statistics. Show Traffic Statistics Running on %1$s (%2$s) %3$s, Android API %4$d + Error signing with Android keystore key %s 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= 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(); + } +} -- cgit v1.2.3