/*
* 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 org.woltage.irssiconnectbot;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collections;
import java.util.EventListener;
import java.util.LinkedList;
import java.util.List;
import org.openintents.intents.FileManagerIntents;
import org.woltage.irssiconnectbot.bean.PubkeyBean;
import org.woltage.irssiconnectbot.service.TerminalManager;
import org.woltage.irssiconnectbot.util.KeyUtils;
import org.woltage.irssiconnectbot.util.PubkeyDatabase;
import org.woltage.irssiconnectbot.util.PubkeyUtils;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.DialogInterface.OnClickListener;
import android.net.Uri;
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.View;
import android.view.ViewGroup;
import android.view.MenuItem.OnMenuItemClickListener;
import android.widget.AdapterView;
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 android.widget.AdapterView.OnItemClickListener;
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 {
public final static String TAG = "ConnectBot.PubkeyListActivity";
public static final String PICK_MODE = "pickmode";
private static final int MAX_KEYFILE_SIZE = 8192;
private static final int REQUEST_CODE_PICK_FILE = 1;
// Constants for AndExplorer's file picking intent
private static final String ANDEXPLORER_TITLE = "explorer_title";
private static final String MIME_TYPE_ANDEXPLORER_FILE = "vnd.android.cursor.dir/lysesoft.andexplorer.file";
public static final String PICKED_PUBKEY_ID = "pubkey_id";
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());
final boolean pickMode = getIntent().getBooleanExtra( PICK_MODE, false );
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) {
if (pickMode) {
Intent intent = new Intent();
intent.putExtra(PICKED_PUBKEY_ID, pubkey.getId());
setResult(RESULT_OK, intent);
finish();
} else {
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) {
Uri sdcard = Uri.fromFile(Environment.getExternalStorageDirectory());
String pickerTitle = getString(R.string.pubkey_list_pick);
// Try to use OpenIntent's file browser to pick a file
Intent intent = new Intent(FileManagerIntents.ACTION_PICK_FILE);
intent.setData(sdcard);
intent.putExtra(FileManagerIntents.EXTRA_TITLE, pickerTitle);
intent.putExtra(FileManagerIntents.EXTRA_BUTTON_TEXT, getString(android.R.string.ok));
try {
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
} catch (ActivityNotFoundException e) {
// If OI didn't work, try AndExplorer
intent = new Intent(Intent.ACTION_PICK);
intent.setDataAndType(sdcard, MIME_TYPE_ANDEXPLORER_FILE);
intent.putExtra(ANDEXPLORER_TITLE, pickerTitle);
try {
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
} catch (ActivityNotFoundException e1) {
pickFileSimple();
}
}
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) {
try {
Object trileadKey = KeyUtils.DecodeKey(pubkey, password);
Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname()));
// save this key in memory
bound.addKey(pubkey, trileadKey, true);
updateHandler.sendEmptyMessage(-1);
} 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).show();
}
}
@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.setEnabled(!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 copyPublicToClipboard = menu.add(R.string.pubkey_copy_public);
copyPublicToClipboard.setEnabled(!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 copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private);
copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported);
copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
try {
String data = null;
if (imported)
data = new String(pubkey.getPrivateKey());
else {
PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType());
data = PubkeyUtils.exportPEM(pk, null);
}
clipboard.setText(data);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
});
MenuItem changePassword = menu.add(R.string.pubkey_change_password);
changePassword.setEnabled(!imported);
changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() {
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 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 REQUEST_CODE_PICK_FILE:
if (resultCode == RESULT_OK && intent != null) {
Uri uri = intent.getData();
try {
if (uri != null) {
readKeyFromFile(new File(URI.create(uri.toString())));
} else {
String filename = intent.getDataString();
if (filename != null)
readKeyFromFile(new File(URI.create(filename)));
}
} catch (IllegalArgumentException e) {
Log.e(TAG, "Couldn't read from picked file", e);
}
}
break;
}
}
/**
* @param file
*/
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 pickFileSimple() {
// build list of all files in sdcard root
final File sdcard = Environment.getExternalStorageDirectory();
Log.d(TAG, sdcard.toString());
// Don't show a dialog if the SD card is completely absent.
final String state = Environment.getExternalStorageState();
if (!Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)
&& !Environment.MEDIA_MOUNTED.equals(state)) {
new AlertDialog.Builder(PubkeyListActivity.this)
.setMessage(R.string.alert_sdcard_absent)
.setNegativeButton(android.R.string.cancel, null).create().show();
return;
}
List<String> names = new LinkedList<String>();
{
File[] files = sdcard.listFiles();
if (files != null) {
for(File file : sdcard.listFiles()) {
if(file.isDirectory()) continue;
names.add(file.getName());
}
}
}
Collections.sort(names);
final String[] namesList = names.toArray(new String[] {});
Log.d(TAG, names.toString());
// prompt user to select any file from the sdcard root
new AlertDialog.Builder(PubkeyListActivity.this)
.setTitle(R.string.pubkey_list_pick)
.setItems(namesList, new OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
String name = namesList[arg1];
readKeyFromFile(new File(sdcard, name));
}
})
.setNegativeButton(android.R.string.cancel, null).create().show();
}
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 {
PublicKey pub = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
holder.caption.setText(PubkeyUtils.describeKey(pub, pubkey.isEncrypted()));
} 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;
}
}
}