diff options
author | Kenny Root <kenny@the-b.org> | 2008-12-26 17:28:45 +0000 |
---|---|---|
committer | Kenny Root <kenny@the-b.org> | 2008-12-26 17:28:45 +0000 |
commit | 964a944602bc08bde825bf43ac1ffa4d42a17e51 (patch) | |
tree | 595e491c15ef1291e1019dff5bc5001b6bdaf35e /src/org | |
parent | 8d30c6c4a63bf6b1ed1fd37b390ff76bc280e2a6 (diff) | |
download | connectbot-964a944602bc08bde825bf43ac1ffa4d42a17e51.tar.gz connectbot-964a944602bc08bde825bf43ac1ffa4d42a17e51.tar.bz2 connectbot-964a944602bc08bde825bf43ac1ffa4d42a17e51.zip |
Allow unencrypted export of private keys in PKCS#8 format
Diffstat (limited to 'src/org')
-rw-r--r-- | src/org/connectbot/PubkeyListActivity.java | 66 | ||||
-rw-r--r-- | src/org/connectbot/bean/AbstractBean.java | 5 | ||||
-rw-r--r-- | src/org/connectbot/bean/HostBean.java | 6 | ||||
-rw-r--r-- | src/org/connectbot/bean/PortForwardBean.java | 6 | ||||
-rw-r--r-- | src/org/connectbot/bean/PubkeyBean.java | 7 | ||||
-rw-r--r-- | src/org/connectbot/util/PubkeyUtils.java | 115 |
6 files changed, 192 insertions, 13 deletions
diff --git a/src/org/connectbot/PubkeyListActivity.java b/src/org/connectbot/PubkeyListActivity.java index 1728f31..4a1b3de 100644 --- a/src/org/connectbot/PubkeyListActivity.java +++ b/src/org/connectbot/PubkeyListActivity.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Collections; @@ -66,6 +67,7 @@ import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.OnItemClickListener; +import com.trilead.ssh2.crypto.Base64; import com.trilead.ssh2.crypto.PEMDecoder; import com.trilead.ssh2.crypto.PEMStructure; @@ -230,27 +232,49 @@ public class PubkeyListActivity extends ListActivity implements EventListener { .setTitle(R.string.pubkey_list_pick) .setItems(namesList, new OnClickListener() { public void onClick(DialogInterface arg0, int arg1) { + PubkeyBean pubkey = new PubkeyBean(); + // find the exact file selected String name = namesList[arg1]; + pubkey.setNickname(name); File actual = new File(sdcard, name); // parse the actual key once to check if its encrypted // then save original file contents into our database try { byte[] raw = readRaw(actual); - PEMStructure struct = PEMDecoder.parsePEM(new String(raw).toCharArray()); - boolean encrypted = PEMDecoder.isPEMEncrypted(struct); - // write new value into database - PubkeyBean pubkey = new PubkeyBean(); - pubkey.setNickname(name); - pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED); - pubkey.setPrivateKey(raw); - pubkey.setEncrypted(encrypted); + String data = new String(raw); + if (data.startsWith(PubkeyUtils.PKCS8_START)) { + int start = data.indexOf(PubkeyUtils.PKCS8_START) + PubkeyUtils.PKCS8_START.length(); + int end = data.indexOf(PubkeyUtils.PKCS8_END); + + if (end > start) { + char[] encoded = data.substring(start, end - 1).toCharArray(); + Log.d(TAG, "encoded: " + new String(encoded)); + byte[] decoded = Base64.decode(encoded); + + KeyPair kp = PubkeyUtils.recoverKeyPair(decoded); + + pubkey.setType(kp.getPrivate().getAlgorithm()); + pubkey.setPrivateKey(kp.getPrivate().getEncoded()); + pubkey.setPublicKey(kp.getPublic().getEncoded()); + } else { + Log.e(TAG, "Problem parsing PKCS#8 file; corrupt?"); + Toast.makeText(PubkeyListActivity.this, + R.string.pubkey_import_parse_problem, + Toast.LENGTH_LONG).show(); + } + } else { + PEMStructure struct = PEMDecoder.parsePEM(new String(raw).toCharArray()); + pubkey.setEncrypted(PEMDecoder.isPEMEncrypted(struct)); + pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED); + pubkey.setPrivateKey(raw); + } + // write new value into database pubkeydb.savePubkey(pubkey); updateHandler.sendEmptyMessage(-1); - } catch(Exception e) { Log.e(TAG, "Problem parsing imported private key", e); Toast.makeText(PubkeyListActivity.this, R.string.pubkey_import_parse_problem, Toast.LENGTH_LONG).show(); @@ -338,7 +362,7 @@ public class PubkeyListActivity extends ListActivity implements EventListener { // prompt for password as needed for passworded keys // cant change password or clipboard imported keys - boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); + final boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); final boolean loaded = bound.isKeyLoaded(pubkey.getNickname()); MenuItem load = menu.add(loaded ? R.string.pubkey_memory_unload : R.string.pubkey_memory_load); @@ -385,6 +409,28 @@ public class PubkeyListActivity extends ListActivity implements EventListener { } }); + MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private); + copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported); + copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + String data = null; + + if (imported) + data = new String(pubkey.getPrivateKey()); + else { + PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType()); + data = new String(PubkeyUtils.exportPEM(pk, null)); + } + + clipboard.setText(data); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + }); + MenuItem changePassword = menu.add(R.string.pubkey_change_password); changePassword.setEnabled(!imported); changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() { diff --git a/src/org/connectbot/bean/AbstractBean.java b/src/org/connectbot/bean/AbstractBean.java index 13dd285..4998727 100644 --- a/src/org/connectbot/bean/AbstractBean.java +++ b/src/org/connectbot/bean/AbstractBean.java @@ -29,11 +29,12 @@ import android.content.ContentValues; */ abstract class AbstractBean { public abstract ContentValues getValues(); + public abstract String getBeanName(); public String toXML() { XmlBuilder xml = new XmlBuilder(); - xml.append("<host>"); + xml.append(String.format("<%s>", getBeanName())); ContentValues values = getValues(); for (Entry<String, Object> entry : values.valueSet()) { @@ -41,7 +42,7 @@ abstract class AbstractBean { if (value != null) xml.append(entry.getKey(), value); } - xml.append("</host>"); + xml.append(String.format("</%s>", getBeanName())); return xml.toString(); } diff --git a/src/org/connectbot/bean/HostBean.java b/src/org/connectbot/bean/HostBean.java index 4ab219b..1b937c9 100644 --- a/src/org/connectbot/bean/HostBean.java +++ b/src/org/connectbot/bean/HostBean.java @@ -26,6 +26,8 @@ import android.content.ContentValues; * */ public class HostBean extends AbstractBean { + public static final String BEAN_NAME = "host"; + /* Database fields */ private long id = -1; private String nickname = null; @@ -47,6 +49,10 @@ public class HostBean extends AbstractBean { } + public String getBeanName() { + return BEAN_NAME; + } + public HostBean(String nickname, String username, String hostname, int port) { this.nickname = nickname; this.username = username; diff --git a/src/org/connectbot/bean/PortForwardBean.java b/src/org/connectbot/bean/PortForwardBean.java index ecf9c44..9a2036f 100644 --- a/src/org/connectbot/bean/PortForwardBean.java +++ b/src/org/connectbot/bean/PortForwardBean.java @@ -27,6 +27,8 @@ import android.content.ContentValues; * */ public class PortForwardBean extends AbstractBean { + public static final String BEAN_NAME = "portforward"; + /* Database fields */ private long id = -1; private long hostId = -1; @@ -72,6 +74,10 @@ public class PortForwardBean extends AbstractBean { this.setDest(dest); } + public String getBeanName() { + return BEAN_NAME; + } + /** * @param id the id to set */ diff --git a/src/org/connectbot/bean/PubkeyBean.java b/src/org/connectbot/bean/PubkeyBean.java index b3d3108..bb9f3c0 100644 --- a/src/org/connectbot/bean/PubkeyBean.java +++ b/src/org/connectbot/bean/PubkeyBean.java @@ -36,6 +36,8 @@ import android.content.ContentValues; * */ public class PubkeyBean extends AbstractBean { + public static final String BEAN_NAME = "pubkey"; + /* Database fields */ private long id; private String nickname; @@ -49,6 +51,10 @@ public class PubkeyBean extends AbstractBean { private boolean unlocked = false; private Object unlockedPrivate = null; + public String getBeanName() { + return BEAN_NAME; + } + public void setId(long id) { this.id = id; } @@ -128,7 +134,6 @@ public class PubkeyBean extends AbstractBean { public ContentValues getValues() { ContentValues values = new ContentValues(); - values.put("_id", id); values.put(PubkeyDatabase.FIELD_PUBKEY_NICKNAME, nickname); values.put(PubkeyDatabase.FIELD_PUBKEY_TYPE, type); values.put(PubkeyDatabase.FIELD_PUBKEY_PRIVATE, privateKey); diff --git a/src/org/connectbot/util/PubkeyUtils.java b/src/org/connectbot/util/PubkeyUtils.java index 63e96c3..0ab667c 100644 --- a/src/org/connectbot/util/PubkeyUtils.java +++ b/src/org/connectbot/util/PubkeyUtils.java @@ -19,26 +19,40 @@ package org.connectbot.util; import java.io.IOException; +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; +import java.security.KeyPair; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.SecureRandom; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPublicKeySpec; import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; import javax.crypto.spec.SecretKeySpec; import com.trilead.ssh2.crypto.Base64; @@ -46,6 +60,9 @@ import com.trilead.ssh2.signature.DSASHA1Verify; import com.trilead.ssh2.signature.RSASHA1Verify; public class PubkeyUtils { + public static final String PKCS8_START = "-----BEGIN PRIVATE KEY-----"; + public static final String PKCS8_END = "-----END PRIVATE KEY-----"; + public static String formatKey(Key key){ String algo = key.getAlgorithm(); String fmt = key.getFormat(); @@ -124,6 +141,41 @@ public class PubkeyUtils { return kf.generatePublic(pubKeySpec); } + public static KeyPair recoverKeyPair(byte[] encoded) throws NoSuchAlgorithmException, InvalidKeySpecException { + KeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded); + KeySpec pubKeySpec; + + PrivateKey priv; + PublicKey pub; + KeyFactory kf; + try { + kf = KeyFactory.getInstance(PubkeyDatabase.KEY_TYPE_RSA); + priv = kf.generatePrivate(privKeySpec); + + pubKeySpec = new RSAPublicKeySpec(((RSAPrivateCrtKey) priv) + .getModulus(), ((RSAPrivateCrtKey) priv) + .getPublicExponent()); + + pub = kf.generatePublic(pubKeySpec); + } catch (ClassCastException e) { + kf = KeyFactory.getInstance(PubkeyDatabase.KEY_TYPE_DSA); + priv = kf.generatePrivate(privKeySpec); + + DSAParams params = ((DSAPrivateKey) priv).getParams(); + + // Calculate public key Y + BigInteger y = params.getG().modPow(((DSAPrivateKey) priv).getX(), + params.getP()); + + pubKeySpec = new DSAPublicKeySpec(y, params.getP(), params.getQ(), + params.getG()); + + pub = kf.generatePublic(pubKeySpec); + } + + return new KeyPair(pub, priv); + } + /* * Trilead compatibility methods */ @@ -180,4 +232,67 @@ public class PubkeyUtils { throw new InvalidKeyException("Unknown key type"); } + + public static String exportPEM(PrivateKey key, String secret) throws NoSuchAlgorithmException, InvalidParameterSpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, InvalidKeySpecException, IllegalBlockSizeException, IOException { + StringBuilder sb = new StringBuilder(); + + byte[] data = key.getEncoded(); + + sb.append(PKCS8_START); + sb.append('\n'); + + if (secret != null) { + byte[] salt = new byte[8]; + SecureRandom random = new SecureRandom(); + random.nextBytes(salt); + + PBEParameterSpec defParams = new PBEParameterSpec(salt, 1); + AlgorithmParameters params = AlgorithmParameters.getInstance(key.getAlgorithm()); + + params.init(defParams); + + PBEKeySpec pbeSpec = new PBEKeySpec(secret.toCharArray()); + + SecretKeyFactory keyFact = SecretKeyFactory.getInstance(key.getAlgorithm()); + Cipher cipher = Cipher.getInstance(key.getAlgorithm()); + cipher.init(Cipher.WRAP_MODE, keyFact.generateSecret(pbeSpec), params); + + byte[] wrappedKey = cipher.wrap(key); + + EncryptedPrivateKeyInfo pinfo = new EncryptedPrivateKeyInfo(params, wrappedKey); + + data = pinfo.getEncoded(); + + sb.append("Proc-Type: 4,ENCRYPTED\n"); + sb.append("DEK-Info: DES-EDE3-CBC,"); + sb.append(encodeHex(salt)); + sb.append("\n\n"); + } + + int i = sb.length(); + sb.append(Base64.encode(data)); + for (i += 63; i < sb.length(); i += 64) { + sb.insert(i, "\n"); + } + + sb.append('\n'); + sb.append(PKCS8_END); + sb.append('\n'); + + return sb.toString(); + } + + final static private char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static String encodeHex(byte[] bytes) { + char[] hex = new char[bytes.length * 2]; + + int i = 0; + for (byte b : bytes) { + hex[i++] = hexDigit[(b >> 4) & 0x0f]; + hex[i++] = hexDigit[b & 0x0f]; + } + + return new String(hex); + } } |