/* * 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.connectbot; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import org.connectbot.bean.PubkeyBean; import org.connectbot.util.Ed25519Provider; import org.connectbot.util.EntropyDialog; import org.connectbot.util.EntropyView; import org.connectbot.util.OnEntropyGatheredListener; import org.connectbot.util.OnKeyGeneratedListener; import org.connectbot.util.PubkeyDatabase; import org.connectbot.util.PubkeyUtils; import com.trilead.ssh2.signature.ECDSASHA2Verify; import android.annotation.SuppressLint; import android.app.Dialog; import android.app.ProgressDialog; import android.graphics.PorterDuff; import android.os.Bundle; import android.support.annotation.VisibleForTesting; import android.support.v7.app.AppCompatActivity; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.RadioGroup.OnCheckedChangeListener; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; public class GeneratePubkeyActivity extends AppCompatActivity implements OnEntropyGatheredListener, OnKeyGeneratedListener { static { // Since this class deals with EdDSA keys, we need to make sure this is available. Ed25519Provider.insertIfNeeded(); } public final static String TAG = "CB.GeneratePubkeyAct"; private final static int[] ECDSA_SIZES = ECDSASHA2Verify.getCurveSizes(); private LayoutInflater inflater = null; private EditText nickname; private SeekBar bitsSlider; private EditText bitsText; private CheckBox unlockAtStartup; private CheckBox confirmUse; private Button save; private ProgressDialog progress; private EditText password1, password2; private KeyType keyType; private int bits; private byte[] entropy; private enum KeyType { RSA(PubkeyDatabase.KEY_TYPE_RSA, 1024, 16384, 2048), DSA(PubkeyDatabase.KEY_TYPE_DSA, 1024, 1024, 1024), EC(PubkeyDatabase.KEY_TYPE_EC, ECDSA_SIZES[0], ECDSA_SIZES[ECDSA_SIZES.length - 1], ECDSA_SIZES[0]), ED25519(PubkeyDatabase.KEY_TYPE_ED25519, 256, 256, 256); public final String name; public final int minimumBits; public final int maximumBits; public final int defaultBits; KeyType(String name, int minimumBits, int maximumBits, int defaultBits) { this.name = name; this.minimumBits = minimumBits; this.maximumBits = maximumBits; this.defaultBits = defaultBits; } } private OnKeyGeneratedListener listener; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.act_generatepubkey); nickname = (EditText) findViewById(R.id.nickname); RadioGroup keyTypeGroup = (RadioGroup) findViewById(R.id.key_type); bitsText = (EditText) findViewById(R.id.bits); bitsSlider = (SeekBar) findViewById(R.id.bits_slider); password1 = (EditText) findViewById(R.id.password1); password2 = (EditText) findViewById(R.id.password2); unlockAtStartup = (CheckBox) findViewById(R.id.unlock_at_startup); confirmUse = (CheckBox) findViewById(R.id.confirm_use); save = (Button) findViewById(R.id.save); inflater = LayoutInflater.from(this); nickname.addTextChangedListener(textChecker); password1.addTextChangedListener(textChecker); password2.addTextChangedListener(textChecker); setKeyType(KeyType.RSA); // TODO add BC to provide EC for devices that don't have it. if (Security.getProviders("KeyPairGenerator.EC") == null) { ((RadioButton) findViewById(R.id.ec)).setEnabled(false); } keyTypeGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { public void onCheckedChanged(RadioGroup group, int checkedId) { if (checkedId == R.id.rsa) { setKeyType(KeyType.RSA); } else if (checkedId == R.id.dsa) { setKeyType(KeyType.DSA); } else if (checkedId == R.id.ec) { setKeyType(KeyType.EC); } else if (checkedId == R.id.ed25519) { setKeyType(KeyType.ED25519); } } }); bitsSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) { setBits(keyType.minimumBits + progress); } public void onStartTrackingTouch(SeekBar seekBar) { // We don't care about the start. } public void onStopTrackingTouch(SeekBar seekBar) { setBits(bits); } }); bitsText.setOnFocusChangeListener(new OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { int newBits; try { newBits = Integer.parseInt(bitsText.getText().toString()); } catch (NumberFormatException nfe) { newBits = keyType.defaultBits; } setBits(newBits); } } }); save.setOnClickListener(new OnClickListener() { public void onClick(View view) { GeneratePubkeyActivity.this.save.setEnabled(false); GeneratePubkeyActivity.this.startEntropyGather(); } }); } private void setKeyType(KeyType newKeyType) { keyType = newKeyType; resetBitDefaults(); switch (newKeyType) { case RSA: case EC: setAllowBitStrengthChange(true); break; case DSA: case ED25519: setAllowBitStrengthChange(false); break; default: throw new AssertionError("Impossible key type encountered"); } } private void setAllowBitStrengthChange(boolean enabled) { bitsSlider.setEnabled(enabled); bitsText.setEnabled(enabled); } private void resetBitDefaults() { bitsSlider.setMax(keyType.maximumBits - keyType.minimumBits); setBits(keyType.defaultBits); } private void setBits(int newBits) { if (newBits < keyType.minimumBits || newBits > keyType.maximumBits) { newBits = keyType.defaultBits; } if (keyType == KeyType.EC) { bits = getClosestFieldSize(newBits); } else { // Stay evenly divisible by 8 because it looks nicer to have // 2048 than 2043 bits. bits = newBits - (newBits % 8); } bitsSlider.setProgress(newBits - keyType.minimumBits); bitsText.setText(String.valueOf(bits)); } private void checkEntries() { boolean allowSave = true; if (!password1.getText().toString().equals(password2.getText().toString())) allowSave = false; if (nickname.getText().length() == 0) allowSave = false; if (allowSave) { save.getBackground().setColorFilter(getResources().getColor(R.color.accent), PorterDuff.Mode.SRC_IN); } else { save.getBackground().setColorFilter(null); } save.setEnabled(allowSave); } private void startEntropyGather() { @SuppressLint("InflateParams") // Dialogs do not have a parent view. final View entropyView = inflater.inflate(R.layout.dia_gatherentropy, null, false); ((EntropyView) entropyView.findViewById(R.id.entropy)).addOnEntropyGatheredListener(this); Dialog entropyDialog = new EntropyDialog(this, entropyView); entropyDialog.show(); } @VisibleForTesting void setListener(OnKeyGeneratedListener listener) { this.listener = listener; } 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 < EntropyView.SHA1_MAX_BYTES; i++) numSetBits += measureNumberOfSetBits(this.entropy[i]); double proportionOfBitsSet = numSetBits / (double) (8 * EntropyView.SHA1_MAX_BYTES); Log.d(TAG, "Entropy gathered; population of ones is " + (int) (100.0 * proportionOfBitsSet) + "%"); startKeyGen(); } private static class KeyGeneratorRunnable implements Runnable { private final String keyType; private final int numBits; private final byte[] entropy; private final OnKeyGeneratedListener listener; KeyGeneratorRunnable(String keyType, int numBits, byte[] entropy, OnKeyGeneratedListener listener) { this.keyType = keyType; this.numBits = numBits; this.entropy = entropy; this.listener = listener; } @Override public void run() { SecureRandom random = new SecureRandom(); // Work around JVM bug random.nextInt(); random.setSeed(entropy); try { KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(keyType); keyPairGen.initialize(numBits, random); listener.onGenerationSuccess(keyPairGen.generateKeyPair()); } catch (Exception e) { listener.onGenerationError(e); } } } private void startKeyGen() { progress = new ProgressDialog(GeneratePubkeyActivity.this); progress.setMessage(GeneratePubkeyActivity.this.getResources().getText(R.string.pubkey_generating)); progress.setIndeterminate(true); progress.setCancelable(false); progress.show(); Log.d(TAG, "Starting generation of " + keyType + " of strength " + bits); KeyGeneratorRunnable keyGen = new KeyGeneratorRunnable(keyType.name, bits, entropy, this); Thread keyGenThread = new Thread(keyGen); keyGenThread.setName("KeyGen " + keyType + " " + bits); keyGenThread.start(); } public void onGenerationSuccess(KeyPair pair) { try { boolean encrypted = false; PrivateKey priv = pair.getPrivate(); PublicKey pub = pair.getPublic(); String secret = password1.getText().toString(); if (secret.length() > 0) encrypted = true; //Log.d(TAG, "private: " + PubkeyUtils.formatKey(priv)); Log.d(TAG, "public: " + PubkeyUtils.formatKey(pub)); PubkeyBean pubkey = new PubkeyBean(); pubkey.setNickname(nickname.getText().toString()); pubkey.setType(keyType.name); pubkey.setPrivateKey(PubkeyUtils.getEncodedPrivate(priv, secret)); pubkey.setPublicKey(pub.getEncoded()); pubkey.setEncrypted(encrypted); pubkey.setStartup(unlockAtStartup.isChecked()); pubkey.setConfirmUse(confirmUse.isChecked()); PubkeyDatabase pubkeydb = PubkeyDatabase.get(GeneratePubkeyActivity.this); pubkeydb.savePubkey(pubkey); } catch (Exception e) { Log.e(TAG, "Could not generate key pair"); e.printStackTrace(); } // Chain this up for testing purposes. This is used to implement an IdlingResource if (listener != null) { listener.onGenerationSuccess(pair); } dismissActivity(); } public void onGenerationError(Exception e) { Log.e(TAG, "Could not generate key pair"); e.printStackTrace(); // Chain this up for testing purposes. This is used to implement an IdlingResource if (listener != null) { listener.onGenerationError(e); } dismissActivity(); } private void dismissActivity() { runOnUiThread(new Runnable() { public void run() { progress.dismiss(); GeneratePubkeyActivity.this.finish(); } }); } 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 int getClosestFieldSize(int bits) { int outBits = ECDSA_SIZES[0]; int distance = Math.abs(bits - outBits); for (int i = 1; i < ECDSA_SIZES.length; i++) { int thisDistance = Math.abs(bits - ECDSA_SIZES[i]); if (thisDistance < distance) { distance = thisDistance; outBits = ECDSA_SIZES[i]; } } return outBits; } }