/*
* ConnectBot: simple, powerful, open-source SSH client for Android
* Copyright 2012 Iordan Iordanov
* 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.pubkeygenerator;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import com.iiordanov.pubkeygenerator.EntropyDialog;
import com.iiordanov.pubkeygenerator.EntropyView;
import com.iiordanov.pubkeygenerator.OnEntropyGatheredListener;
import com.iiordanov.pubkeygenerator.PubkeyDatabase;
import com.iiordanov.pubkeygenerator.PubkeyUtils;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Base64;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioGroup;
import android.widget.Toast;
import android.widget.RadioGroup.OnCheckedChangeListener;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.text.ClipboardManager;
public class GeneratePubkeyActivity extends Activity implements OnEntropyGatheredListener {
public final static String TAG = "GeneratePubkeyActivity";
final static int MIN_BITS_RSA = 768;
final static int DEFAULT_BITS_RSA = 2048;
final static int MAX_BITS_RSA = 4096;
final static int MIN_BITS_DSA = 512;
final static int DEFAULT_BITS_DSA = 1024;
final static int MAX_BITS_DSA = 1024;
private LayoutInflater inflater = null;
private RadioGroup keyTypeGroup;
private SeekBar bitsSlider;
private EditText bitsText;
private Button generate;
private Button importKey;
private Button share;
private Button decrypt;
private Button copy;
private Button save;
private Dialog entropyDialog;
private ProgressDialog progress;
private EditText password1;
private EditText file_name;
private String keyType = PubkeyDatabase.KEY_TYPE_RSA;
private int minBits = MIN_BITS_RSA;
private int bits = DEFAULT_BITS_RSA;
private byte[] entropy;
// Variables we use to receive (from calling activity)
// and recover all key-pair related information.
private String passphrase;
private String sshPrivKey;
private String sshPubKey;
private boolean recovered = false;
private KeyPair kp = null;
private String publicKeySSHFormat;
ClipboardManager cm;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.act_generatepubkey);
cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
keyTypeGroup = (RadioGroup) findViewById(R.id.key_type);
bitsText = (EditText) findViewById(R.id.bits);
bitsSlider = (SeekBar) findViewById(R.id.bits_slider);
file_name = (EditText) findViewById(R.id.file_name);
password1 = (EditText) findViewById(R.id.password);
generate = (Button) findViewById(R.id.generate);
share = (Button) findViewById(R.id.share);
decrypt = (Button) findViewById(R.id.decrypt);
copy = (Button) findViewById(R.id.copy);
save = (Button) findViewById(R.id.save);
importKey = (Button) findViewById(R.id.importKey);
inflater = LayoutInflater.from(this);
password1.addTextChangedListener(textChecker);
// Get the private key and passphrase from calling activity if added.
sshPrivKey = getIntent().getStringExtra("PrivateKey");
passphrase = password1.getText().toString();
if (sshPrivKey != null && sshPrivKey.length() != 0) {
decryptAndRecoverKey ();
} else {
Toast.makeText(getBaseContext(), "Key not generated yet. Set parameters and tap 'Generate New Key'.",
Toast.LENGTH_LONG).show();
}
keyTypeGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(RadioGroup group, int checkedId) {
if (checkedId == R.id.rsa) {
minBits = MIN_BITS_RSA;
bitsSlider.setEnabled(true);
bitsSlider.setProgress(DEFAULT_BITS_RSA);
bitsSlider.setMax(MAX_BITS_RSA - minBits);
bitsText.setText(String.valueOf(DEFAULT_BITS_RSA));
bitsText.setEnabled(true);
keyType = PubkeyDatabase.KEY_TYPE_RSA;
} else if (checkedId == R.id.dsa) {
minBits = MIN_BITS_DSA;
bitsSlider.setEnabled(true);
bitsSlider.setProgress(DEFAULT_BITS_DSA);
bitsSlider.setMax(MAX_BITS_DSA - minBits);
bitsText.setText(String.valueOf(DEFAULT_BITS_DSA));
bitsText.setEnabled(true);
keyType = PubkeyDatabase.KEY_TYPE_DSA;
}
}
});
bitsSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromTouch) {
// Stay evenly divisible by 8 because it looks nicer to have
// 2048 than 2043 bits.
int leftover = progress % 8;
int ourProgress = progress;
if (leftover > 0)
ourProgress += 8 - leftover;
bits = minBits + ourProgress;
bitsText.setText(String.valueOf(bits));
}
public void onStartTrackingTouch(SeekBar seekBar) {
// We don't care about the start.
}
public void onStopTrackingTouch(SeekBar seekBar) {
// We don't care about the stop.
}
});
bitsText.setOnFocusChangeListener(new OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
try {
bits = Integer.parseInt(bitsText.getText().toString());
if (bits < minBits) {
bits = minBits;
bitsText.setText(String.valueOf(bits));
}
} catch (NumberFormatException nfe) {
bits = DEFAULT_BITS_RSA;
bitsText.setText(String.valueOf(bits));
}
bitsSlider.setProgress(bits - minBits);
}
}
});
generate.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
hideSoftKeyboard(view);
GeneratePubkeyActivity.this.startEntropyGather();
}
});
decrypt.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
hideSoftKeyboard(view);
decryptAndRecoverKey();
}
});
share.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
hideSoftKeyboard(view);
// This is an UGLY HACK for Blackberry devices which do not transmit the "+" character.
// Remove as soon as the bug is fixed.
String s = android.os.Build.MODEL;
if (s.contains("BlackBerry")) {
Toast.makeText(getBaseContext(), "ERROR: Blackberry devices have problems sharing public keys. " +
"The '+' character is not transmitted. Please save as a file and attach in an email, or " +
"copy to clipboard and paste when connected to the server with a password.",
Toast.LENGTH_LONG).show();
return;
}
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("text/plain");
share.putExtra(Intent.EXTRA_TEXT, publicKeySSHFormat);
startActivity(Intent.createChooser(share, "Share Pubkey"));
}
});
copy.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
hideSoftKeyboard(view);
cm.setText(publicKeySSHFormat);
Toast.makeText(getBaseContext(), "Copied public key in OpenSSH format to clipboard.",
Toast.LENGTH_SHORT).show();
}
});
save.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
hideSoftKeyboard(view);
String fname = file_name.getText().toString();
if (fname.length() == 0) {
Toast.makeText(getBaseContext(), "Please enter file name.",
Toast.LENGTH_SHORT).show();
return;
}
File dir = Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_DOWNLOADS);
File file = new File(dir, fname);
fname = dir.getName() + "/" + fname;
try {
dir.mkdirs();
file.createNewFile();
FileOutputStream fout = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(fout);
writer.append(publicKeySSHFormat);
writer.close();
fout.close();
} catch (IOException e) {
Toast.makeText(getBaseContext(), "Failed to write " + fname,
Toast.LENGTH_LONG).show();
Log.e (TAG, "Failed to output file " + fname);
e.printStackTrace();
return;
}
Toast.makeText(getBaseContext(), "Successfully wrote public key in OpenSSH format to " + fname,
Toast.LENGTH_LONG).show();
}
});
importKey.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
hideSoftKeyboard(view);
String fname = file_name.getText().toString();
if (fname.length() == 0) {
Toast.makeText(getBaseContext(), "Please enter file name (at the bottom) to import PEM formatted " +
"encrypted/unencrypted RSA keys, PKCS#8 unencrypted DSA keys. " +
"Keys generated with 'ssh-keygen -t rsa' are known to work.", Toast.LENGTH_LONG).show();
return;
}
File dir = Environment.getExternalStoragePublicDirectory (Environment.DIRECTORY_DOWNLOADS);
fname = dir.getAbsolutePath() + "/" + fname;
String data = "";
try {
data = readFile(fname);
} catch (IOException e) {
e.printStackTrace();
Log.e (TAG, "Failed to read key from file: " + fname);
Toast.makeText(getBaseContext(), "Failed to read file: " + fname + ". Please ensure it is present " +
"in Download directory.", Toast.LENGTH_LONG).show();
return;
}
try {
passphrase = password1.getText().toString();
KeyPair pair = PubkeyUtils.tryImportingPemAndPkcs8(data, passphrase);
converToBase64AndSendIntent (pair);
} catch (Exception e) {
e.printStackTrace();
Log.e (TAG, "Failed to decode key.");
Toast.makeText(getBaseContext(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(getBaseContext(), "Successfully imported SSH key from file.", Toast.LENGTH_LONG).show();
finish();
}
});
}
/**
* Hides the soft keyboard.
*/
public void hideSoftKeyboard (View view) {
InputMethodManager imm = (InputMethodManager)getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* Decrypts and recovers the key pair, as well as generating public key in OpenSSH format.
* @param view
*/
public boolean decryptAndRecoverKey () {
boolean success = true;
passphrase = password1.getText().toString();
if (!recovered) {
kp = PubkeyUtils.decryptAndRecoverKeyPair(sshPrivKey, passphrase);
if (kp == null) {
success = false;
} else {
try {
publicKeySSHFormat = PubkeyUtils.convertToOpenSSHFormat(kp.getPublic(), null);
} catch (Exception e) {
e.printStackTrace();
success = false;
}
}
if (success)
recovered = true;
}
if (recovered) {
Toast.makeText(getBaseContext(), "Successfully decrypted key.", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getBaseContext(),
"Could not decrypt key. Please enter correct passphrase and try decrypting again.",
Toast.LENGTH_LONG).show();
}
checkEntries();
return success;
}
/**
* Turns buttons on and off depending on state of pubkey.
*/
private void checkEntries() {
if (recovered) {
share.setEnabled(true);
copy.setEnabled(true);
save.setEnabled(true);
decrypt.setEnabled(false);
} else {
share.setEnabled(false);
copy.setEnabled(false);
save.setEnabled(false);
if (sshPrivKey.length() != 0)
decrypt.setEnabled(true);
}
}
private void startEntropyGather() {
final View entropyView = inflater.inflate(R.layout.dia_gatherentropy, null, false);
((EntropyView)entropyView.findViewById(R.id.entropy)).addOnEntropyGatheredListener(GeneratePubkeyActivity.this);
entropyDialog = new EntropyDialog(GeneratePubkeyActivity.this, entropyView);
entropyDialog.show();
}
public void onEntropyGathered(byte[] entropy) {
// For some reason the entropy dialog was aborted, exit activity
if (entropy == null) {
finish();
return;
}
this.entropy = entropy.clone();
int numSetBits = 0;
for (int i = 0; i < 20; i++)
numSetBits += measureNumberOfSetBits(this.entropy[i]);
Log.d(TAG, "Entropy distribution=" + (int)(100.0 * numSetBits / 160.0) + "%");
Log.d(TAG, "entropy gathered; attemping to generate key...");
startKeyGen();
}
private void startKeyGen() {
progress = new ProgressDialog(GeneratePubkeyActivity.this);
progress.setMessage(getResources().getText(R.string.pubkey_generating));
progress.setIndeterminate(true);
progress.setCancelable(false);
progress.show();
Thread keyGenThread = new Thread(mKeyGen);
keyGenThread.setName("KeyGen");
keyGenThread.start();
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
progress.setMessage(getResources().getText(R.string.pubkey_generated));
progress.dismiss();
GeneratePubkeyActivity.this.finish();
}
};
final private Runnable mKeyGen = new Runnable() {
public void run() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(entropy);
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(keyType);
keyPairGen.initialize(bits, random);
KeyPair pair = keyPairGen.generateKeyPair();
converToBase64AndSendIntent (pair);
} catch (Exception e) {
Log.e(TAG, "Could not generate key pair");
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
};
final private TextWatcher textChecker = new TextWatcher() {
public void afterTextChanged(Editable s) {}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {}
public void onTextChanged(CharSequence s, int start, int before,
int count) {
checkEntries();
}
};
private int measureNumberOfSetBits(byte b) {
int numSetBits = 0;
for (int i = 0; i < 8; i++) {
if ((b & 1) == 1)
numSetBits++;
b >>= 1;
}
return numSetBits;
}
private static String readFile(String path) throws IOException {
FileInputStream stream = new FileInputStream(new File(path));
try {
FileChannel fc = stream.getChannel();
MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
/* Instead of using default, pass in a decoder. */
return Charset.defaultCharset().decode(bb).toString();
}
finally {
stream.close();
}
}
/**
* Sets the sshPrivKey and sshPubKey private variables from provided KeyPair.
* @param pair
* @throws Exception
*/
private void converToBase64AndSendIntent (KeyPair pair) throws Exception {
PrivateKey priv = pair.getPrivate();
PublicKey pub = pair.getPublic();
String secret = password1.getText().toString();
Log.d(TAG, "private: " + PubkeyUtils.formatKey(priv));
Log.d(TAG, "public: " + PubkeyUtils.formatKey(pub));
sshPrivKey = Base64.encodeToString(PubkeyUtils.getEncodedPrivate(priv, secret), Base64.DEFAULT);
sshPubKey = Base64.encodeToString(PubkeyUtils.getEncodedPublic(pub), Base64.DEFAULT);
// Send the generated data back to the calling activity.
Intent databackIntent = new Intent();
databackIntent.putExtra("PrivateKey", sshPrivKey);
databackIntent.putExtra("PublicKey", sshPubKey);
setResult(Activity.RESULT_OK, databackIntent);
}
}