Thursday, May 8, 2008

SSH key-based authentication in Java

Yet again the next project of the week was the intersection of work and play (kinda). There was the upcoming project where I would need to exercise remote control using SSH. And there was the next program I was toying with: a nice lightweight commander clone written in Java. What's the common theme here? Well, muCommander has several virtual filesystems, including one for SFTP (and if you didn't know it before, that's a file transfer protocol for SSH).

But muCommander didn't use public key authentication, and I have disabled password-based authentication on my home SSH server. So there was the opportunity to play around with J2SSH (which muCommander was using) and both implement key-based authentication in my current file manager and explore one of the libraries for my next project. OK, maybe not applicable for my work project, because J2SSH is GPL, but I can get to play, OK?

With license questions out of the way, we can get down to business. And it is not difficult at all, as the j2ssh/examples directory contains a You just need to instantiate a com.sshtools.j2ssh.SshClient, call connect, passing the hostname as a String and authenticate using the method... which was it... ah yes authenticate, passing a com.sshtools.j2ssh.authentication.SshAuthenticationClient.

Now getting this SshAuthenticationClient is the fun part. In our case, we are using the com.sshtools.j2ssh.authentication.PublicKeyAuthenticationClient and we need several more steps to prep it up. After instantiating it, we need to call setUsername with a String (can't tell you what it stands for). Then we need to set the private key itself using the setKey method and we're ready.

What? setKey expects a parameter? Well yes it does, and it's another of those j2ssh classes: com.sshtools.j2ssh.transport.publickey.SshPrivateKey. But we seem to be stuck here, as you can't instantiate SshPrivateKey- it's abstract. What does this mean? Argh, never mind, you can't instantiate it. Try it. See? Told ya so.

So then we see there's this similarly named com.sshtools.j2ssh.transport.publickey.SshPrivateKeyFile. Might it have something to do with our SshPrivateKey? Yes, as it turns out, it has a method toPrivateKey (takes a passphrase as a String- you do use passphrases, don't you?), and returns our most wanted SshPrivateKey. What's that you're saying? You can't instantiate SshPrivateKeyFile either? Fear not, because it has a static method parse, which overloaded to accept both a File and a byte array with the actual private key file or data.

If you couldn't follow this convoluted line of thought (I couldn't), here's the code, as taken from the patch of muCommander:

PublicKeyAuthenticationClient pk = new PublicKeyAuthenticationClient();

SshPrivateKey key = null;
// Throw an AuthException if problems with private key file
try {
SshPrivateKeyFile file = SshPrivateKeyFile.parse(new File(privateKeyPath));
key = file.toPrivateKey(credentials.getPassword());
} catch (InvalidSshKeyException iske) {
throw new AuthException(realm, "Invalid private key file or passphrase"); // Todo: localize this entry
} catch (IOException ioe) {
throw new AuthException(realm, "Error reading private key file"); // Todo: localize this entry


Now if sshClient.authenticate returns AuthenticationProtocolState.COMPLETE, you're ready to go. You can open a shell or an SftpClient. Mission accomplished.

If you had the urge to download the muCommander source code and apply this patch, no need to: it's already implemented in CVS.