2 * ConnectBot: simple, powerful, open-source SSH client for Android
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 package org.connectbot;
20 import java.io.ByteArrayOutputStream;
22 import java.io.FileInputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
26 import java.security.KeyPair;
27 import java.security.PrivateKey;
28 import java.security.PublicKey;
29 import java.util.Collections;
30 import java.util.EventListener;
31 import java.util.LinkedList;
32 import java.util.List;
34 import org.connectbot.bean.PubkeyBean;
35 import org.connectbot.service.TerminalManager;
36 import org.connectbot.util.PubkeyDatabase;
37 import org.connectbot.util.PubkeyUtils;
38 import org.openintents.intents.FileManagerIntents;
40 import android.app.AlertDialog;
41 import android.app.ListActivity;
42 import android.content.ActivityNotFoundException;
43 import android.content.ComponentName;
44 import android.content.Context;
45 import android.content.DialogInterface;
46 import android.content.Intent;
47 import android.content.ServiceConnection;
48 import android.content.DialogInterface.OnClickListener;
49 import android.net.Uri;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.os.IBinder;
54 import android.os.Message;
55 import android.text.ClipboardManager;
56 import android.util.Log;
57 import android.view.ContextMenu;
58 import android.view.LayoutInflater;
59 import android.view.Menu;
60 import android.view.MenuItem;
61 import android.view.View;
62 import android.view.ViewGroup;
63 import android.view.MenuItem.OnMenuItemClickListener;
64 import android.widget.AdapterView;
65 import android.widget.ArrayAdapter;
66 import android.widget.EditText;
67 import android.widget.ImageView;
68 import android.widget.TableRow;
69 import android.widget.TextView;
70 import android.widget.Toast;
71 import android.widget.AdapterView.OnItemClickListener;
73 import com.trilead.ssh2.crypto.Base64;
74 import com.trilead.ssh2.crypto.PEMDecoder;
75 import com.trilead.ssh2.crypto.PEMStructure;
78 * List public keys in database by nickname and describe their properties. Allow users to import,
79 * generate, rename, and delete key pairs.
83 public class PubkeyListActivity extends ListActivity implements EventListener {
84 public final static String TAG = "ConnectBot.PubkeyListActivity";
86 private static final int MAX_KEYFILE_SIZE = 8192;
87 private static final int REQUEST_CODE_PICK_FILE = 1;
89 // Constants for AndExplorer's file picking intent
90 private static final String ANDEXPLORER_TITLE = "explorer_title";
91 private static final String MIME_TYPE_ANDEXPLORER_FILE = "vnd.android.cursor.dir/lysesoft.andexplorer.file";
93 protected PubkeyDatabase pubkeydb;
94 private List<PubkeyBean> pubkeys;
96 protected ClipboardManager clipboard;
98 protected LayoutInflater inflater = null;
100 protected TerminalManager bound = null;
102 private MenuItem onstartToggle = null;
103 private MenuItem confirmUse = null;
105 private ServiceConnection connection = new ServiceConnection() {
106 public void onServiceConnected(ComponentName className, IBinder service) {
107 bound = ((TerminalManager.TerminalBinder) service).getService();
109 // update our listview binder to find the service
113 public void onServiceDisconnected(ComponentName className) {
120 public void onStart() {
123 bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
126 pubkeydb = new PubkeyDatabase(this);
130 public void onStop() {
133 unbindService(connection);
135 if(pubkeydb != null) {
142 public void onCreate(Bundle icicle) {
143 super.onCreate(icicle);
144 setContentView(R.layout.act_pubkeylist);
146 this.setTitle(String.format("%s: %s",
147 getResources().getText(R.string.app_name),
148 getResources().getText(R.string.title_pubkey_list)));
150 // connect with hosts database and populate list
151 pubkeydb = new PubkeyDatabase(this);
155 registerForContextMenu(getListView());
157 getListView().setOnItemClickListener(new OnItemClickListener() {
158 public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
159 PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(position);
160 boolean loaded = bound.isKeyLoaded(pubkey.getNickname());
162 // handle toggling key in-memory on/off
164 bound.removeKey(pubkey.getNickname());
165 updateHandler.sendEmptyMessage(-1);
167 handleAddKey(pubkey);
173 clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
175 inflater = LayoutInflater.from(this);
179 * Read given file into memory as <code>byte[]</code>.
181 protected static byte[] readRaw(File file) throws Exception {
182 InputStream is = new FileInputStream(file);
183 ByteArrayOutputStream os = new ByteArrayOutputStream();
186 byte[] buffer = new byte[1024];
187 while ((bytesRead = is.read(buffer)) != -1) {
188 os.write(buffer, 0, bytesRead);
195 return os.toByteArray();
200 public boolean onCreateOptionsMenu(Menu menu) {
201 super.onCreateOptionsMenu(menu);
203 MenuItem generatekey = menu.add(R.string.pubkey_generate);
204 generatekey.setIcon(android.R.drawable.ic_menu_manage);
205 generatekey.setIntent(new Intent(PubkeyListActivity.this, GeneratePubkeyActivity.class));
207 MenuItem importkey = menu.add(R.string.pubkey_import);
208 importkey.setIcon(android.R.drawable.ic_menu_upload);
209 importkey.setOnMenuItemClickListener(new OnMenuItemClickListener() {
210 public boolean onMenuItemClick(MenuItem item) {
211 Uri sdcard = Uri.fromFile(Environment.getExternalStorageDirectory());
212 String pickerTitle = getString(R.string.pubkey_list_pick);
214 // Try to use OpenIntent's file browser to pick a file
215 Intent intent = new Intent(FileManagerIntents.ACTION_PICK_FILE);
216 intent.setData(sdcard);
217 intent.putExtra(FileManagerIntents.EXTRA_TITLE, pickerTitle);
218 intent.putExtra(FileManagerIntents.EXTRA_BUTTON_TEXT, getString(android.R.string.ok));
221 startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
222 } catch (ActivityNotFoundException e) {
223 // If OI didn't work, try AndExplorer
224 intent = new Intent(Intent.ACTION_PICK);
225 intent.setDataAndType(sdcard, MIME_TYPE_ANDEXPLORER_FILE);
226 intent.putExtra(ANDEXPLORER_TITLE, pickerTitle);
229 startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
230 } catch (ActivityNotFoundException e1) {
242 protected void handleAddKey(final PubkeyBean pubkey) {
243 if (pubkey.isEncrypted()) {
244 final View view = inflater.inflate(R.layout.dia_password, null);
245 final EditText passwordField = (EditText)view.findViewById(android.R.id.text1);
247 new AlertDialog.Builder(PubkeyListActivity.this)
249 .setPositiveButton(R.string.pubkey_unlock, new DialogInterface.OnClickListener() {
250 public void onClick(DialogInterface dialog, int which) {
251 handleAddKey(pubkey, passwordField.getText().toString());
254 .setNegativeButton(android.R.string.cancel, null).create().show();
256 handleAddKey(pubkey, null);
260 protected void handleAddKey(PubkeyBean pubkey, String password) {
261 Object trileadKey = null;
262 if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) {
263 // load specific key using pem format
265 trileadKey = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password);
266 } catch(Exception e) {
267 String message = getResources().getString(R.string.pubkey_failed_add, pubkey.getNickname());
268 Log.e(TAG, message, e);
269 Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG);
273 // load using internal generated format
274 PrivateKey privKey = null;
275 PublicKey pubKey = null;
277 privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType(), password);
278 pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
279 } catch (Exception e) {
280 String message = getResources().getString(R.string.pubkey_failed_add, pubkey.getNickname());
281 Log.e(TAG, message, e);
282 Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG);
286 // convert key to trilead format
287 trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey);
288 Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
291 if(trileadKey == null) return;
293 Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname()));
295 // save this key in memory
296 bound.addKey(pubkey, trileadKey);
298 updateHandler.sendEmptyMessage(-1);
302 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
303 // Create menu to handle deleting and editing pubkey
304 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
305 final PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(info.position);
307 menu.setHeaderTitle(pubkey.getNickname());
309 // TODO: option load/unload key from in-memory list
310 // prompt for password as needed for passworded keys
312 // cant change password or clipboard imported keys
313 final boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType());
314 final boolean loaded = bound.isKeyLoaded(pubkey.getNickname());
316 MenuItem load = menu.add(loaded ? R.string.pubkey_memory_unload : R.string.pubkey_memory_load);
317 load.setOnMenuItemClickListener(new OnMenuItemClickListener() {
318 public boolean onMenuItemClick(MenuItem item) {
320 bound.removeKey(pubkey.getNickname());
321 updateHandler.sendEmptyMessage(-1);
323 handleAddKey(pubkey);
324 //bound.addKey(nickname, trileadKey);
330 onstartToggle = menu.add(R.string.pubkey_load_on_start);
331 onstartToggle.setEnabled(!pubkey.isEncrypted());
332 onstartToggle.setCheckable(true);
333 onstartToggle.setChecked(pubkey.isStartup());
334 onstartToggle.setOnMenuItemClickListener(new OnMenuItemClickListener() {
335 public boolean onMenuItemClick(MenuItem item) {
336 // toggle onstart status
337 pubkey.setStartup(!pubkey.isStartup());
338 pubkeydb.savePubkey(pubkey);
339 updateHandler.sendEmptyMessage(-1);
344 MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public);
345 copyPublicToClipboard.setEnabled(!imported);
346 copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() {
347 public boolean onMenuItemClick(MenuItem item) {
349 PublicKey pk = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
350 String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname());
352 clipboard.setText(openSSHPubkey);
353 } catch (Exception e) {
360 MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private);
361 copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported);
362 copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() {
363 public boolean onMenuItemClick(MenuItem item) {
368 data = new String(pubkey.getPrivateKey());
370 PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType());
371 data = PubkeyUtils.exportPEM(pk, null);
374 clipboard.setText(data);
375 } catch (Exception e) {
382 MenuItem changePassword = menu.add(R.string.pubkey_change_password);
383 changePassword.setEnabled(!imported);
384 changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() {
385 public boolean onMenuItemClick(MenuItem item) {
386 final View changePasswordView = inflater.inflate(R.layout.dia_changepassword, null, false);
387 ((TableRow)changePasswordView.findViewById(R.id.old_password_prompt))
388 .setVisibility(pubkey.isEncrypted() ? View.VISIBLE : View.GONE);
389 new AlertDialog.Builder(PubkeyListActivity.this)
390 .setView(changePasswordView)
391 .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() {
392 public void onClick(DialogInterface dialog, int which) {
393 String oldPassword = ((EditText)changePasswordView.findViewById(R.id.old_password)).getText().toString();
394 String password1 = ((EditText)changePasswordView.findViewById(R.id.password1)).getText().toString();
395 String password2 = ((EditText)changePasswordView.findViewById(R.id.password2)).getText().toString();
397 if (!password1.equals(password2)) {
398 new AlertDialog.Builder(PubkeyListActivity.this)
399 .setMessage(R.string.alert_passwords_do_not_match_msg)
400 .setPositiveButton(android.R.string.ok, null)
406 if (!pubkey.changePassword(oldPassword, password1))
407 new AlertDialog.Builder(PubkeyListActivity.this)
408 .setMessage(R.string.alert_wrong_password_msg)
409 .setPositiveButton(android.R.string.ok, null)
412 pubkeydb.savePubkey(pubkey);
413 updateHandler.sendEmptyMessage(-1);
415 } catch (Exception e) {
416 Log.e(TAG, "Could not change private key password", e);
417 new AlertDialog.Builder(PubkeyListActivity.this)
418 .setMessage(R.string.alert_key_corrupted_msg)
419 .setPositiveButton(android.R.string.ok, null)
424 .setNegativeButton(android.R.string.cancel, null).create().show();
430 confirmUse = menu.add(R.string.pubkey_confirm_use);
431 confirmUse.setCheckable(true);
432 confirmUse.setChecked(pubkey.isConfirmUse());
433 confirmUse.setOnMenuItemClickListener(new OnMenuItemClickListener() {
434 public boolean onMenuItemClick(MenuItem item) {
435 // toggle confirm use
436 pubkey.setConfirmUse(!pubkey.isConfirmUse());
437 pubkeydb.savePubkey(pubkey);
438 updateHandler.sendEmptyMessage(-1);
443 MenuItem delete = menu.add(R.string.pubkey_delete);
444 delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
445 public boolean onMenuItemClick(MenuItem item) {
446 // prompt user to make sure they really want this
447 new AlertDialog.Builder(PubkeyListActivity.this)
448 .setMessage(getString(R.string.delete_message, pubkey.getNickname()))
449 .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() {
450 public void onClick(DialogInterface dialog, int which) {
452 // dont forget to remove from in-memory
454 bound.removeKey(pubkey.getNickname());
456 // delete from backend database and update gui
457 pubkeydb.deletePubkey(pubkey);
458 updateHandler.sendEmptyMessage(-1);
461 .setNegativeButton(R.string.delete_neg, null).create().show();
470 protected Handler updateHandler = new Handler() {
472 public void handleMessage(Message msg) {
477 protected void updateList() {
478 if (pubkeydb == null) return;
480 pubkeys = pubkeydb.allPubkeys();
481 PubkeyAdapter adapter = new PubkeyAdapter(this, pubkeys);
483 this.setListAdapter(adapter);
487 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
488 super.onActivityResult(requestCode, resultCode, intent);
490 switch (requestCode) {
491 case REQUEST_CODE_PICK_FILE:
492 if (resultCode == RESULT_OK && intent != null) {
493 Uri uri = intent.getData();
496 readKeyFromFile(new File(URI.create(uri.toString())));
498 String filename = intent.getDataString();
499 if (filename != null)
500 readKeyFromFile(new File(URI.create(filename)));
502 } catch (IllegalArgumentException e) {
503 Log.e(TAG, "Couldn't read from picked file", e);
513 private void readKeyFromFile(File file) {
514 PubkeyBean pubkey = new PubkeyBean();
516 // find the exact file selected
517 pubkey.setNickname(file.getName());
519 if (file.length() > MAX_KEYFILE_SIZE) {
520 Toast.makeText(PubkeyListActivity.this,
521 R.string.pubkey_import_parse_problem,
522 Toast.LENGTH_LONG).show();
526 // parse the actual key once to check if its encrypted
527 // then save original file contents into our database
529 byte[] raw = readRaw(file);
531 String data = new String(raw);
532 if (data.startsWith(PubkeyUtils.PKCS8_START)) {
533 int start = data.indexOf(PubkeyUtils.PKCS8_START) + PubkeyUtils.PKCS8_START.length();
534 int end = data.indexOf(PubkeyUtils.PKCS8_END);
537 char[] encoded = data.substring(start, end - 1).toCharArray();
538 Log.d(TAG, "encoded: " + new String(encoded));
539 byte[] decoded = Base64.decode(encoded);
541 KeyPair kp = PubkeyUtils.recoverKeyPair(decoded);
543 pubkey.setType(kp.getPrivate().getAlgorithm());
544 pubkey.setPrivateKey(kp.getPrivate().getEncoded());
545 pubkey.setPublicKey(kp.getPublic().getEncoded());
547 Log.e(TAG, "Problem parsing PKCS#8 file; corrupt?");
548 Toast.makeText(PubkeyListActivity.this,
549 R.string.pubkey_import_parse_problem,
550 Toast.LENGTH_LONG).show();
553 PEMStructure struct = PEMDecoder.parsePEM(new String(raw).toCharArray());
554 pubkey.setEncrypted(PEMDecoder.isPEMEncrypted(struct));
555 pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED);
556 pubkey.setPrivateKey(raw);
559 // write new value into database
560 if (pubkeydb == null)
561 pubkeydb = new PubkeyDatabase(this);
562 pubkeydb.savePubkey(pubkey);
564 updateHandler.sendEmptyMessage(-1);
565 } catch(Exception e) {
566 Log.e(TAG, "Problem parsing imported private key", e);
567 Toast.makeText(PubkeyListActivity.this, R.string.pubkey_import_parse_problem, Toast.LENGTH_LONG).show();
574 private void pickFileSimple() {
575 // build list of all files in sdcard root
576 final File sdcard = Environment.getExternalStorageDirectory();
577 Log.d(TAG, sdcard.toString());
579 // Don't show a dialog if the SD card is completely absent.
580 final String state = Environment.getExternalStorageState();
581 if (!Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)
582 && !Environment.MEDIA_MOUNTED.equals(state)) {
583 new AlertDialog.Builder(PubkeyListActivity.this)
584 .setMessage(R.string.alert_sdcard_absent)
585 .setNegativeButton(android.R.string.cancel, null).create().show();
589 List<String> names = new LinkedList<String>();
591 File[] files = sdcard.listFiles();
593 for(File file : sdcard.listFiles()) {
594 if(file.isDirectory()) continue;
595 names.add(file.getName());
599 Collections.sort(names);
601 final String[] namesList = names.toArray(new String[] {});
602 Log.d(TAG, names.toString());
604 // prompt user to select any file from the sdcard root
605 new AlertDialog.Builder(PubkeyListActivity.this)
606 .setTitle(R.string.pubkey_list_pick)
607 .setItems(namesList, new OnClickListener() {
608 public void onClick(DialogInterface arg0, int arg1) {
609 String name = namesList[arg1];
611 readKeyFromFile(new File(sdcard, name));
614 .setNegativeButton(android.R.string.cancel, null).create().show();
617 class PubkeyAdapter extends ArrayAdapter<PubkeyBean> {
618 private List<PubkeyBean> pubkeys;
621 public TextView nickname;
622 public TextView caption;
623 public ImageView icon;
626 public PubkeyAdapter(Context context, List<PubkeyBean> pubkeys) {
627 super(context, R.layout.item_pubkey, pubkeys);
629 this.pubkeys = pubkeys;
633 public View getView(int position, View convertView, ViewGroup parent) {
636 if (convertView == null) {
637 convertView = inflater.inflate(R.layout.item_pubkey, null, false);
639 holder = new ViewHolder();
641 holder.nickname = (TextView) convertView.findViewById(android.R.id.text1);
642 holder.caption = (TextView) convertView.findViewById(android.R.id.text2);
643 holder.icon = (ImageView) convertView.findViewById(android.R.id.icon1);
645 convertView.setTag(holder);
647 holder = (ViewHolder) convertView.getTag();
649 PubkeyBean pubkey = pubkeys.get(position);
650 holder.nickname.setText(pubkey.getNickname());
652 boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType());
656 PEMStructure struct = PEMDecoder.parsePEM(new String(pubkey.getPrivateKey()).toCharArray());
657 String type = (struct.pemType == PEMDecoder.PEM_RSA_PRIVATE_KEY) ? "RSA" : "DSA";
658 holder.caption.setText(String.format("%s unknown-bit", type));
659 } catch (IOException e) {
660 Log.e(TAG, "Error decoding IMPORTED public key at " + pubkey.getId(), e);
664 PublicKey pub = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
665 holder.caption.setText(PubkeyUtils.describeKey(pub, pubkey.isEncrypted()));
666 } catch (Exception e) {
667 Log.e(TAG, "Error decoding public key at " + pubkey.getId(), e);
668 holder.caption.setText(R.string.pubkey_unknown_format);
673 holder.icon.setVisibility(View.GONE);
675 holder.icon.setVisibility(View.VISIBLE);
677 if (bound.isKeyLoaded(pubkey.getNickname()))
678 holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true);
680 holder.icon.setImageState(new int[] { }, true);