1#! /usr/local/bin/python3.8 2# 3# ssh-multiadd - add multiple ssh keys, maybe some with the same passphrase 4# Copyright (C) 2001-2002 Matthew Mueller <donut@azstarnet.com> 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 20import os,errno,pty,sys,getopt,re,commands 21 22def pathfind(pl, path=os.environ.get('PATH',os.defpath).split(os.pathsep), notfound=None, pathadd=[]): 23 if type(pl)==type(''): pl = (pl,) 24 for a in [os.path.join(d,p) for p in pl for d in path+pathadd]: 25 if os.path.exists(a): 26 return a 27 # if we don't find it, return with no explicit path so that we get a useful 28 # "file not found" error rather than some "bad operand for +" if we 29 # returned (and tried to use) None 30 return notfound is None and pl[0] or notfound 31 32# default configuration: 33keys = ('identity', 'id_dsa') 34ssh = pathfind('ssh') 35sshadd = pathfind('ssh-add') 36sshaskpass = os.environ.get('SSH_ASKPASS', pathfind(('ssh-askpass','x11-ssh-askpass','ssh-askpass2','ssh-askpass1','gnome-ssh-askpass'),pathadd=[os.path.join(os.sep,'usr','lib','ssh')])) 37sshdir = pathfind(('.ssh','.ssh2'), [os.path.expanduser('~')], '') 38useaskpass = 'auto' 39verbose = 0 40listidentities = 0 41sshgetfingerprint = -1 42 43# user configuration: 44conffile = os.path.join(os.path.expanduser('~'),'.ssh-multiadd.rc.py') 45if os.path.exists(conffile): 46 execfile(conffile) 47 48def p(s,nl=1,outf=sys.stdout): 49 outf.write(s) 50 if nl: outf.write('\n') 51def pverbose(s,nl=1): 52 if verbose>0: p(s,nl) 53def pinfo(s,nl=1): 54 if verbose>=0: p(s,nl) 55def perror(s,nl=1): 56 if verbose>=-1: p(s,nl,outf=sys.stderr) 57 58 59def ptyopen(blah,args): 60 "kinda like popen..." 61 pverbose('ptyopen: '+blah+' '+str(args)) 62 pid,fd=pty.fork() 63 if pid==0: 64 os.execv(blah,[blah]+args) 65 else: 66 return fd 67def writen(fd, data): 68 "Write all the data to a descriptor." 69 while data != '': 70 n = os.write(fd, data) 71 data = data[n:] 72 73def exitstatus_str(st): 74 if os.WIFSTOPPED(st): 75 return 'stopped: sig %i'%os.WSTOPSIG(st) 76 if os.WIFSIGNALED(st): 77 return 'killed: sig %i'%os.WTERMSIG(st) 78 if os.WIFEXITED(st): 79 return 'exit status: %i'%os.WEXITSTATUS(st) 80 return '(??: %i)'%st 81 82def getoutput(cmd): 83 pverbose('running %r...'%cmd) 84 st,p=commands.getstatusoutput(cmd) 85 if not os.WIFEXITED(st): 86 perror('%r %s (%s)'%(cmd,exitstatus_str(st),p)) 87 sys.exit(1) 88 return p 89 90def getoutput_checkstatus(cmd): 91 pverbose('running %r...'%cmd) 92 st,p=commands.getstatusoutput(cmd) 93 if st: 94 perror('%r %s (%s)'%(cmd,exitstatus_str(st),p)) 95 sys.exit(1) 96 return p 97 98def askpass(prompt=''): 99 p='' 100 while p=='': 101 if useaskpass=='yes' or (useaskpass=='auto' and not sys.stdin.isatty()): 102 p=getoutput_checkstatus(sshaskpass + (prompt and ' '+`prompt` or '')) 103 else: 104 import getpass 105 p=getpass.getpass(prompt or 'enter pass: ') 106 return p 107 108version='1.3.2' 109 110def printusage(err=0): 111 def isdef(c): return c and ' (default)' or '' 112 perror('Usage: ssh-multiadd [opts] [keyfiles (%s)]'%', '.join(keys)) 113 perror(' -a <a> use ssh-askpass (auto)') 114 perror(' -A <p> ssh-askpass (%s)'%sshaskpass) 115 perror(' -s <p> ssh-add (%s)'%sshadd) 116 perror(' -S <p> ssh (%s)'%ssh) 117 perror(' -d <d> dir to look for keyfiles (%s)'%sshdir) 118 perror(' -f force all keys to be added'+isdef(sshgetfingerprint==None)) 119 perror(' -l lists all identities represented by the agent when done'+isdef(listidentities==1)) 120 perror(' -L lists differences in represented identities when done'+isdef(listidentities==2)) 121 perror(" --nolist disable -l/-L"+isdef(listidentities==0)) 122 perror(' -h/--help show help') 123 perror(' --version show version') 124 sys.exit(err) 125def printhelp(): 126 perror('ssh-multiadd v%s - Copyright (C) 2001-2002 Matthew Mueller - GPL license'%version) 127 printusage() 128 129def main(argv): 130 try: 131 optlist, args = getopt.getopt(argv, 'a:A:s:d:S:flLh?', ['help','version','debug','nolist']) 132 except getopt.error, a: 133 perror("ssh-multiadd: %s"%a) 134 printusage(1) 135 136 global useaskpass,sshaskpass,sshadd,sshdir,verbose,listidentities,keys,sshgetfingerprint,ssh 137 #prevopt='' 138 for o,a in optlist: 139 if o=='-a': 140 if a in ('yes','no','auto'): 141 useaskpass=a 142 else: 143 perror("invalid -a arg '%s'"%a) 144 printusage(1) 145 elif o=='-A': 146 sshaskpass=a 147 elif o=='-s': 148 sshadd=a 149 elif o=='-d': 150 sshdir=a 151 elif o=='-S': 152 ssh=a 153 elif o=='-f': 154 sshgetfingerprint=None 155 elif o=='-l': 156 listidentities=1 157 elif o=='-L': 158 listidentities=2 159 elif o=='--nolist': 160 listidentities=0 161 elif o=='--debug': 162 verbose=1 163 elif o=='-h' or o=='-?' or o=='--help': 164 printhelp() 165 elif o=='--version': 166 print version 167 sys.exit(0) 168 169 keys = [os.path.join(sshdir,d) for d in args or keys] 170 171 if sshgetfingerprint == -1: 172 sver = getoutput_checkstatus(ssh + ' -V').splitlines()[0] 173 if sver.find('OpenSSH')>=0: 174 sdir = os.path.split(ssh)[0] 175 sshgetfingerprint = (os.path.join(sdir,'ssh-keygen -l -f "%s"'), '(\S+\s+\S+)') 176 elif sver.find('SSH Version 1')>=0: 177 sshgetfingerprint = (pathfind('cat')+' "%s.pub"', '(\S+\s+\S+\s+\S+)') 178 elif sver.find('SSH Version 2')>=0: 179 sshgetfingerprint = (pathfind('basename')+' "%s"', '(.+)') #kludge since sshv2's output is minimal 180 else: 181 sshgetfingerprint = None 182 pverbose('ssh ver %s, sshgetfingerprint=%s'%(sver,sshgetfingerprint)) 183 184 if listidentities==2 or sshgetfingerprint: 185 pre_identities=getoutput(sshadd + ' -l').split(os.linesep) 186 pverbose('pre_identities=%s'%(pre_identities)) 187 if sshgetfingerprint: 188 for k in keys[:]: 189 fp = getoutput_checkstatus(sshgetfingerprint[0]%k) 190 pverbose('%s = %s'%(k,fp)) 191 x = re.match(sshgetfingerprint[1],fp) 192 kid = x.group(1) 193 for pid in pre_identities: 194 if pid.find(kid)>=0: 195 pinfo('key %s already loaded'%(k)) 196 #pinfo('key %s(%s) already loaded (%s)'%(k,kid,pid)) 197 keys.remove(k) 198 break 199 if not keys: 200 pinfo('all keys loaded already, exiting') 201 return 202 203#we have to use a pty since ssh-add will always call ssh_askpass if its stdin is not a tty. 204 fd=ptyopen(sshadd, keys) 205 s='' 206 passphrases=[] 207 readded=re.compile(r"added: (.*)$",re.M|re.I)#commercial ssh2 doesn't say this at all. oh well. 208 rebadpass=re.compile(r"Bad passphrase",re.I) 209 #commercial ssh don't say enter passphrase for ..., so get it from the need 210 reneed=re.compile(r"Need passphrase for (.*)",re.I) 211 reenterpass=re.compile(r"Enter passphrase(?: for (.*?)):? ",re.I) 212 while 1: 213 try: 214 r=os.read(fd,1024) 215 except OSError, err: 216 if err[0]==errno.EAGAIN or err[0]==errno.EINTR: 217 continue #dunno if these can happen here, the docs don't say.. be safe. :) 218 if err[0]==errno.EIO: 219 break #seems to return oserror on eof :) 220 raise 221 if not len(r): 222 break 223 perror('hm, I wonder why we are here') 224 continue 225 pverbose('read: {'+r+'}') 226 s += r 227 while 1: #mulitple added messages coulde be in the same read if no passphrase used.. 228 x=readded.search(s) 229 if not x: 230 break 231 pinfo('added: %s'%x.group(1)) 232 #pverbose('s before: {'+s+'}') 233 s=s[:x.start(0)]+s[x.end(0):] #cut the added part out so we don't see it again 234 #pverbose('s after: {'+s+'}') 235 236 x=reneed.search(s) 237 if x: 238 curkey=x.group(1) 239 x=reenterpass.search(s) 240 if x: 241 curpass=0 242 if x.group(1): 243 curkey=x.group(1) 244 curaskextra='' 245 askedyet=0 246 else: 247 x=rebadpass.search(s) 248 if not x: 249 continue 250 if askedyet and not curaskextra: #if we haven't asked yet for this key, any bad pass messages will just be from trying previous passphrases. 251 curaskextra='Bad Pass. ' 252 if curpass>=len(passphrases): 253 passphrases.append(askpass('%sEnter passphrase for %s: '%(curaskextra,curkey))) 254 askedyet=1 255 writen(fd,passphrases[curpass]+'\n') 256 curpass+=1 257 pverbose('<pass>') 258 s='' 259 os.close(fd) 260 w=os.wait() 261 if w[1]: 262 perror('%s[%i] %s'%(sshadd,w[0],exitstatus_str(w[1]))) 263 x=re.search('(.*)$',s) #. does not match \n without re.DOTALL 264 if x: 265 perror('last line of output was: %s'%x.group(1)) #we only want to print the last line in case a passphrase was in that output somewhere... and just because its cleaner :) 266 sys.exit(1) 267 pverbose('wait: '+str(w)) 268 269 if listidentities==2: 270 excl=re.compile('agent has (no|[0-9]+) ',re.I) 271 post_identities=getoutput(sshadd + ' -l').split(os.linesep) 272 added=[x for x in post_identities if x not in pre_identities and not excl.search(x)] 273 delled=[x for x in pre_identities if x not in post_identities and not excl.search(x)] 274 if added or delled: 275 for x in added: print '+'+x 276 for x in delled: print '-'+x 277 else: 278 pverbose("no change in agent's identities") 279 elif listidentities: 280 os.system(sshadd + ' -l') 281 282if __name__=='__main__': 283 try: 284 main(sys.argv[1:]) 285 except KeyboardInterrupt: 286 # in debug mode print the traceback, otherwise exit nicely on ^C 287 if verbose>0: raise 288