Mercurial > 510Connectbot
diff src/com/five_ten_sg/connectbot/PubkeyListActivity.java @ 0:0ce5cc452d02
initial version
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Thu, 22 May 2014 10:41:19 -0700 |
parents | |
children | 265a4733edcb |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/com/five_ten_sg/connectbot/PubkeyListActivity.java Thu May 22 10:41:19 2014 -0700 @@ -0,0 +1,673 @@ +/* + * 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.five_ten_sg.connectbot; + +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.five_ten_sg.connectbot.bean.PubkeyBean; +import com.five_ten_sg.connectbot.service.TerminalManager; +import com.five_ten_sg.connectbot.util.FileChooser; +import com.five_ten_sg.connectbot.util.FileChooserCallback; +import com.five_ten_sg.connectbot.util.PubkeyDatabase; +import com.five_ten_sg.connectbot.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.IBinder; +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()); + updateList(); + } + 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 keybean, String password) { + KeyPair pair = null; + + if (PubkeyDatabase.KEY_TYPE_IMPORTED.equals(keybean.getType())) { + // load specific key using pem format + try { + pair = PEMDecoder.decode(new String(keybean.getPrivateKey()).toCharArray(), password); + } + catch (Exception e) { + String message = getResources().getString(R.string.pubkey_failed_add, keybean.getNickname()); + Log.e(TAG, message, e); + Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG).show(); + } + } + else { + // load using internal generated format + try { + PrivateKey privKey = PubkeyUtils.decodePrivate(keybean.getPrivateKey(), keybean.getType(), password); + PublicKey pubKey = PubkeyUtils.decodePublic(keybean.getPublicKey(), keybean.getType()); + Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey)); + pair = new KeyPair(pubKey, privKey); + } + catch (Exception e) { + String message = getResources().getString(R.string.pubkey_failed_add, keybean.getNickname()); + Log.e(TAG, message, e); + Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG).show(); + return; + } + } + + if (pair == null) return; + + Log.d(TAG, String.format("Unlocked key '%s'", keybean.getNickname())); + // save this key in memory + bound.addKey(keybean, pair, true); + updateList(); + } + + @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()); + updateList(); + } + 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); + updateList(); + 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); + updateList(); + } + } + 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); + updateList(); + return true; + } + }); + MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public); + copyPublicToClipboard.setVisible(!imported); + copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + PublicKey pk = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); + String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname()); + clipboard.setText(openSSHPubkey); + } + catch (Exception e) { + e.printStackTrace(); + } + + 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); + updateList(); + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + return true; + } + }); + } + + 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); + updateList(); + } + 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; + } + } +}