""" pyPinentry A wrapper module to access GnuPG's pinentry program out of python Copyright (c) 2008, Phillip Berndt This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import os import popen2 import signal import sys import warnings import threading """ This module uses it's own $PATH to prevent compromized pinentry versions from beeing used. """ PINENTRY_PATH = [ "/usr/bin/", "/usr/local/bin/", "/bin/", "/sbin/" ] class PinentryStartupFailedException(Exception): """ Pinentry will throw this exception when pinentry failed to start. One could implement a fallback to getpass for that case. """ pass class PinentrySyntaxException(Exception): """ This exception means that pinentry behaved strange... """ pass class PinentryException(Exception): """ This exception is used if pinentry itself returns an error message """ pass class Pinentry(object): """ Pinentry wrapper class Every time you create a Pinentry object a pinentry process will be spawned. Use this class to interact with it. If you intend to share an instance of this class between threads always use the high-level functions askpin/askconfirm/showmessage """ _pinentry_process = None _usage_lock = None def _readmsg(self): """ Read a response from pinentry """ msg = "" while "OK" not in msg and "ERR" not in msg: msg += self._pinentry_process.fromchild.readline() return msg def __init__(self, globalgrab=True, parentwindowid=None): """ If you set globalgrab to False, pinentry will not grab keyboard and mouse in X11. Use parentwindowid to position the pinentry dialog over that window. """ # Spawn pinentry process parameters = "" if not globalgrab: parameters += "-g " if parentwindowid: parameters += "--parent-wid %d" % parentwindowid path = False for dir in PINENTRY_PATH: path = os.path.join(dir, "pinentry") if os.access(path, os.X_OK): break if not path: warnings.warn("Failed to find pinentry in prefered paths.") path = "pinentry" try: self._pinentry_process = popen2.Popen3("%s %s" % (path, parameters)) except: raise PinentryStartupFailedException() if self._readmsg()[0:2] != "OK": raise PinentryIncompatibleException() # Set terminal options self._runcmd("OPTION ttyname=/dev/tty") if "TERM" in os.environ: self._runcmd("OPTION ttytype=%s" % os.environ["TERM"]) else: self._runcmd("OPTION ttytype=vt-100") if "LC_CTYPE" in os.environ: self._runcmd("OPTION lc-ctype=%s" % os.environ["LC_CTYPE"]) else: self._runcmd("OPTION lc-ctype=UTF-8") # Create a lock for threading self._usage_lock = threading.Lock() def __del__(self): # Exit pinentry if self._pinentry_process: self._pinentry_process.tochild.close() self._pinentry_process.fromchild.close() try: os.kill(self._pinentry_process.pid, signal.SIGTERM) os.kill(self._pinentry_process.pid, signal.SIGKILL) except: pass del self._pinentry_process def _runcmd(self, command): """ Execute a pinentry Assuan command and return it's output For internal use. """ self._pinentry_process.tochild.write(command + "\n") self._pinentry_process.tochild.flush() response = self._readmsg().split() if response[0] == "ERR": raise PinentryException(" ".join(response[2:]), int(response[1])) return response def setdesc(self, description): """ Set the descriptive text to be displayed This is used for getting pins, confirmations and displaying messages. """ self._runcmd("SETDESC %s" % description) def setprompt(self, prompt): """ Set the prompt to be shown when asking for a pin """ self._runcmd("SETPROMPT %s" % prompt) def setbuttontext(self, oktext="Yes", canceltext="No"): """ Set the button texts for windows """ self._runcmd("SETOK %s" % oktext) self._runcmd("SETCANCEL %s" % canceltext) def seterror(self, errortext): """ Set the Error text """ self._runcmd("SETERROR %s" % errortext) def enablequalityindicator(self, tooltip = False): """ Enable a passphrase quality indicator """ warnings.warn("Quality indicators crash in some pinentry versions. Ignoring request") return False self._runcmd("SETQUALITYBAR") if tooltip: self._runcmd("SETQUALITYBAR_TT %s" % tooltip) def askpin(self, description=None, prompt=None, oktext=None, canceltext=None, errortext=None, enablequalityindicator=None): """ Ask for a PIN """ self._usage_lock.acquire() if description: self.setdesc(description) if prompt: self.setprompt(prompt) if oktext or canceltext: self.setbuttontext(oktext, canceltext) if errortext: self.seterror(errortext) if enablequalityindicator: self.enablequalityindicator() response = self._runcmd("GETPIN") if response[0] != "D" or "OK" not in response: raise PinentrySyntaxException() self._usage_lock.release() return " ".join(response[1:response.index("OK")]) def askconfirm(self, description=None, oktext=None, canceltext=None): """ Ask for confirmation """ self._usage_lock.acquire() if description: self.setdesc(description) if oktext or canceltext: self.setbuttontext(oktext, canceltext) try: self._runcmd("CONFIRM") self._usage_lock.release() return True except PinentryException: self._usage_lock.release() if sys.exc_info()[1].args[1] == 83886194: # Not confirmed return False else: raise sys.exc_info()[1] def showmessage(self, message=None, buttontext=None): """ Show a message dialog """ self._usage_lock.acquire() if message: self.setdesc(message) if buttontext: self.setbuttontext(oktext=buttontext) self._runcmd("MESSAGE") self._usage_lock.release() if __name__ == "__main__": # Some tests... test = Pinentry() try: passw = test.askpin("Enter a password", "Prompt:", enablequalityindicator=True) except: passw = "1234" try: if test.askpin("Enter %s" % passw) != passw: while test.askpin("Enter %s" % passw, errortext="You are too dumb") != passw: pass print "Neat" except PinentryException: print "Why did u cancel? :(" if test.askconfirm("I will now rm -rf /, ok?", "Maybe", "Yes"): print "Bashian roulette!" else: print "Haha. Fool." test.showmessage("Ok. Done.", "Merci") print "Killing object" i = test._pinentry_process.pid os.system("ps -p %d" % i) del test os.system("ps -p %d" % i) print "Process test" test = Pinentry() pid = os.fork() try: print test.askpin("Ask pin in %d" % pid) except PinentrySyntaxException: print sys.exc_info()[1]