/* * ConnectBot: simple, powerful, open-source SSH client for Android * Copyright 2007 Kenny Root, Jeffrey Sharkey * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.iiordanov.bssh; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.EventListener; import java.util.List; import com.iiordanov.bssh.bean.PubkeyBean; import com.iiordanov.bssh.service.TerminalManager; import com.iiordanov.bssh.util.FileChooser; import com.iiordanov.bssh.util.FileChooserCallback; import com.iiordanov.bssh.util.PubkeyDatabase; import com.iiordanov.bssh.util.PubkeyUtils; import android.app.AlertDialog; import android.app.ListActivity; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.IBinder; 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.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.TableRow; import android.widget.TextView; import android.widget.Toast; import com.trilead.ssh2.crypto.Base64; import com.trilead.ssh2.crypto.PEMDecoder; import com.trilead.ssh2.crypto.PEMStructure; /** * List public keys in database by nickname and describe their properties. Allow users to import, * generate, rename, and delete key pairs. * * @author Kenny Root */ public class PubkeyListActivity extends ListActivity implements EventListener, FileChooserCallback { public final static String TAG = "ConnectBot.PubkeyListActivity"; private static final int MAX_KEYFILE_SIZE = 8192; private static final int KEYTYPE_PUBLIC = 0; private static final int KEYTYPE_PRIVATE = 1; protected PubkeyDatabase pubkeydb; private List<PubkeyBean> pubkeys; protected ClipboardManager clipboard; protected LayoutInflater inflater = null; protected TerminalManager bound = null; private MenuItem onstartToggle = null; private MenuItem confirmUse = null; private ServiceConnection connection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { bound = ((TerminalManager.TerminalBinder) service).getService(); // update our listview binder to find the service updateList(); } public void onServiceDisconnected(ComponentName className) { bound = null; updateList(); } }; @Override public void onStart() { super.onStart(); bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); if(pubkeydb == null) pubkeydb = new PubkeyDatabase(this); } @Override public void onStop() { super.onStop(); unbindService(connection); if(pubkeydb != null) { pubkeydb.close(); pubkeydb = null; } } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.act_pubkeylist); this.setTitle(String.format("%s: %s", getResources().getText(R.string.app_name), getResources().getText(R.string.title_pubkey_list))); // connect with hosts database and populate list pubkeydb = new PubkeyDatabase(this); updateList(); registerForContextMenu(getListView()); getListView().setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> adapter, View view, int position, long id) { PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(position); boolean loaded = bound.isKeyLoaded(pubkey.getNickname()); // handle toggling key in-memory on/off if(loaded) { bound.removeKey(pubkey.getNickname()); updateHandler.sendEmptyMessage(-1); } else { handleAddKey(pubkey); } } }); clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE); inflater = LayoutInflater.from(this); } /** * Read given file into memory as <code>byte[]</code>. */ protected static byte[] readRaw(File file) throws Exception { InputStream is = new FileInputStream(file); ByteArrayOutputStream os = new ByteArrayOutputStream(); int bytesRead; byte[] buffer = new byte[1024]; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } os.flush(); os.close(); is.close(); return os.toByteArray(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuItem generatekey = menu.add(R.string.pubkey_generate); generatekey.setIcon(android.R.drawable.ic_menu_manage); generatekey.setIntent(new Intent(PubkeyListActivity.this, GeneratePubkeyActivity.class)); MenuItem importkey = menu.add(R.string.pubkey_import); importkey.setIcon(android.R.drawable.ic_menu_upload); importkey.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { FileChooser.selectFile(PubkeyListActivity.this, PubkeyListActivity.this, FileChooser.REQUEST_CODE_SELECT_FILE, getString(R.string.file_chooser_select_file,getString(R.string.select_for_key_import))); return true; } }); return true; } protected void handleAddKey(final PubkeyBean pubkey) { if (pubkey.isEncrypted()) { final View view = inflater.inflate(R.layout.dia_password, null); final EditText passwordField = (EditText)view.findViewById(android.R.id.text1); new AlertDialog.Builder(PubkeyListActivity.this) .setView(view) .setPositiveButton(R.string.pubkey_unlock, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { handleAddKey(pubkey, passwordField.getText().toString()); } }) .setNegativeButton(android.R.string.cancel, null).create().show(); } else { handleAddKey(pubkey, null); } } protected void handleAddKey(PubkeyBean pubkey, String password) { Object trileadKey = null; if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) { // load specific key using pem format try { trileadKey = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password); } catch(Exception e) { String message = getResources().getString(R.string.pubkey_failed_add, pubkey.getNickname()); Log.e(TAG, message, e); Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG); } } else { // load using internal generated format PrivateKey privKey = null; PublicKey pubKey = null; try { privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType(), password); pubKey = pubkey.getPublicKey(); } catch (Exception e) { String message = getResources().getString(R.string.pubkey_failed_add, pubkey.getNickname()); Log.e(TAG, message, e); Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG); return; } // convert key to trilead format trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey); Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey)); } if(trileadKey == null) return; Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname())); // save this key in memory bound.addKey(pubkey, trileadKey, true); updateHandler.sendEmptyMessage(-1); } @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; final PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(info.position); menu.setHeaderTitle(pubkey.getNickname()); // TODO: option load/unload key from in-memory list // prompt for password as needed for passworded keys // cant change password or clipboard imported keys 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); load.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { if(loaded) { bound.removeKey(pubkey.getNickname()); updateHandler.sendEmptyMessage(-1); } else { handleAddKey(pubkey); //bound.addKey(nickname, trileadKey); } return true; } }); onstartToggle = menu.add(R.string.pubkey_load_on_start); onstartToggle.setVisible(!pubkey.isEncrypted()); onstartToggle.setCheckable(true); onstartToggle.setChecked(pubkey.isStartup()); onstartToggle.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { // toggle onstart status pubkey.setStartup(!pubkey.isStartup()); pubkeydb.savePubkey(pubkey); updateHandler.sendEmptyMessage(-1); return true; } }); MenuItem changePassword = menu.add(R.string.pubkey_change_password); changePassword.setVisible(!imported); changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final View changePasswordView = inflater.inflate(R.layout.dia_changepassword, null, false); ((TableRow)changePasswordView.findViewById(R.id.old_password_prompt)) .setVisibility(pubkey.isEncrypted() ? View.VISIBLE : View.GONE); new AlertDialog.Builder(PubkeyListActivity.this) .setView(changePasswordView) .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { String oldPassword = ((EditText)changePasswordView.findViewById(R.id.old_password)).getText().toString(); String password1 = ((EditText)changePasswordView.findViewById(R.id.password1)).getText().toString(); String password2 = ((EditText)changePasswordView.findViewById(R.id.password2)).getText().toString(); if (!password1.equals(password2)) { new AlertDialog.Builder(PubkeyListActivity.this) .setMessage(R.string.alert_passwords_do_not_match_msg) .setPositiveButton(android.R.string.ok, null) .create().show(); return; } try { if (!pubkey.changePassword(oldPassword, password1)) new AlertDialog.Builder(PubkeyListActivity.this) .setMessage(R.string.alert_wrong_password_msg) .setPositiveButton(android.R.string.ok, null) .create().show(); else { pubkeydb.savePubkey(pubkey); updateHandler.sendEmptyMessage(-1); } } catch (Exception e) { Log.e(TAG, "Could not change private key password", e); new AlertDialog.Builder(PubkeyListActivity.this) .setMessage(R.string.alert_key_corrupted_msg) .setPositiveButton(android.R.string.ok, null) .create().show(); } } }) .setNegativeButton(android.R.string.cancel, null).create().show(); return true; } }); confirmUse = menu.add(R.string.pubkey_confirm_use); confirmUse.setCheckable(true); confirmUse.setChecked(pubkey.isConfirmUse()); confirmUse.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { // toggle confirm use pubkey.setConfirmUse(!pubkey.isConfirmUse()); pubkeydb.savePubkey(pubkey); updateHandler.sendEmptyMessage(-1); return true; } }); MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public); copyPublicToClipboard.setVisible(!imported); copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { String keyString = PubkeyUtils.getPubkeyString(pubkey); if (keyString != null) clipboard.setText(keyString); return true; } }); MenuItem exportPublic = menu.add(R.string.pubkey_export_public); exportPublic.setVisible(!imported); exportPublic.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { String keyString = PubkeyUtils.getPubkeyString(pubkey); if (keyString != null) saveKeyToFile(keyString, pubkey.getNickname(), KEYTYPE_PUBLIC); return true; } }); MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private); copyPrivateToClipboard.setVisible(!pubkey.isEncrypted() || imported); copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { String keyString = PubkeyUtils.getPrivkeyString(pubkey, null); if (keyString != null) clipboard.setText(keyString); return true; } }); MenuItem exportPrivate = menu.add(R.string.pubkey_export_private); exportPrivate.setVisible(!pubkey.isEncrypted() || imported); exportPrivate.setOnMenuItemClickListener(new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { /* if (pubkey.isEncrypted()) { final View view = inflater.inflate(R.layout.dia_password, null); final EditText passwordField = (EditText)view.findViewById(android.R.id.text1); new AlertDialog.Builder(PubkeyListActivity.this) .setView(view) .setPositiveButton(R.string.pubkey_unlock, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { String keyString = PubkeyUtils.getPrivkeyString(pubkey, passwordField.getText().toString()); if (keyString != null) saveKeyToFile(keyString, pubkey.getNickname(), KEYTYPE_PRIVATE); } }) .setNegativeButton(android.R.string.cancel, null).create().show(); } else { */ String keyString = PubkeyUtils.getPrivkeyString(pubkey, null); if (keyString != null) saveKeyToFile(keyString, pubkey.getNickname(), KEYTYPE_PRIVATE); // } return true; } }); 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, pubkey.getNickname())) .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // dont forget to remove from in-memory if(loaded) bound.removeKey(pubkey.getNickname()); // delete from backend database and update gui pubkeydb.deletePubkey(pubkey); updateHandler.sendEmptyMessage(-1); } }) .setNegativeButton(R.string.delete_neg, null).create().show(); return true; } }); } protected Handler updateHandler = new Handler() { @Override public void handleMessage(Message msg) { updateList(); } }; protected void updateList() { if (pubkeydb == null) return; pubkeys = pubkeydb.allPubkeys(); PubkeyAdapter adapter = new PubkeyAdapter(this, pubkeys); this.setListAdapter(adapter); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); switch (requestCode) { case FileChooser.REQUEST_CODE_SELECT_FILE: if (resultCode == RESULT_OK && intent != null) { File file = FileChooser.getSelectedFile(intent); if (file != null) readKeyFromFile(file); } break; } } /** * @param name */ private void readKeyFromFile(File file) { PubkeyBean pubkey = new PubkeyBean(); // find the exact file selected pubkey.setNickname(file.getName()); if (file.length() > MAX_KEYFILE_SIZE) { Toast.makeText(PubkeyListActivity.this, R.string.pubkey_import_parse_problem, Toast.LENGTH_LONG).show(); return; } // parse the actual key once to check if its encrypted // then save original file contents into our database try { byte[] raw = readRaw(file); 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 if (pubkeydb == null) pubkeydb = new PubkeyDatabase(this); 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(); } } private void saveKeyToFile(final String keyString, final String nickName, int keyType) { final int titleId, messageId, successId, errorId; final String errorString; if (keyType == KEYTYPE_PRIVATE) { titleId = R.string.pubkey_private_save_as; messageId = R.string.pubkey_private_save_as_desc; successId = R.string.pubkey_private_export_success; errorId = R.string.pubkey_private_export_problem; errorString = "Error exporting private key"; } else { titleId = R.string.pubkey_public_save_as; messageId = R.string.pubkey_public_save_as_desc; errorId = R.string.pubkey_public_export_problem; successId = R.string.pubkey_public_export_success; errorString = "Error exporting public key"; } final String sdcard = Environment.getExternalStorageDirectory().toString(); final EditText fileName = new EditText(PubkeyListActivity.this); fileName.setSingleLine(); if (nickName != null) { if (keyType == KEYTYPE_PRIVATE) fileName.setText(sdcard + "/" + nickName.trim()); else fileName.setText(sdcard + "/" + nickName.trim() + ".pub"); } new AlertDialog.Builder(PubkeyListActivity.this) .setTitle(titleId) .setMessage(messageId) .setView(fileName) .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { File keyFile = new File(fileName.getText().toString()); if (!keyFile.exists()) { try { keyFile.createNewFile(); } catch (IOException e) { Log.e(TAG, errorString); Toast.makeText(PubkeyListActivity.this, errorId, Toast.LENGTH_LONG).show(); return; } } FileOutputStream fout = null; try { fout = new FileOutputStream(keyFile); fout.write(keyString.getBytes(),0,keyString.getBytes().length); fout.flush(); } catch (Exception e) { Log.e(TAG, errorString); Toast.makeText(PubkeyListActivity.this, errorId, Toast.LENGTH_LONG).show(); return; } Toast.makeText(PubkeyListActivity.this, getResources().getString(successId,keyFile.getPath().toString()), Toast.LENGTH_LONG).show(); } }).setNegativeButton(android.R.string.cancel, null).create().show(); } public void fileSelected(File f) { Log.d(TAG, "File chooser returned " + f); readKeyFromFile(f); } class PubkeyAdapter extends ArrayAdapter<PubkeyBean> { private List<PubkeyBean> pubkeys; class ViewHolder { public TextView nickname; public TextView caption; public ImageView icon; } public PubkeyAdapter(Context context, List<PubkeyBean> pubkeys) { super(context, R.layout.item_pubkey, pubkeys); this.pubkeys = pubkeys; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.item_pubkey, null, false); holder = new ViewHolder(); holder.nickname = (TextView) convertView.findViewById(android.R.id.text1); holder.caption = (TextView) convertView.findViewById(android.R.id.text2); holder.icon = (ImageView) convertView.findViewById(android.R.id.icon1); convertView.setTag(holder); } else holder = (ViewHolder) convertView.getTag(); PubkeyBean pubkey = pubkeys.get(position); holder.nickname.setText(pubkey.getNickname()); boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); if (imported) { try { PEMStructure struct = PEMDecoder.parsePEM(new String(pubkey.getPrivateKey()).toCharArray()); String type = (struct.pemType == PEMDecoder.PEM_RSA_PRIVATE_KEY) ? "RSA" : "DSA"; holder.caption.setText(String.format("%s unknown-bit", type)); } catch (IOException e) { Log.e(TAG, "Error decoding IMPORTED public key at " + pubkey.getId(), e); } } else { try { holder.caption.setText(pubkey.getDescription()); } catch (Exception e) { Log.e(TAG, "Error decoding public key at " + pubkey.getId(), e); holder.caption.setText(R.string.pubkey_unknown_format); } } if (bound == null) { holder.icon.setVisibility(View.GONE); } else { holder.icon.setVisibility(View.VISIBLE); if (bound.isKeyLoaded(pubkey.getNickname())) holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true); else holder.icon.setImageState(new int[] { }, true); } return convertView; } } }