Java tutorial
/* * 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.schoentoon.connectbot; 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 android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.ServiceConnection; 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.support.v4.app.NavUtils; import android.text.ClipboardManager; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; 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.actionbarsherlock.app.SherlockListActivity; import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.ActionMode.Callback; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.view.MenuItem.OnMenuItemClickListener; import com.schoentoon.connectbot.bean.PubkeyBean; import com.schoentoon.connectbot.service.TerminalManager; import com.schoentoon.connectbot.util.PubkeyDatabase; import com.schoentoon.connectbot.util.PubkeyUtils; 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 SherlockListActivity implements EventListener, OnItemLongClickListener { public final static String TAG = "ConnectBot.PubkeyListActivity"; 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"; 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); getListView().setOnItemLongClickListener(this); getSupportActionBar().setSubtitle(R.string.title_pubkey_list); getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 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); } @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case android.R.id.home: NavUtils.navigateUpTo(this, new Intent(this, HostListActivity.class)); return true; } return super.onMenuItemSelected(featureId, item); } /** * 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.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 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.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 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) { 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).show(); } } 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).show(); 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); } public boolean onItemLongClick(AdapterView<?> l, View v, int position, long id) { final PubkeyBean pubkey = (PubkeyBean) getListAdapter().getItem(position); startActionMode(new PubKeySettings(pubkey)); return true; } private class PubKeySettings implements Callback { public PubKeySettings(PubkeyBean pubkey) { this.pubkey = pubkey; } public boolean onCreateActionMode(final ActionMode mode, Menu menu) { 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); } mode.finish(); 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); mode.finish(); 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 = pubkey.getPublicKey(); String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname()); clipboard.setText(openSSHPubkey); } catch (Exception e) { e.printStackTrace(); } mode.finish(); 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(); } mode.finish(); 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(); mode.finish(); 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); mode.finish(); 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(); mode.finish(); return true; } }); return true; } public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; } public void onDestroyActionMode(ActionMode mode) { } private PubkeyBean pubkey; } 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 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 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 { 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; } } }