#!/usr/bin/env perl # ssh-pass --- cache login password for underlying ssh sessions # Author: Noah Friedman # Created: 2003-02-13 # Public domain. # $Id: ssh-pass,v 1.3 2006/05/24 07:27:19 friedman Exp $ # Commentary: # The ssh-agent program can be used to perform public key authentication # without continually needing to provide a password for every connection. # But some routers and switches don't have any filesystem on which these # keys can be stored, even if they support the ssh protocol for # connections. So you have to enter a login password every time unless # you disable passwords entirely. # This wrapper will store a single password and provide it to any # underlying ssh command invoked which resorts to "keyboard-interactive" # authentication, so that a password need only be entered once. # This wrapper works with OpenSSH. It might work with other # implementations but probably it won't, unless they support the # SSH_ASKPASS environment variable. # Theory of operation: # Under certain circumstances, OpenSSH will use an external helper program # to query the user for a login password when one is required. Normally it # will just request a password using the tty from which the command was # run, but sometimes the process may not have any controlling tty (e.g. if # launched via a gui menu option). If the client has no controlling tty # but the DISPLAY environment variable is set (indicating an X terminal), # openssh will launch this helper program to read a password. You can # control the helper application that is launched with the SSH_ASKPASS # environment variable. You do not necessarily need to run a true X helper # application to query the password, though ssh will not run any program # unless DISPLAY is set. # When this program is launched, it reads a password from the terminal and # then stores the obfuscated result in an environment variable, and # arranges that if ssh invokes a helper application to read a password, it # launches this very program again. Then it launches the application with # arguments specified on the command line. # If this program is run recursively via an ssh client, it will use the # data passed in the environment to supply the password that was previously # cached. # A example session might go something like this: # # $ ssh-pass sh -c 'for host in oc mw gd; do echo -n $host:; ssh $host uptime; done' # Password to use for ssh sessions: # oc: 00:08:33 up 30 days, 9:04, 2 users, load average: 0.10, 0.08, 0.01 # mw: 00:10:49 up 14 days, 2:29, 1 user, load average: 0.23, 0.15, 0.10 # gd: 00:08:23 up 4 days, 4:22, 1 user, load average: 0.03, 0.02, 0.01 # $ # # This is in contrast to the non-wrapped variant: # # $ for host in oc mw gd; do echo -n $host:; ssh $host uptime; done # oc:noah@oc's password: # 00:09:37 up 30 days, 9:05, 2 users, load average: 0.08, 0.07, 0.01 # mw:noah@mw's password: # 00:11:54 up 14 days, 2:30, 1 user, load average: 0.14, 0.13, 0.09 # gd:noah@gd's password: # 00:09:29 up 4 days, 4:23, 1 user, load average: 0.04, 0.03, 0.01 # $ # # Code: $^W = 1; # enable warnings use POSIX qw(:sys_wait_h setsid); use Symbol; use strict; my $progname = $0; my @exit_hook; END { map {&$_} @exit_hook } sub getpass { my $prompt = shift; $prompt = "Password: " unless (defined $prompt); my $tty_restore_fn; if (-t 0) { my $stty_settings = `stty -g`; chop $stty_settings; $tty_restore_fn = sub { system ("stty", $stty_settings) }; push @exit_hook, $tty_restore_fn; system ("stty", "-echo"); } print STDERR $prompt; my $pass = ; $pass =~ s/\r?\n$//; if ($tty_restore_fn) { print STDERR "\n"; &$tty_restore_fn; pop @exit_hook; } return $pass; } sub start { my $pid = fork; die "fork: $!" unless (defined $pid); if ($pid == 0) # child { # dissociate from controlling tty because openssh client will insist # on reading password from controlling tty if it has one. setsid (); local $^W = 0; # turn off duplicate warning from die exec (@_) || die "exec: $_[0]: $!\n\tDied"; } return $pid; } sub exitstat { my ($pid, $nowaitp) = @_; my $result = waitpid ($pid, ($nowaitp? WNOHANG : 0)); return undef if (!defined $result || $result == -1); return WEXITSTATUS ($?) if WIFEXITED ($?); return WTERMSIG ($?) if WIFSIGNALED ($?); return WSTOPSIG ($?) if WIFSTOPPED ($?); return undef; } # These are not meant to be cryptographically secure; they are just meant # to obfuscate sensitive data so they are not discovered accidentally. sub scramble { local $_ = shift; tr/[\x00-\x7f][\x80-\xff]/[\x80-\xff][\x00-\x7f]/; # rot128 $_ = $_ ^ ("\xff" x length ($_)); # invert bits s/(.)/sprintf "%02x", ord($1)/ego; # base16-encode return $_; } sub unscramble { local $_ = shift; s/(..)/chr hex $1/ego; # base16-decode $_ = $_ ^ ("\xff" x length ($_)); # invert bits tr/[\x00-\x7f][\x80-\xff]/[\x80-\xff][\x00-\x7f]/; # rot128 return $_; } sub handle_subcall { # We might be invoked to inquire whether or not to # connect to a host for which we have no stored key; # if that happens, inquire from user. if ($ARGV[0] =~ m|yes/no|o) { print STDERR $ARGV[0]; my $ans = ; print $ans; return; } print unscramble ($ENV{_ssp_data}), "\n"; } sub pkill { my ($sig, $pid) = @_; # subprocess is the session leader via setsid; signal whole session kill ($sig, -$pid); } sub main { unless (@ARGV) { print STDERR "Usage: $0 [command {command args...}]\n"; exit (1); } unless ($progname =~ m|^/|) { use Cwd; my $pwd = getcwd (); $progname =~ s|^|$pwd/|; } # In order to determine whether this script is being invoked by the user # or invoked recursively via ssh to fetch a password, we inspect several # conditions: # * env var is set (containing password) # * ssh_askpass env var is set to this program # * 1 arg (a prompt) is passed from ssh client # * output is not a tty # # If any of these conditions fail, we assume this is a primary invocation # and therefore query user for a password and launch a command. return handle_subcall () if (exists $ENV{_ssp_data} && exists $ENV{SSH_ASKPASS} && $ENV{SSH_ASKPASS} eq $progname && @ARGV == 1 && ! -t 1); # If display is not already set, we must set it now or ssh will not # invoke the askpass program. Since we are performing a non-interactive # response we don't really need a display. We use an invalid display # name to prevent any inadvertent grants of X access on remote hosts. $ENV{DISPLAY} = "none." unless exists $ENV{DISPLAY}; $ENV{SSH_ASKPASS} = $progname; $ENV{_ssp_data} = scramble (getpass ("Password to use for ssh sessions: ")); my $pid = start (@ARGV); my $sighandler = sub { pkill ($_[0], $pid); }; map { $SIG{$_} = $sighandler } qw(HUP INT QUIT TERM TSTP); exit (exitstat ($pid)); } main (); # ssh-pass ends here