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