|
58 | 58 | import urllib.request, urllib.parse, urllib.error |
59 | 59 | from urllib.parse import urlparse |
60 | 60 |
|
61 | | -try: |
62 | | - import crypt |
63 | | -except ImportError: |
64 | | - crypt = None |
65 | 61 |
|
66 | 62 | from Mailman import mm_cfg |
67 | 63 | from Mailman import Utils |
68 | 64 | from Mailman import Errors |
69 | 65 | from Mailman.Logging.Syslog import syslog |
70 | | -from Mailman.Utils import md5_new, sha_new |
| 66 | +from Mailman.Utils import sha_new, hash_password, verify_password |
71 | 67 |
|
72 | 68 |
|
73 | 69 | class SecurityManager(object): |
@@ -142,81 +138,146 @@ def Authenticate(self, authcontexts, response, user=None): |
142 | 138 |
|
143 | 139 | for ac in authcontexts: |
144 | 140 | if ac == mm_cfg.AuthCreator: |
145 | | - ok = Utils.check_global_password(response, siteadmin=0) |
| 141 | + # Auto-upgrade global passwords when used for authentication |
| 142 | + ok = Utils.check_global_password(response, siteadmin=0, auto_upgrade=True) |
146 | 143 | if ok: |
147 | 144 | return mm_cfg.AuthCreator |
148 | 145 | elif ac == mm_cfg.AuthSiteAdmin: |
149 | | - ok = Utils.check_global_password(response) |
| 146 | + # Auto-upgrade global passwords when used for authentication |
| 147 | + ok = Utils.check_global_password(response, auto_upgrade=True) |
150 | 148 | if ok: |
151 | 149 | return mm_cfg.AuthSiteAdmin |
152 | 150 | elif ac == mm_cfg.AuthListAdmin: |
153 | | - def cryptmatchp(response, secret): |
154 | | - try: |
155 | | - salt = secret[:2] |
156 | | - if crypt and crypt.crypt(response, salt) == secret: |
157 | | - return True |
158 | | - return False |
159 | | - except TypeError: |
160 | | - # BAW: Hard to say why we can get a TypeError here. |
161 | | - # SF bug report #585776 says crypt.crypt() can raise |
162 | | - # this if salt contains null bytes, although I don't |
163 | | - # know how that can happen (perhaps if a MM2.0 list |
164 | | - # with USE_CRYPT = 0 has been updated? Doubtful. |
165 | | - return False |
166 | | - # The password for the list admin and list moderator are not |
167 | | - # kept as plain text, but instead as an sha hexdigest. The |
168 | | - # response being passed in is plain text, so we need to |
169 | | - # digestify it first. Note however, that for backwards |
170 | | - # compatibility reasons, we'll also check the admin response |
171 | | - # against the crypted and md5'd passwords, and if they match, |
172 | | - # we'll auto-migrate the passwords to sha. |
| 151 | + # The password for the list admin is stored as a hash. |
| 152 | + # We support multiple formats for backwards compatibility: |
| 153 | + # - New format: PBKDF2-SHA256 with $pbkdf2$ prefix |
| 154 | + # - Old format: SHA1 hexdigest (40 hex chars, auto-upgrade to PBKDF2) |
173 | 155 | key, secret = self.AuthContextInfo(ac) |
174 | 156 | if secret is None: |
175 | 157 | continue |
176 | 158 | if isinstance(response, str): |
177 | 159 | response = response.encode('utf-8') |
178 | 160 |
|
179 | | - sharesponse = sha_new(response).hexdigest() |
180 | | - upgrade = ok = False |
181 | | - if sharesponse == secret: |
182 | | - ok = True |
183 | | - elif md5_new(response).digest() == secret: |
184 | | - ok = upgrade = True |
185 | | - elif cryptmatchp(response, secret): |
186 | | - ok = upgrade = True |
187 | | - if upgrade: |
| 161 | + # Try new PBKDF2 or old SHA1 format (verify_password handles both) |
| 162 | + ok, needs_upgrade = verify_password(response, secret) |
| 163 | + upgrade = needs_upgrade |
| 164 | + |
| 165 | + # Upgrade to new PBKDF2 format if needed |
| 166 | + if upgrade and ok: |
188 | 167 | save_and_unlock = False |
189 | 168 | if not self.Locked(): |
190 | 169 | self.Lock() |
191 | 170 | save_and_unlock = True |
192 | 171 | try: |
193 | | - self.password = sharesponse |
| 172 | + # Convert response back to string for hash_password |
| 173 | + if isinstance(response, bytes): |
| 174 | + response_str = response.decode('utf-8') |
| 175 | + else: |
| 176 | + response_str = response |
| 177 | + self.password = hash_password(response_str) |
194 | 178 | if save_and_unlock: |
195 | 179 | self.Save() |
| 180 | + except (PermissionError, IOError) as e: |
| 181 | + # Log permission error but don't fail authentication |
| 182 | + # Get uid/euid/gid/egid for debugging |
| 183 | + try: |
| 184 | + uid = os.getuid() |
| 185 | + euid = os.geteuid() |
| 186 | + gid = os.getgid() |
| 187 | + egid = os.getegid() |
| 188 | + except (AttributeError, OSError): |
| 189 | + # Fallback if getuid/geteuid not available |
| 190 | + uid = euid = gid = egid = 'unknown' |
| 191 | + syslog('error', |
| 192 | + 'Could not auto-upgrade list admin password for %s: %s (uid=%s euid=%s gid=%s egid=%s)', |
| 193 | + self.internal_name(), e, uid, euid, gid, egid) |
| 194 | + # Continue - authentication still succeeds even if upgrade fails |
196 | 195 | finally: |
197 | 196 | if save_and_unlock: |
198 | 197 | self.Unlock() |
199 | 198 | if ok: |
200 | 199 | return ac |
201 | 200 | elif ac == mm_cfg.AuthListModerator: |
202 | | - # The list moderator password must be sha'd |
| 201 | + # The list moderator password is stored as a hash. |
| 202 | + # Supports both new PBKDF2 and old SHA1 formats with auto-upgrade. |
203 | 203 | key, secret = self.AuthContextInfo(ac) |
204 | 204 | if secret: |
205 | 205 | if isinstance(response, str): |
206 | 206 | response_bytes = response.encode('utf-8') |
207 | 207 | else: |
208 | 208 | response_bytes = response |
209 | | - if sha_new(response_bytes).hexdigest() == secret: |
| 209 | + ok, needs_upgrade = verify_password(response_bytes, secret) |
| 210 | + if ok: |
| 211 | + # Upgrade to new format if needed |
| 212 | + if needs_upgrade: |
| 213 | + save_and_unlock = False |
| 214 | + if not self.Locked(): |
| 215 | + self.Lock() |
| 216 | + save_and_unlock = True |
| 217 | + try: |
| 218 | + if isinstance(response, str): |
| 219 | + response_str = response |
| 220 | + else: |
| 221 | + response_str = response_bytes.decode('utf-8') |
| 222 | + self.mod_password = hash_password(response_str) |
| 223 | + if save_and_unlock: |
| 224 | + self.Save() |
| 225 | + except (PermissionError, IOError) as e: |
| 226 | + # Log permission error but don't fail authentication |
| 227 | + try: |
| 228 | + uid = os.getuid() |
| 229 | + euid = os.geteuid() |
| 230 | + gid = os.getgid() |
| 231 | + egid = os.getegid() |
| 232 | + except (AttributeError, OSError): |
| 233 | + uid = euid = gid = egid = 'unknown' |
| 234 | + syslog('error', |
| 235 | + 'Could not auto-upgrade moderator password for %s: %s (uid=%s euid=%s gid=%s egid=%s)', |
| 236 | + self.internal_name(), e, uid, euid, gid, egid) |
| 237 | + finally: |
| 238 | + if save_and_unlock: |
| 239 | + self.Unlock() |
210 | 240 | return ac |
211 | 241 | elif ac == mm_cfg.AuthListPoster: |
212 | | - # The list poster password must be sha'd |
| 242 | + # The list poster password is stored as a hash. |
| 243 | + # Supports both new PBKDF2 and old SHA1 formats with auto-upgrade. |
213 | 244 | key, secret = self.AuthContextInfo(ac) |
214 | 245 | if secret: |
215 | 246 | if isinstance(response, str): |
216 | 247 | response_bytes = response.encode('utf-8') |
217 | 248 | else: |
218 | 249 | response_bytes = response |
219 | | - if sha_new(response_bytes).hexdigest() == secret: |
| 250 | + ok, needs_upgrade = verify_password(response_bytes, secret) |
| 251 | + if ok: |
| 252 | + # Upgrade to new format if needed |
| 253 | + if needs_upgrade: |
| 254 | + save_and_unlock = False |
| 255 | + if not self.Locked(): |
| 256 | + self.Lock() |
| 257 | + save_and_unlock = True |
| 258 | + try: |
| 259 | + if isinstance(response, str): |
| 260 | + response_str = response |
| 261 | + else: |
| 262 | + response_str = response_bytes.decode('utf-8') |
| 263 | + self.post_password = hash_password(response_str) |
| 264 | + if save_and_unlock: |
| 265 | + self.Save() |
| 266 | + except (PermissionError, IOError) as e: |
| 267 | + # Log permission error but don't fail authentication |
| 268 | + try: |
| 269 | + uid = os.getuid() |
| 270 | + euid = os.geteuid() |
| 271 | + gid = os.getgid() |
| 272 | + egid = os.getegid() |
| 273 | + except (AttributeError, OSError): |
| 274 | + uid = euid = gid = egid = 'unknown' |
| 275 | + syslog('error', |
| 276 | + 'Could not auto-upgrade poster password for %s: %s (uid=%s euid=%s gid=%s egid=%s)', |
| 277 | + self.internal_name(), e, uid, euid, gid, egid) |
| 278 | + finally: |
| 279 | + if save_and_unlock: |
| 280 | + self.Unlock() |
220 | 281 | return ac |
221 | 282 | elif ac == mm_cfg.AuthUser: |
222 | 283 | if user is not None: |
|
0 commit comments