1# -*- coding: ascii -*- 2""" 3web2ldap plugin classes for OATH-LDAP 4 5see https://www.stroeder.com/oath-ldap.html 6""" 7 8import re 9import datetime 10import base64 11from typing import Dict 12 13from ldap0 import LDAPError 14 15from ... import cmp 16from ...utctime import strptime, ts2repr 17from ..schema.syntaxes import ( 18 DirectoryString, 19 DynamicDNSelectList, 20 GeneralizedTime, 21 HMACAlgorithmOID, 22 JSONValue, 23 LDAPv3ResultCode, 24 OctetString, 25 SelectList, 26 Timespan, 27 syntax_registry, 28) 29 30 31syntax_registry.reg_at( 32 JSONValue.oid, [ 33 '1.3.6.1.4.1.5427.1.389.4226.4.12', # oathEncKey 34 '1.3.6.1.4.1.5427.1.389.4226.4.14', # oathTokenPIN 35 ] 36) 37 38 39class OathOTPLength(SelectList): 40 oid: str = 'OathOTPLength-oid' 41 desc: str = 'number of OTP digits' 42 attr_value_dict: Dict[str, str] = { 43 '6': '6', 44 '8': '8', 45 } 46 47syntax_registry.reg_at( 48 OathOTPLength.oid, [ 49 '1.3.6.1.4.1.5427.1.389.4226.4.5', # oathOTPLength 50 ] 51) 52 53 54class OathHOTPParams(DynamicDNSelectList): 55 oid: str = 'OathHOTPParams-oid' 56 desc: str = 'DN of the oathHOTPParams entry' 57 ldap_url = 'ldap:///_?cn?sub?(objectClass=oathHOTPParams)' 58 ref_attrs = ( 59 (None, 'Same params', None, None), 60 ) 61 62syntax_registry.reg_at( 63 OathHOTPParams.oid, [ 64 '1.3.6.1.4.1.5427.1.389.4226.4.5.1', # oathHOTPParams 65 ] 66) 67 68 69class OathResultCode(LDAPv3ResultCode): 70 oid: str = 'OathResultCode-oid' 71 72syntax_registry.reg_at( 73 OathResultCode.oid, [ 74 '1.3.6.1.4.1.5427.1.389.4226.4.13.1', # oathSuccessResultCode 75 '1.3.6.1.4.1.5427.1.389.4226.4.13.2', # oathFailureResultCode 76 ] 77) 78 79 80class OathHOTPToken(DynamicDNSelectList): 81 oid: str = 'OathHOTPToken-oid' 82 desc: str = 'DN of the oathHOTPToken entry' 83 ldap_url = 'ldap:///_?oathTokenSerialNumber?sub?(objectClass=oathHOTPToken)' 84 ref_attrs = ( 85 (None, 'Users', None, None), 86 ) 87 88syntax_registry.reg_at( 89 OathHOTPToken.oid, [ 90 '1.3.6.1.4.1.5427.1.389.4226.4.9.1', # oathHOTPToken 91 ] 92) 93 94 95class OathTOTPParams(DynamicDNSelectList): 96 oid: str = 'OathTOTPParams-oid' 97 desc: str = 'DN of the oathTOTPParams entry' 98 ldap_url = 'ldap:///_?cn?sub?(objectClass=oathTOTPParams)' 99 ref_attrs = ( 100 (None, 'Same params', None, None), 101 ) 102 103syntax_registry.reg_at( 104 OathTOTPParams.oid, [ 105 '1.3.6.1.4.1.5427.1.389.4226.4.5.2', # oathTOTPParams 106 ] 107) 108 109 110class OathTOTPToken(DynamicDNSelectList): 111 oid: str = 'OathTOTPToken-oid' 112 desc: str = 'DN of the oathTOTPToken entry' 113 ldap_url = 'ldap:///_?oathTokenSerialNumber?sub?(objectClass=oathTOTPToken)' 114 ref_attrs = ( 115 (None, 'Users', None, None), 116 ) 117 118syntax_registry.reg_at( 119 OathTOTPToken.oid, [ 120 '1.3.6.1.4.1.5427.1.389.4226.4.9.2', # oathTOTPToken 121 ] 122) 123 124 125class OathTokenIdentifier(DirectoryString): 126 """ 127 see http://openauthentication.org/specification/tokenSpecs 128 """ 129 oid: str = 'OathTokenIdentifier-oid' 130 desc: str = 'Globally unique token identifier' 131 max_len: str = 12 132 pattern = re.compile(r'^[a-zA-Z0-9]{12}$') 133 134syntax_registry.reg_at( 135 OathTokenIdentifier.oid, [ 136 '1.3.6.1.4.1.5427.1.389.4226.4.3', # oathTokenIdentifier 137 ] 138) 139 140 141class OathInitPwAlphabet(DirectoryString): 142 oid: str = 'OathInitPwAlphabet-oid' 143 desc: str = 'Alphabet used to generate init passwords' 144 145 def sanitize(self, attr_value: bytes) -> bytes: 146 return b''.join([ 147 self._app.ls.uc_encode(c)[0] 148 for c in sorted(set( 149 self._app.ls.uc_decode(attr_value or '')[0].replace(' ', '') 150 )) 151 ]) 152 153 154syntax_registry.reg_at( 155 HMACAlgorithmOID.oid, [ 156 '1.3.6.1.4.1.5427.1.389.4226.4.6', # oathHMACAlgorithm 157 ] 158) 159 160 161syntax_registry.reg_at( 162 Timespan.oid, [ 163 '1.3.6.1.4.1.5427.1.389.4226.4.4.1', # oathTOTPTimeStepPeriod 164 '1.3.6.1.4.1.5427.1.389.4226.4.8', # oathSecretMaxAge 165 ] 166) 167 168 169class OathSecret(OctetString): 170 oid: str = 'OathSecret-oid' 171 desc: str = 'OATH shared secret' 172 173 def display(self, vidx, links) -> str: 174 return '<br>'.join(( 175 self._app.form.s2d(base64.b32encode(self._av).decode('ascii')), 176 OctetString.display(self, vidx, links), 177 )) 178 179syntax_registry.reg_at( 180 OathSecret.oid, [ 181 '1.3.6.1.4.1.5427.1.389.4226.4.1', # oathSecret 182 ] 183) 184 185 186class OathSecretTime(GeneralizedTime): 187 oid: str = 'OathSecretTime-oid' 188 desc: str = 'OATH secret change time' 189 time_divisors = Timespan.time_divisors 190 191 def display(self, vidx, links) -> str: 192 ocs = self._entry.object_class_oid_set() 193 gt_disp_html = GeneralizedTime.display(self, vidx, links) 194 if 'oathHOTPToken' in ocs: 195 oath_params_dn_attr = 'oathHOTPParams' 196 elif 'oathTOTPToken' in ocs: 197 oath_params_dn_attr = 'oathTOTPParams' 198 else: 199 return gt_disp_html 200 try: 201 oath_secret_time_dt = strptime(self._av) 202 except ValueError: 203 return gt_disp_html 204 try: 205 oath_params_dn = self._entry[oath_params_dn_attr][0].decode(self._app.ls.charset) 206 except KeyError: 207 return gt_disp_html 208 try: 209 oath_params = self._app.ls.l.read_s(oath_params_dn, attrlist=['oathSecretMaxAge']) 210 except LDAPError: 211 return gt_disp_html 212 try: 213 oath_secret_max_age_secs = int(oath_params.entry_s['oathSecretMaxAge'][0]) 214 except KeyError: 215 expire_msg = 'will never expire' 216 except ValueError: 217 return gt_disp_html 218 else: 219 if oath_secret_max_age_secs: 220 oath_secret_max_age = datetime.timedelta(seconds=oath_secret_max_age_secs) 221 current_time = datetime.datetime.utcnow() 222 expire_dt = oath_secret_time_dt+oath_secret_max_age 223 expired_since = (expire_dt-current_time).total_seconds() 224 expire_cmp = cmp(expire_dt, current_time) 225 expire_msg = '%s %s (%s %s)' % ( 226 { 227 -1: 'expired since', 228 0: '', 229 1: 'will expire', 230 }[expire_cmp], 231 expire_dt.strftime('%c'), 232 self._app.form.s2d( 233 ts2repr( 234 self.time_divisors, 235 ' ', 236 abs(expired_since), 237 ) 238 ), 239 { 240 -1: 'ago', 241 0: '', 242 1: 'ahead', 243 }[expire_cmp], 244 ) 245 else: 246 expire_msg = 'will never expire' 247 return self.read_sep.join((gt_disp_html, expire_msg)) 248 249 250syntax_registry.reg_at( 251 OathSecretTime.oid, [ 252 '1.3.6.1.4.1.5427.1.389.4226.4.7.3', # oathSecretTime 253 ] 254) 255 256 257# Register all syntax classes in this module 258syntax_registry.reg_syntaxes(__name__) 259