javaandroidsecuritysshjsch

How to let user confirm host fingerprint with JSch?


In an Android app, I am attempting to connect to an SSH server using the JSch library. The remote server is specified by the user, so I don't know the remote fingerprint in advance. At the same time I don't want to set StrictHostKeyChecking to no as I see in so many examples.

I'd like to get the remote server fingerprint, show it to the user for acceptance. Is this possible either with JSch or regular Java, perhaps with sockets?

Here's an example you can try, just paste it in the onCreate of an Android activity:

new Thread(new Runnable() {
    @Override
    public void run() {

        com.jcraft.jsch.Session session;
        JSch jsch;
        try {
            jsch = new JSch();
            jsch.setLogger(new MyLogger());

            session = jsch.getSession("git", "github.com", 22);
            session.setPassword("hunter2");

            Properties prop = new Properties();
            prop.put("StrictHostKeyChecking", "yes");
            session.setConfig(prop);

            //**Get a host key and show it to the user**

            session.connect();  // reject HostKey: github.com
        }
        catch (Exception e){
            LOG.error("Could not JSCH", e);
        }
    }
}).start();

Solution

  • OK I've found a way to do this. It may not be the best way but it is a way. Using the UserInfo.promptYesNo required looping at the expense of CPU while waiting for user response or with the overhead of an Executor/FutureTask/BlockingQueue. Instead the async thread which executes the connection (since network tasks cannot occur on UI thread) is more conducive to doing this twice - once to 'break' and get the user to accept, second to succeed. I guess this is the 'Android way'. For this, the hostkey needs storing somewhere. Suppose I store it in Android's PreferenceManager, then to start with grab the key from there, defaulting to empty if not available

    String keystring = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("target_hostkey","");
    
    if(!Strings.isNullOrEmpty(keystring)){
        byte[] key = Base64.decode ( keystring, Base64.DEFAULT );
        jsch.getHostKeyRepository().add(new HostKey("github.com", key ), null);
    }
    

    Next, proceed as usual to connect to the server

    session = jsch.getSession("git", "github.com", 22);
    session.setPassword("hunter2");
    
    Properties prop = new Properties();
    prop.put("StrictHostKeyChecking", "yes");
    session.setConfig(prop);
    session.connect(); 
    

    But this time, catch the JSchException. In there, the session has a HostKey available.

    catch(final JSchException jex){
    
        LOG.debug(session.getHostKey().getKey());
        final com.jcraft.jsch.Session finalSession = session;
    
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                new MaterialDialog.Builder(MyActivity.this)
                        .title("Accept this host with fingerprint?")
                        .negativeText(R.string.cancel)
                        .positiveText(R.string.ok)
                        .content(finalSession.getHostKey().getFingerPrint(jsch))
                        .onPositive(new MaterialDialog.SingleButtonCallback() {
                            @Override
                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                                PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit().putString("target_hostkey", finalSession.getHostKey().getKey()).apply();
                            }
                        }).show();
            }
        });
    
    }
    

    After this, it's a matter of re-invoking the Thread or AsyncTask but this time the hostkey is added to the hostkey repository for JSch.