Blob Blame History Raw
From 88724949fa960549ade0107c15624eb54aa4ae5b Mon Sep 17 00:00:00 2001
From: Robbie Harwood <rharwood@redhat.com>
Date: Mon, 26 Feb 2018 18:08:00 -0500
Subject: [PATCH] Port to python-gssapi from pykerberos

python-gssapi has a visible, active upstream and a more pleasant
interface.  python-gssapi is present in most distributions, while
pykerberos is slated for removal from Fedora/RHEL/CentOS.

Github-ref: https://github.com/OfflineIMAP/offlineimap/pull/529
Tested-by: Robbie Harwood <rharwood@redhat.com>
Signed-off-by: Robbie Harwood <rharwood@redhat.com>
Signed-off-by: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
---
 offlineimap/imapserver.py | 69 +++++++++++++++++++++--------------------------
 1 file changed, 30 insertions(+), 39 deletions(-)

diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py
index 0a130575..95c4d662 100644
--- a/offlineimap/imapserver.py
+++ b/offlineimap/imapserver.py
@@ -17,7 +17,6 @@
 
 import hmac
 import socket
-import base64
 import json
 import urllib
 import time
@@ -36,13 +35,10 @@
 
 
 try:
-    # do we have a recent pykerberos?
-    have_gss = False
-    import kerberos
-    if 'authGSSClientWrap' in dir(kerberos):
-        have_gss = True
+    import gssapi
+    have_gss = True
 except ImportError:
-    pass
+    have_gss = False
 
 
 class IMAPServer(object):
@@ -55,9 +51,6 @@ class IMAPServer(object):
      delim The server's folder delimiter. Only valid after acquireconnection()
     """
 
-    GSS_STATE_STEP = 0
-    GSS_STATE_WRAP = 1
-
     def __init__(self, repos):
         """:repos: a IMAPRepository instance."""
 
@@ -127,7 +120,6 @@ def __init__(self, repos):
         self.connectionlock = Lock()
         self.reference = repos.getreference()
         self.idlefolders = repos.getidlefolders()
-        self.gss_step = self.GSS_STATE_STEP
         self.gss_vc = None
         self.gssapi = False
 
@@ -267,33 +259,35 @@ def __xoauth2handler(self, response):
         self.ui.debug('imap', 'xoauth2handler: returning "%s"'% auth_string)
         return auth_string
 
-    def __gssauth(self, response):
-        data = base64.b64encode(response)
+    # Perform the next step handling a GSSAPI connection.
+    # Client sends first, so token will be ignored if there is no context.
+    def __gsshandler(self, token):
+        if token == "":
+            token = None
         try:
-            if self.gss_step == self.GSS_STATE_STEP:
-                if not self.gss_vc:
-                    rc, self.gss_vc = kerberos.authGSSClientInit(
-                        'imap@' + self.hostname)
-                    response = kerberos.authGSSClientResponse(self.gss_vc)
-                rc = kerberos.authGSSClientStep(self.gss_vc, data)
-                if rc != kerberos.AUTH_GSS_CONTINUE:
-                    self.gss_step = self.GSS_STATE_WRAP
-            elif self.gss_step == self.GSS_STATE_WRAP:
-                rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
-                response = kerberos.authGSSClientResponse(self.gss_vc)
-                rc = kerberos.authGSSClientWrap(
-                    self.gss_vc, response, self.username)
-            response = kerberos.authGSSClientResponse(self.gss_vc)
-        except kerberos.GSSError as err:
-            # Kerberos errored out on us, respond with None to cancel the
+            if not self.gss_vc:
+                name = gssapi.Name('imap@' + self.hostname,
+                                   gssapi.NameType.hostbased_service)
+                self.gss_vc = gssapi.SecurityContext(usage="initiate",
+                                                     name=name)
+
+            if not self.gss_vc.complete:
+                response = self.gss_vc.step(token)
+                return response if response else ""
+
+            # Don't bother checking qop because we're over a TLS channel
+            # already.  But hey, if some server started encrypting tomorrow,
+            # we'd be ready since krb5 always requests integrity and
+            # confidentiality support.
+            response = self.gss_vc.unwrap(token)
+            response = self.gss_vc.wrap(response.message, response.encrypted)
+            return response.message if response.message else ""
+        except gssapi.exceptions.GSSError as err:
+            # GSSAPI errored out on us; respond with None to cancel the
             # authentication
-            self.ui.debug('imap', '%s: %s'% (err[0][0], err[1][0]))
+            self.ui.debug('imap', err.gen_message())
             return None
 
-        if not response:
-            response = ''
-        return base64.b64decode(response)
-
     def __start_tls(self, imapobj):
         if 'STARTTLS' in imapobj.capabilities and not self.usessl:
             self.ui.debug('imap', 'Using STARTTLS connection')
@@ -327,16 +321,14 @@ def __authn_gssapi(self, imapobj):
 
         self.connectionlock.acquire()
         try:
-            imapobj.authenticate('GSSAPI', self.__gssauth)
+            imapobj.authenticate('GSSAPI', self.__gsshandler)
             return True
         except imapobj.error as e:
             self.gssapi = False
             raise
         else:
             self.gssapi = True
-            kerberos.authGSSClientClean(self.gss_vc)
             self.gss_vc = None
-            self.gss_step = self.GSS_STATE_STEP
         finally:
             self.connectionlock.release()
 
@@ -680,8 +672,7 @@ def close(self):
             self.assignedconnections = []
             self.availableconnections = []
             self.lastowner = {}
-            # reset kerberos state
-            self.gss_step = self.GSS_STATE_STEP
+            # reset GSSAPI state
             self.gss_vc = None
             self.gssapi = False