diff options
author | Kenny Root <kenny@the-b.org> | 2008-10-29 05:01:19 +0000 |
---|---|---|
committer | Kenny Root <kenny@the-b.org> | 2008-10-29 05:01:19 +0000 |
commit | dc59e640790d70a4542b37e3718dbccbd6be0d08 (patch) | |
tree | 22e0b3112753ddafe4831f41575d37e7689f4d43 /src | |
parent | 540c693d2c0fdd10426468de6b66982d02a3ec91 (diff) | |
download | connectbot-dc59e640790d70a4542b37e3718dbccbd6be0d08.tar.gz connectbot-dc59e640790d70a4542b37e3718dbccbd6be0d08.tar.bz2 connectbot-dc59e640790d70a4542b37e3718dbccbd6be0d08.zip |
* First pass at publickey authentication.
* RSA and DSA keys can be generated (not imported yet).
* RSA and DSA keys can be copied to the clipboard and deleted.
* Encrypted keys are not tried right now, only unencrypted.
* Restore Marcus's name (Jeffrey, fix your editor!)
* Fix a typo in the EULA.
Diffstat (limited to 'src')
-rw-r--r-- | src/com/trilead/ssh2/Connection.java | 52 | ||||
-rw-r--r-- | src/de/mud/terminal/VDUBuffer.java | 4 | ||||
-rw-r--r-- | src/de/mud/terminal/VDUInput.java | 2 | ||||
-rw-r--r-- | src/org/connectbot/GeneratePubkeyActivity.java | 284 | ||||
-rw-r--r-- | src/org/connectbot/HostListActivity.java | 6 | ||||
-rw-r--r-- | src/org/connectbot/PubkeyListActivity.java | 226 | ||||
-rw-r--r-- | src/org/connectbot/service/TerminalBridge.java | 57 | ||||
-rw-r--r-- | src/org/connectbot/util/EntropyDialog.java | 50 | ||||
-rw-r--r-- | src/org/connectbot/util/EntropyView.java | 144 | ||||
-rw-r--r-- | src/org/connectbot/util/OnEntropyGatheredListener.java | 23 | ||||
-rw-r--r-- | src/org/connectbot/util/PubkeyDatabase.java | 110 | ||||
-rw-r--r-- | src/org/connectbot/util/PubkeyUtils.java | 194 |
12 files changed, 1138 insertions, 14 deletions
diff --git a/src/com/trilead/ssh2/Connection.java b/src/com/trilead/ssh2/Connection.java index 38b96c5..8fbf6fa 100644 --- a/src/com/trilead/ssh2/Connection.java +++ b/src/com/trilead/ssh2/Connection.java @@ -444,7 +444,59 @@ public class Connection return authenticated;
}
+
+ /**
+ * After a successful connect, one has to authenticate oneself. The
+ * authentication method "publickey" works by signing a challenge sent by
+ * the server. The signature is either DSA or RSA based - it just depends on
+ * the type of private key you specify, either a DSA or RSA private key in
+ * PEM format. And yes, this is may seem to be a little confusing, the
+ * method is called "publickey" in the SSH-2 protocol specification, however
+ * since we need to generate a signature, you actually have to supply a
+ * private key =).
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If the server does not accept the request (or if further
+ * authentication steps are needed), <code>false</code> is returned and
+ * one can retry either by using this or any other authentication method
+ * (use the <code>getRemainingAuthMethods</code> method to get a list of
+ * the remaining possible methods).
+ *
+ * @param user
+ * A <code>String</code> holding the username.
+ * @param key
+ * A <code>RSAPrivateKey</code> or <code>DSAPrivateKey</code>
+ * containing a DSA or RSA private key of
+ * the user in Trilead object format.
+ *
+ * @return whether the connection is now authenticated.
+ * @throws IOException
+ */
+ public synchronized boolean authenticateWithPublicKey(String user, Object key)
+ throws IOException
+ {
+ if (tm == null)
+ throw new IllegalStateException("Connection is not established!");
+
+ if (authenticated)
+ throw new IllegalStateException("Connection is already authenticated!");
+
+ if (am == null)
+ am = new AuthenticationManager(tm);
+ if (cm == null)
+ cm = new ChannelManager(tm);
+
+ if (user == null)
+ throw new IllegalArgumentException("user argument is null");
+
+ if (key == null)
+ throw new IllegalArgumentException("Key argument is null");
+
+ authenticated = am.authenticatePublicKey(user, key, getOrCreateSecureRND());
+
+ return authenticated;
+ }
/**
* A convenience wrapper function which reads in a private key (PEM format,
* either DSA or RSA) and then calls
diff --git a/src/de/mud/terminal/VDUBuffer.java b/src/de/mud/terminal/VDUBuffer.java index 60e7abf..a0d6b3b 100644 --- a/src/de/mud/terminal/VDUBuffer.java +++ b/src/de/mud/terminal/VDUBuffer.java @@ -1,7 +1,7 @@ /* * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". * - * (c) Matthias L. Jugel, Marcus Meiner 1996-2005. All Rights Reserved. + * (c) Matthias L. Jugel, Marcus Mei§ner 1996-2005. All Rights Reserved. * * Please visit http://javatelnet.org/ for updates and contact. * @@ -33,7 +33,7 @@ import android.util.Log; * all methods to manipulate the buffer that stores characters and their * attributes as well as the regions displayed. * - * @author Matthias L. Jugel, Marcus Meiner + * @author Matthias L. Jugel, Marcus Mei§ner * @version $Id: VDUBuffer.java 503 2005-10-24 07:34:13Z marcus $ */ public class VDUBuffer { diff --git a/src/de/mud/terminal/VDUInput.java b/src/de/mud/terminal/VDUInput.java index 0d89f7a..ca7e68b 100644 --- a/src/de/mud/terminal/VDUInput.java +++ b/src/de/mud/terminal/VDUInput.java @@ -1,7 +1,7 @@ /* * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". * - * (c) Matthias L. Jugel, Marcus Meiner 1996-2005. All Rights Reserved. + * (c) Matthias L. Jugel, Marcus Mei§ner 1996-2005. All Rights Reserved. * * Please visit http://javatelnet.org/ for updates and contact. * diff --git a/src/org/connectbot/GeneratePubkeyActivity.java b/src/org/connectbot/GeneratePubkeyActivity.java new file mode 100644 index 0000000..d33ffb3 --- /dev/null +++ b/src/org/connectbot/GeneratePubkeyActivity.java @@ -0,0 +1,284 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package org.connectbot; + +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import org.connectbot.util.EntropyDialog; +import org.connectbot.util.EntropyView; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.OnEntropyGatheredListener; +import org.connectbot.util.PubkeyUtils; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.RadioGroup.OnCheckedChangeListener; +import android.widget.SeekBar.OnSeekBarChangeListener; + +public class GeneratePubkeyActivity extends Activity implements OnEntropyGatheredListener { + public final static String TAG = GeneratePubkeyActivity.class.toString(); + + final static String KEY_TYPE_RSA = "RSA", + KEY_TYPE_DSA = "DSA"; + + final static int DEFAULT_BITS = 1024; + + protected LayoutInflater inflater = null; + + private EditText nickname; + private RadioGroup keyTypeGroup; + private SeekBar bitsSlider; + private EditText bitsText; + private CheckBox unlockAtStartup; + private Button save; + private Dialog entropyDialog; + private ProgressDialog progress; + + private EditText password1, password2; + + private String keyType = KEY_TYPE_RSA; + private int minBits = 768; + private int bits = DEFAULT_BITS; + + private byte[] entropy; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.act_generatepubkey); + + nickname = (EditText) findViewById(R.id.nickname); + + keyTypeGroup = (RadioGroup) findViewById(R.id.key_type); + + bitsText = (EditText) findViewById(R.id.bits); + bitsSlider = (SeekBar) findViewById(R.id.bits_slider); + + password1 = (EditText) findViewById(R.id.password1); + password2 = (EditText) findViewById(R.id.password2); + + unlockAtStartup = (CheckBox) findViewById(R.id.unlock_at_startup); + + save = (Button) findViewById(R.id.save); + + inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + nickname.addTextChangedListener(textChecker); + password1.addTextChangedListener(textChecker); + password2.addTextChangedListener(textChecker); + + keyTypeGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + public void onCheckedChanged(RadioGroup group, int checkedId) { + if (checkedId == R.id.rsa) { + minBits = 768; + + bitsSlider.setEnabled(true); + bitsSlider.setProgress(DEFAULT_BITS - minBits); + + bitsText.setText(String.valueOf(DEFAULT_BITS)); + bitsText.setEnabled(true); + + keyType = KEY_TYPE_RSA; + } else if (checkedId == R.id.dsa) { + // DSA keys can only be 1024 bits + + bitsSlider.setEnabled(false); + bitsSlider.setProgress(DEFAULT_BITS - minBits); + + bitsText.setText(String.valueOf(DEFAULT_BITS)); + bitsText.setEnabled(false); + + keyType = KEY_TYPE_DSA; + } + } + }); + + bitsSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + // Stay evenly divisible by 8 because it looks nicer to have + // 2048 than 2043 bits. + + int leftover = progress % 8; + + if (leftover > 0) + progress += 8 - leftover; + + bits = minBits + progress; + bitsText.setText(String.valueOf(bits)); + } + + public void onStartTrackingTouch(SeekBar seekBar) { + // We don't care about the start. + } + + public void onStopTrackingTouch(SeekBar seekBar) { + // We don't care about the stop. + } + }); + + bitsText.setOnFocusChangeListener(new OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus) { + try { + bits = Integer.parseInt(bitsText.getText().toString()); + } catch (NumberFormatException nfe) { + bits = DEFAULT_BITS; + bitsText.setText(String.valueOf(bits)); + } + } + } + }); + + save.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + GeneratePubkeyActivity.this.save.setEnabled(false); + + GeneratePubkeyActivity.this.startEntropyGather(); + } + }); + + } + + private void checkEntries() { + boolean allowSave = true; + + if (!password1.getText().toString().equals(password2.getText().toString())) + allowSave = false; + + if (nickname.getText().length() == 0) + allowSave = false; + + save.setEnabled(allowSave); + } + + private void startEntropyGather() { + final View entropyView = inflater.inflate(R.layout.dia_gatherentropy, null, false); + ((EntropyView)entropyView.findViewById(R.id.entropy)).addOnEntropyGatheredListener(GeneratePubkeyActivity.this); + entropyDialog = new EntropyDialog(GeneratePubkeyActivity.this, entropyView); + entropyDialog.show(); + } + + public void onEntropyGathered(byte[] entropy) { + this.entropy = entropy; + + Log.d(TAG, "entropy gathered; attemping to generate key..."); + startKeyGen(); + } + + private void startKeyGen() { + progress = new ProgressDialog(GeneratePubkeyActivity.this); + progress.setMessage(GeneratePubkeyActivity.this.getResources().getText(R.string.pubkey_generating)); + progress.setIndeterminate(true); + progress.setCancelable(false); + progress.show(); + + new Thread(mKeyGen).start(); + } + + private Handler handler = new Handler() { + public void handleMessage(Message msg) { + progress.dismiss(); + GeneratePubkeyActivity.this.finish(); + } + }; + + final private Runnable mKeyGen = new Runnable() { + public void run() { + try { + boolean encrypted = false; + + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + + random.setSeed(entropy); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(keyType); + + keyPairGen.initialize(bits, random); + + KeyPair pair = keyPairGen.generateKeyPair(); + PrivateKey priv = pair.getPrivate(); + PublicKey pub = pair.getPublic(); + + String secret = password1.getText().toString(); + if (secret.length() > 0) + encrypted = true; + + Log.d(TAG, "private: " + PubkeyUtils.formatKey(priv)); + Log.d(TAG, "public: " + PubkeyUtils.formatKey(pub)); + + PubkeyDatabase hostdb = new PubkeyDatabase(GeneratePubkeyActivity.this); + hostdb.createPubkey(null, + nickname.getText().toString(), + keyType, + PubkeyUtils.getEncodedPrivate(priv, secret), + PubkeyUtils.getEncodedPublic(pub), + encrypted, + unlockAtStartup.isChecked()); + } catch (Exception e) { + Log.e(TAG, "Could not generate key pair"); + + e.printStackTrace(); + } + + handler.sendEmptyMessage(0); + } + + }; + + final private TextWatcher textChecker = new TextWatcher() { + public void afterTextChanged(Editable s) {} + + public void beforeTextChanged(CharSequence s, int start, int count, + int after) {} + + public void onTextChanged(CharSequence s, int start, int before, + int count) { + checkEntries(); + } + }; +} diff --git a/src/org/connectbot/HostListActivity.java b/src/org/connectbot/HostListActivity.java index 0e6c915..fc1ef6b 100644 --- a/src/org/connectbot/HostListActivity.java +++ b/src/org/connectbot/HostListActivity.java @@ -346,9 +346,9 @@ public class HostListActivity extends ListActivity { } }); - //MenuItem keys = menu.add("Manage keys"); - //keys.setIcon(android.R.drawable.ic_lock_lock); - //keys.setEnabled(false); + MenuItem keys = menu.add("Manage keys"); + keys.setIcon(android.R.drawable.ic_lock_lock); + keys.setIntent(new Intent(HostListActivity.this, PubkeyListActivity.class)); MenuItem settings = menu.add(R.string.list_menu_settings); settings.setIcon(android.R.drawable.ic_menu_preferences); diff --git a/src/org/connectbot/PubkeyListActivity.java b/src/org/connectbot/PubkeyListActivity.java new file mode 100644 index 0000000..a8b2bed --- /dev/null +++ b/src/org/connectbot/PubkeyListActivity.java @@ -0,0 +1,226 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package org.connectbot; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.EventListener; + +import org.connectbot.util.EntropyView; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.ClipboardManager; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.MenuItem.OnMenuItemClickListener; +import android.widget.AdapterView; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +public class PubkeyListActivity extends ListActivity implements EventListener { + public final static String TAG = PubkeyListActivity.class.toString(); + + protected PubkeyDatabase pubkeydb; + protected Cursor pubkeys; + + protected int COL_ID, COL_NICKNAME, COL_TYPE, COL_PRIVATE, COL_PUBLIC, COL_ENCRYPTED, COL_STARTUP; + + protected ClipboardManager clipboard; + + @Override + public void onStart() { + super.onStart(); + + if(this.pubkeydb == null) + this.pubkeydb = new PubkeyDatabase(this); + + this.updateCursor(); + + ListView list = this.getListView(); + this.registerForContextMenu(list); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_pubkeylist); + + // connect with hosts database and populate list + this.pubkeydb = new PubkeyDatabase(this); + + this.updateCursor(); + + this.COL_ID = pubkeys.getColumnIndexOrThrow("_id"); + this.COL_NICKNAME = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_NICKNAME); + this.COL_TYPE = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_TYPE); + this.COL_PRIVATE = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PRIVATE); + this.COL_PUBLIC = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PUBLIC); + this.COL_ENCRYPTED = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED); + this.COL_STARTUP = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_STARTUP); + + this.clipboard = (ClipboardManager)this.getSystemService(CLIPBOARD_SERVICE); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + MenuItem generatekey = menu.add(R.string.pubkey_generate); + generatekey.setIcon(android.R.drawable.ic_lock_lock); + generatekey.setIntent(new Intent(PubkeyListActivity.this, GeneratePubkeyActivity.class)); + + // TODO: allow importing of keys + //MenuItem importkey = menu.add("Import"); + //importkey.setIcon(android.R.drawable.ic_lock_lock); + //importkey.setIntent(new Intent(PubkeyListActivity.this, ImportPubkeyActivity.class)); + + return true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + // Create menu to handle deleting and editing pubkey + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + Cursor cursor = (Cursor) this.getListView().getItemAtPosition(info.position); + + final String nickname = cursor.getString(COL_NICKNAME); + menu.setHeaderTitle(nickname); + final int id = cursor.getInt(COL_ID); + final byte[] pubkeyEncoded = cursor.getBlob(COL_PUBLIC); + final String keyType = cursor.getString(COL_TYPE); + + MenuItem delete = menu.add(R.string.pubkey_delete); + delete.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // prompt user to make sure they really want this + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(getString(R.string.delete_message, nickname)) + .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + pubkeydb.deletePubkey(id); + updateHandler.sendEmptyMessage(-1); + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + + return true; + } + }); + + MenuItem copyToClipboard = menu.add(R.string.pubkey_copy_clipboard); + copyToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + Log.d(TAG, "Trying to decode public key format: " + keyType); + + PublicKey pk = PubkeyUtils.decodePublic(pubkeyEncoded, keyType); + String openSSHPubkey = new String(PubkeyUtils.convertToOpenSSHFormat(pk)); + + Log.d(TAG, "OpenSSH format: " + openSSHPubkey); + + clipboard.setText(openSSHPubkey); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + }); + } + + public Handler updateHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + PubkeyListActivity.this.updateCursor(); + } + }; + + protected void updateCursor() { + this.pubkeys = this.pubkeydb.allPubkeys(); + + this.setListAdapter(new PubkeyCursorAdapter(this, this.pubkeys)); + } + + class PubkeyCursorAdapter extends CursorAdapter { + private final LayoutInflater mInflater; + private final int mNickname; + private final int mPubkey; + private final int mKeyType; + private final int mEncrypted; + + public PubkeyCursorAdapter(Context context, Cursor c) { + super(context, c); + + mInflater = LayoutInflater.from(context); + mNickname = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_NICKNAME); + mPubkey = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PUBLIC); + mEncrypted = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED); + mKeyType = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_TYPE); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView text1 = (TextView) view.findViewById(android.R.id.text1); + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + + text1.setText(cursor.getString(mNickname)); + + String keyType = cursor.getString(mKeyType); + int encrypted = cursor.getInt(mEncrypted); + PublicKey pk; + try { + pk = PubkeyUtils.decodePublic(cursor.getBlob(mPubkey), keyType); + text2.setText(PubkeyUtils.describeKey(pk, encrypted)); + } catch (Exception e) { + e.printStackTrace(); + + Log.e(TAG, "Error decoding public key at " + cursor.toString()); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + final LinearLayout view = (LinearLayout) mInflater.inflate( + R.layout.item_pubkey, parent, false); + return view; + } + } + +} diff --git a/src/org/connectbot/service/TerminalBridge.java b/src/org/connectbot/service/TerminalBridge.java index c8df48f..9f8bd9d 100644 --- a/src/org/connectbot/service/TerminalBridge.java +++ b/src/org/connectbot/service/TerminalBridge.java @@ -21,12 +21,15 @@ package org.connectbot.service; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.concurrent.Semaphore; +import java.security.PrivateKey; +import java.security.PublicKey; -import org.connectbot.ConsoleActivity; import org.connectbot.TerminalView; -import org.connectbot.util.HostDatabase; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; @@ -34,8 +37,6 @@ import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.Bitmap.Config; import android.graphics.Paint.FontMetricsInt; -import android.os.Handler; -import android.os.Message; import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -121,6 +122,10 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal private String currentMethod = null; + protected PubkeyDatabase pubkeydb = null; + protected Cursor pubkeys = null; + final int COL_NICKNAME, COL_TYPE, COL_PRIVATE, COL_PUBLIC, COL_ENCRYPTED; + private boolean pubkeysExhausted = false; public class HostKeyVerifier implements ServerHostKeyVerifier { @@ -226,6 +231,14 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal this.connection = new Connection(hostname, port); this.connection.addConnectionMonitor(this); + this.pubkeydb = new PubkeyDatabase(manager); + this.pubkeys = this.pubkeydb.allPubkeys(); + + this.COL_NICKNAME = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_NICKNAME); + this.COL_TYPE = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_TYPE); + this.COL_PRIVATE = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PRIVATE); + this.COL_PUBLIC = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PUBLIC); + this.COL_ENCRYPTED = pubkeys.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED); } public final static int AUTH_TRIES = 20; @@ -268,9 +281,34 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal try { - // TODO: insert publickey auth check here - - if(connection.isAuthMethodAvailable(username, AUTH_PASSWORD)) { + if (!pubkeysExhausted && + connection.isAuthMethodAvailable(username, AUTH_PUBLICKEY)) { + Cursor cursor = pubkeydb.allPubkeys(); + String keyNickname; + PrivateKey privKey; + PublicKey pubKey; + + while (cursor.moveToNext()) { + if (cursor.getInt(COL_ENCRYPTED) == 0) { + keyNickname = cursor.getString(COL_NICKNAME); + privKey = PubkeyUtils.decodePrivate(cursor.getBlob(COL_PRIVATE), + cursor.getString(COL_TYPE)); + pubKey = PubkeyUtils.decodePublic(cursor.getBlob(COL_PUBLIC), + cursor.getString(COL_TYPE)); + + Log.d("TerminalBridge", "Trying key " + PubkeyUtils.formatKey(pubKey)); + + if (connection.authenticateWithPublicKey(username, + PubkeyUtils.convertToTrilead(privKey, pubKey))) { + finishConnection(); + } else { + outputLine("Authentication method 'publickey' with key " + keyNickname + " failed"); + } + } + } + + pubkeysExhausted = true; + } else if (connection.isAuthMethodAvailable(username, AUTH_PASSWORD)) { outputLine("Attempting 'password' authentication"); String password = promptHelper.requestStringPrompt("Password"); if(connection.authenticateWithPassword(username, password)) { @@ -638,6 +676,9 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal this.bitmap.recycle(); this.bitmap = null; this.canvas.setBitmap(null); + + if (this.pubkeydb != null) + this.pubkeydb.close(); } public void setVDUBuffer(VDUBuffer buffer) { diff --git a/src/org/connectbot/util/EntropyDialog.java b/src/org/connectbot/util/EntropyDialog.java new file mode 100644 index 0000000..c6dc4e1 --- /dev/null +++ b/src/org/connectbot/util/EntropyDialog.java @@ -0,0 +1,50 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package org.connectbot.util; + +import org.connectbot.R; + +import android.app.Dialog; +import android.content.Context; +import android.view.View; + +public class EntropyDialog extends Dialog implements OnEntropyGatheredListener { + + public EntropyDialog(Context context) { + super(context); + + this.setContentView(R.layout.dia_gatherentropy); + this.setTitle(R.string.pubkey_gather_entropy); + + ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this); + } + + public EntropyDialog(Context context, View view) { + super(context); + + this.setContentView(view); + this.setTitle(R.string.pubkey_gather_entropy); + + ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this); + } + + public void onEntropyGathered(byte[] entropy) { + this.dismiss(); + } + +} diff --git a/src/org/connectbot/util/EntropyView.java b/src/org/connectbot/util/EntropyView.java new file mode 100644 index 0000000..977b536 --- /dev/null +++ b/src/org/connectbot/util/EntropyView.java @@ -0,0 +1,144 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +package org.connectbot.util; + +import java.util.Vector; + +import org.connectbot.R; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.Paint.FontMetrics; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +public class EntropyView extends View { + private Paint mPaint; + private FontMetrics mFontMetrics; + private boolean mFlipFlop; + private long mLastTime; + private Vector<OnEntropyGatheredListener> listeners; + + private byte[] mEntropy; + private int mEntropyIdx; + + private int splitText = 0; + + private float lastX = 0.0f, lastY = 0.0f; + + public EntropyView(Context context) { + super(context); + + setUpEntropy(); + } + + public EntropyView(Context context, AttributeSet attrs) { + super(context, attrs); + + setUpEntropy(); + } + + private void setUpEntropy() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTypeface(Typeface.DEFAULT); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setTextSize(16); + mPaint.setColor(Color.WHITE); + mFontMetrics = mPaint.getFontMetrics(); + + mEntropy = new byte[20]; + mEntropyIdx = 0; + + listeners = new Vector<OnEntropyGatheredListener>(); + } + + public void addOnEntropyGatheredListener(OnEntropyGatheredListener listener) { + listeners.add(listener); + } + + public void removeOnEntropyGatheredListener(OnEntropyGatheredListener listener) { + listeners.remove(listener); + } + + public void onDraw(Canvas c) { + String prompt = getResources().getString(R.string.pubkey_touch_prompt) + + " " + (int)(100.0 * (mEntropyIdx / 20.0)) + "% done"; + if (splitText > 0 || + mPaint.measureText(prompt) > (getWidth() * 0.8)) { + if (splitText == 0) + splitText = prompt.indexOf(" ", prompt.length() / 2); + + c.drawText(prompt.substring(0, splitText), + getWidth() / 2, + getHeight() / 2 + (mPaint.ascent() + mPaint.descent()), + mPaint); + c.drawText(prompt.substring(splitText), + getWidth() / 2, + getHeight() / 2 - (mPaint.ascent() + mPaint.descent()), + mPaint); + } else { + c.drawText(prompt, + getWidth() / 2, + getHeight() / 2 - (mFontMetrics.ascent + mFontMetrics.descent) / 2, + mPaint); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mEntropyIdx >= 20 + || lastX == event.getX() + || lastY == event.getY()) + return true; + + // Only get entropy every 200 milliseconds to ensure the user has moved around. + long now = System.currentTimeMillis(); + if ((now - mLastTime) < 200) + return true; + else + mLastTime = now; + + // Get the lowest 4 bits of each X, Y input and concat to the entropy-gathering + // string. + if (mFlipFlop) + mEntropy[mEntropyIdx++] += (byte)((((int)event.getX() & 0x0F) << 4) | ((int)event.getY() & 0x0F)); + else + mEntropy[mEntropyIdx++] += (byte)((((int)event.getY() & 0x0F) << 4) | ((int)event.getX() & 0x0F)); + + mFlipFlop = !mFlipFlop; + lastX = event.getX(); + lastY = event.getY(); + + // SHA1PRNG only keeps 20 bytes (160 bits) of entropy. + if (mEntropyIdx >= 20) { + for (OnEntropyGatheredListener listener: listeners) { + listener.onEntropyGathered(mEntropy); + } + } + + invalidate(); + + return true; + } +} diff --git a/src/org/connectbot/util/OnEntropyGatheredListener.java b/src/org/connectbot/util/OnEntropyGatheredListener.java new file mode 100644 index 0000000..9dd2e44 --- /dev/null +++ b/src/org/connectbot/util/OnEntropyGatheredListener.java @@ -0,0 +1,23 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package org.connectbot.util; + +public interface OnEntropyGatheredListener { + void onEntropyGathered(byte[] entropy); +} diff --git a/src/org/connectbot/util/PubkeyDatabase.java b/src/org/connectbot/util/PubkeyDatabase.java new file mode 100644 index 0000000..8cfebe9 --- /dev/null +++ b/src/org/connectbot/util/PubkeyDatabase.java @@ -0,0 +1,110 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package org.connectbot.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * Public Key Encryption database. Contains private and public key pairs + * for public key authentication. + * + * @author kroot + */ +public class PubkeyDatabase extends SQLiteOpenHelper { + + public final static String TAG = PubkeyDatabase.class.toString(); + + public final static String DB_NAME = "pubkeys"; + public final static int DB_VERSION = 1; + + public final static String TABLE_PUBKEYS = "pubkeys"; + public final static String FIELD_PUBKEY_NICKNAME = "nickname"; + public final static String FIELD_PUBKEY_TYPE = "type"; + public final static String FIELD_PUBKEY_PRIVATE = "private"; + public final static String FIELD_PUBKEY_PUBLIC = "public"; + public final static String FIELD_PUBKEY_ENCRYPTED = "encrypted"; + public final static String FIELD_PUBKEY_STARTUP = "startup"; + + public PubkeyDatabase(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_PUBKEYS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_PUBKEY_NICKNAME + " TEXT, " + + FIELD_PUBKEY_TYPE + " TEXT, " + + FIELD_PUBKEY_PRIVATE + " BLOB, " + + FIELD_PUBKEY_PUBLIC + " BLOB, " + + FIELD_PUBKEY_ENCRYPTED + " INTEGER, " + + FIELD_PUBKEY_STARTUP + " INTEGER)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } + + /** + * Create a new pubkey using the given parameters, and return its new + * <code>_id</code> value. + */ + public long createPubkey(SQLiteDatabase db, String nickname, String type, byte[] privatekey, + byte[] publickey, boolean encrypted, boolean startup) { + // create and insert new host + + if (db == null) + db = this.getWritableDatabase(); + + ContentValues values = new ContentValues(); + values.put(FIELD_PUBKEY_NICKNAME, nickname); + values.put(FIELD_PUBKEY_TYPE, type); + values.put(FIELD_PUBKEY_PRIVATE, privatekey); + values.put(FIELD_PUBKEY_PUBLIC, publickey); + values.put(FIELD_PUBKEY_ENCRYPTED, encrypted ? 1 : 0); + values.put(FIELD_PUBKEY_STARTUP, startup ? 1 : 0); + + return db.insert(TABLE_PUBKEYS, null, values); + } + + /** + * Delete a specific host by its <code>_id</code> value. + */ + public void deletePubkey(long id) { + SQLiteDatabase db = this.getWritableDatabase(); + db.delete(TABLE_PUBKEYS, "_id = ?", new String[] { Long.toString(id) }); + } + + /** + * Return a cursor that contains information about all known hosts. + */ + public Cursor allPubkeys() { + SQLiteDatabase db = this.getReadableDatabase(); + return db.query(TABLE_PUBKEYS, new String[] { "_id", + FIELD_PUBKEY_NICKNAME, FIELD_PUBKEY_TYPE, FIELD_PUBKEY_PRIVATE, + FIELD_PUBKEY_PUBLIC, FIELD_PUBKEY_ENCRYPTED, FIELD_PUBKEY_STARTUP }, + null, null, null, null, null); + } + +} diff --git a/src/org/connectbot/util/PubkeyUtils.java b/src/org/connectbot/util/PubkeyUtils.java new file mode 100644 index 0000000..b73c58e --- /dev/null +++ b/src/org/connectbot/util/PubkeyUtils.java @@ -0,0 +1,194 @@ +/* + ConnectBot: simple, powerful, open-source SSH client for Android + Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package org.connectbot.util; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import android.util.Log; + +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + +public class PubkeyUtils { + public static String formatKey(Key key){ + String algo = key.getAlgorithm(); + String fmt = key.getFormat(); + byte[] encoded = key.getEncoded(); + return "Key[algorithm=" + algo + ", format=" + fmt + + ", bytes=" + encoded.length + "]"; + } + + public static String describeKey(Key key, int encrypted) { + String desc = null; + if (key instanceof RSAPublicKey) { + desc = "RSA " + String.valueOf(((RSAPublicKey)key).getModulus().bitLength()) + "-bit"; + } else if (key instanceof DSAPublicKey) { + desc = "DSA 1024-bit"; + } else { + desc = "Unknown Key Type"; + } + + if (encrypted != 0) + desc += " (encrypted)"; + + return desc; + } + + public static byte[] sha1(byte[] data) throws NoSuchAlgorithmException { + MessageDigest hash = MessageDigest.getInstance("SHA-256"); + byte[] hashed = hash.digest(data); + Log.d("KeyUtils", "hash is " + hashed.length + " bytes"); + return hash.digest(data); + } + + public static byte[] cipher(int mode, byte[] data, byte[] secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + SecretKeySpec secretKeySpec = new SecretKeySpec(sha1(secret), "AES"); + Cipher c = Cipher.getInstance("AES"); + c.init(mode, secretKeySpec); + return c.doFinal(data); + } + + public static byte[] encrypt(byte[] cleartext, byte[] secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + return cipher(Cipher.ENCRYPT_MODE, cleartext, secret); + } + + public static byte[] encrypt(byte[] cleartext, String secret) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + return cipher(Cipher.ENCRYPT_MODE, cleartext, secret.getBytes()); + } + + public static byte[] decrypt(byte[] ciphertext, byte[] secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + return cipher(Cipher.DECRYPT_MODE, ciphertext, secret); + } + + public static byte[] decrypt(byte[] ciphertext, String secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + return cipher(Cipher.DECRYPT_MODE, ciphertext, secret.getBytes()); + } + + public static byte[] getEncodedPublic(PublicKey pk) { + X509EncodedKeySpec x509 = new X509EncodedKeySpec(pk.getEncoded()); + return x509.getEncoded(); + } + + public static byte[] getEncodedPrivate(PrivateKey pk) { + PKCS8EncodedKeySpec pkcs8 = new PKCS8EncodedKeySpec(pk.getEncoded()); + return pkcs8.getEncoded(); + } + + public static byte[] getEncodedPrivate(PrivateKey pk, String secret) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + if (secret.length() > 0) + return encrypt(getEncodedPrivate(pk), secret); + else + return getEncodedPrivate(pk); + } + + public static PrivateKey decodePrivate(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException { + PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance(keyType); + return kf.generatePrivate(privKeySpec); + } + + public static PrivateKey decodePrivate(byte[] encoded, String keyType, String secret) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException { + if (secret.length() > 0) + return decodePrivate(decrypt(encoded, secret), keyType); + else + return decodePrivate(encoded, keyType); + } + + public static PublicKey decodePublic(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException { + X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance(keyType); + return kf.generatePublic(pubKeySpec); + } + + /* + * Trilead compatibility methods + */ + + public static Object convertToTrilead(PublicKey pk) { + if (pk instanceof RSAPublicKey) { + return new com.trilead.ssh2.signature.RSAPublicKey( + ((RSAPublicKey) pk).getPublicExponent(), + ((RSAPublicKey) pk).getModulus()); + } else if (pk instanceof DSAPublicKey) { + DSAParams dp = ((DSAPublicKey) pk).getParams(); + return new com.trilead.ssh2.signature.DSAPublicKey( + dp.getP(), dp.getQ(), dp.getG(), ((DSAPublicKey) pk).getY()); + } + + throw new IllegalArgumentException("PrivateKey is not RSA or DSA format"); + } + + public static Object convertToTrilead(PrivateKey priv, PublicKey pub) { + if (priv instanceof RSAPrivateKey) { + return new com.trilead.ssh2.signature.RSAPrivateKey( + ((RSAPrivateKey) priv).getPrivateExponent(), + ((RSAPublicKey) pub).getPublicExponent(), + ((RSAPrivateKey) priv).getModulus()); + } else if (priv instanceof DSAPrivateKey) { + DSAParams dp = ((DSAPrivateKey) priv).getParams(); + return new com.trilead.ssh2.signature.DSAPrivateKey( + dp.getP(), dp.getQ(), dp.getG(), ((DSAPublicKey) pub).getY(), + ((DSAPrivateKey) priv).getX()); + } + + throw new IllegalArgumentException("Key is not RSA or DSA format"); + } + + /* + * OpenSSH compatibility methods + */ + + public static String convertToOpenSSHFormat(PublicKey pk) throws IOException, InvalidKeyException { + if (pk instanceof RSAPublicKey) { + String data = "ssh-rsa "; + data += String.valueOf(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey( + (com.trilead.ssh2.signature.RSAPublicKey)convertToTrilead(pk)))); + return data + " connectbot@android"; + } else if (pk instanceof DSAPublicKey) { + String data = "ssh-dss "; + data += String.valueOf(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey( + (com.trilead.ssh2.signature.DSAPublicKey)convertToTrilead(pk)))); + return data + " connectbot@android"; + } + + throw new InvalidKeyException("Unknown key type"); + } +} |