The Turnstone's Bill

Wrapping Rsync or SSH in an NSTask

One of the most useful features of DropSync is its ability to sync with remote computers. DropSync achieves this by calling rsync tasks behing the scenes, so it inherits this ability to sync remotely almost for free. It’s not entirely free though, because getting rsync to authenticate with remote machines when it is run as an NSTask isn’t so trivial. In this blog post I’ll outline how I achieved this. You can download a small demo app here which shows how to authenticate ssh when it is run as an NSTask.

Since rsync uses ssh for authentication (by default), the problem to be solved is essentially how to supply a password to ssh so that users don’t need setup public/private keys (a painful experience for those unfamiliar with the command-line). In this post, I’ll run through the details of how this is done in DropSync. Note that since there are lots of good tutorials on the basics of wrapping command-line utilities using NSTask I won’t go over that again. If you’re not familiar with NSTask, take a look (eg here) before reading further.

Background

Rsync is the name of both a command-line utility and an algorithm for rapidly updating (synchronizing) files over a network. The rsync program, which gets its name from the algorithm it implements, is an incredibly flexible tool with huge numbers of options. Its basic usage is pretty simple though. For example;

1
rsync  ~/iracooke/Documents iracooke@mudflatsoftware.com:~/backups/

would sync the Documents folder on my local machine up to a directory called backups on my server (note that although I’ve used a “backups” example many shared hosting providers explicitly forbid using their services as backup storage so check with your provider before doing this). When rsync is used to perform a sync to or from a remote host machine it will authenticate using SSH (this can be changed but is the default and most common setting). SSH clients authenticate in many possible ways, and the method used depends on the availability of keypairs and several environment variables. The three most commonly supported types of authentication are;

  1. Public key authentication: If the local and remote host can exchange public keys then authentication proceeds on the basis of the password that was setup with the key pair. If this password is blank then SSH will authenticate without any further interaction from the user. On Mac OSX after leopard, if the password is not blank a system-wide ssh-askpass program will be invoked to ask for the password (or obtain it from the keychain).
  2. Password based authentication with password supplied via a tty: If no matching keys are found the ssh program obtains a password from standard input (provided that standard input is a tty). In essence this means the user needs to supply the password on the command-line
  3. Password based authentication with password supplied via an Askpass program: If standard input is set to null and an ASKPASS environment variable is set, ssh executes the ASKPASS program to obtain the password

Note: Some ssh clients, including the openSSH SSH client shipped with snow leopard also support GSSAPI/Kerberos based authentication

Since the goal is to wrap rsync inside an NSTask, authentication must proceed via methods 1 or 3. Over-riding the standard input pipe of an NSTask isn’t an option because standard input must be a tty.

From the developers perspective, a simple option is to rely on authentication method 1. This has the advantage of being easy to implement (it requires no work on the part of the programmer), but it requires considerable command-line expertise from the user, who will need to generate key pairs and place them in the appropriate places on local and remote machines.

A better alternative is to setup NSTasks so that SSH will be able to use option (3) above. This allows authentication to proceed in a way that most users are familiar with, ie, enter a username/password combination into a configuration box. This is the method I’ll describe below. It involves two steps, (1) setting up the NSTask so that it will invoke a custom program to supply a password, and (2) writing the actual program that supplies the password.

Setting up the NSTask

Our NSTask will act as a wrapper for a command-line program that invokes the ssh client. This could be the ssh client program itself, or rsync (which in turn invokes ssh). For simplicity in my sample code I’ll just use ssh directly, but the same applies if you’re using rsync. All the code for this is contained in SSHNSTaskAppDelegate.m under the run method.

The first part of setting up the NSTask is assigning input and output pipes. The only unusual thing here is that we set the standard input pipe to Null. This is sometimes required in order to get SSH to use our Askpass program (see the man page for SSH).

1
[taskObject setStandardInput:[NSFileHandle fileHandleWithNullDevice]];

The next part of the program gets the path for the Askpass executable, which is included as part of the application’s main bundle. (In order to do this, find the executable under “Products” in xcode, and then drag it into “Copy Bundle Resources” on the main application’s target.)

1
2
3
4
5
// Get the path of the Askpass program, which is
// setup to be included as part of the main application bundle
NSString *askPassPath = [NSBundle pathForResource:@"Askpass"
              ofType:@"" 
              inDirectory:[[NSBundle mainBundle] bundlePath]];

Next a set of environment variables is created for the NSTask. The variables SSH_ASKPASS and DISPLAY are required to get ssh to use our askpass program. The AUTH_USERNAME and AUTH_HOSTNAME variables are used to communicate the username and hostname to the askpass program so that it can attempt to find the password in the keychain.

1
2
3
4
5
6
7
8
9
10
// This creates a dictionary of environment variables (keys) and
// their values (objects) to be set in the environment where the
// task will be run. This environment dictionary will then be
// accessible to our Askpass program.
NSDictionary *env = [NSDictionary dictionaryWithObjectsAndKeys:
                  @"NONE", @"DISPLAY",
                  askPassPath, @"SSH_ASKPASS",
                  [self userName],@"AUTH_USERNAME",
                  [self hostName],@"AUTH_HOSTNAME",
                  nil];

The remaining steps for setup and launch are pretty standard. Just set up the arguments to the task, then set the environment variables and launch path.

Writing the askpass program

The Askpass program performs the following steps

  1. Gets the username and hostname environment variables that were set on the invoking NSTask
  2. Figures out whether ssh needs a password, or if it is asking us to accept a host identity (in which case we should reply “yes”)
  3. Looks for a password in the keychain and if it is not available prompt the user for it.

The first step is accomplished by taking advantage of the fact that the Askpass program is invoked in our NSTask’s environment. As a result, the username and hostname are available as environment variables.

1
2
3
NSDictionary *dict = [[NSProcessInfo processInfo] environment];
NSString *usernameString = [dict valueForKey:@"AUTH_USERNAME"];
NSString *hostnameString = [dict valueForKey:@"AUTH_HOSTNAME"];

When ssh asks for a password it prompts with some text like “Password:”, or if it requires the user to accept a host identity it prompts with a “yes/no”. This prompting text is available in the argument list of the Askpass program. We access it via;

[[[NSProcessInfo processInfo] arguments] objectAtIndex:1];

By examining this text it is possible to respond with “yes” or a password as appropriate.

The last step checks to see if the password can be obtained from the user’s keychain and then if it isn’t available it prompts the user. The main code for doing this is contained in two class methods of PasswordHelper. I won’t delve into how this is done because it’s a bit off topic (see apple’s docs on keychain services).

Conclusion

Authenticating ssh using an ASKPASS program allows ssh (and programs like rsync) to be run as NSTask’s with minimal hassle by the user. In my opinion, this is a better option than asking the user to create key-pairs.

The main shortcoming of this method is that it is difficult to communicate all the relevant information between the main application and the ASKPASS helper program. As a result, some aspects of the authentication are not ideal. For example, if the user supplies an incorrect password the program will fail to authenticate rather than asking the user a second time.

Update

Since writing this post I’ve realised that I omitted an important additional step in setting up the environment variables for the NSTask. In order to make passwordless key-based authentication work it’s necessary to grab the SSH_AUTH_SOCK variable from the user’s environment and include this in the NSTask’s environment. So, when setting environment variables for example;

1
2
3
4
5
6
7
8
9
10
11
12
13
NSTask *task;
NSDictionary *environmentDict = [[NSProcessInfo processInfo] environment];
// Environment variables needed for password based authentication 
NSMutableDictionary *env = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                       @"NONE", @"DISPLAY",                           askPassPath, @"SSH_ASKPASS",
                       userName,@"AUTH_USERNAME",
                       hostName,@"AUTH_HOSTNAME",
                       nil];

// Environment variable needed for key based authentication
[env setObject:(environmentDict objectForKey:@"SSH_AUTH_SOCK"] forKey:@"SSH_AUTH_SOCK"];
// Setting the task's environment
[task setEnvironment:env];