// Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. // // 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 // 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 'dart:math'; import 'package:wyatt_http_client/src/utils/convert.dart'; import 'package:wyatt_http_client/src/utils/crypto.dart'; class DigestAuth { // request counter DigestAuth(this.username, this.password); String username; String password; // must get from first response String? _algorithm; String? _qop; String? _realm; String? _nonce; String? _opaque; int _nc = 0; /// Splits WWW-Authenticate header into a map. Map? splitWWWAuthenticateHeader(String header) { if (!header.startsWith('Digest ')) { throw ArgumentError.value( header, 'header', 'Header must start with "Digest "', ); } final h = header.substring(7); // remove 'Digest ' final ret = {}; final components = h.split(',').map((token) => token.trim()); for (final component in components) { final kv = component.split('='); ret[kv[0]] = kv.getRange(1, kv.length).join('=').replaceAll('"', ''); } return ret; } String _computeNonce() { final rnd = Random.secure(); final values = List.generate(16, (i) => rnd.nextInt(256)); return Convert.toHex(values); } String _formatNonceCount(int nc) => nc.toRadixString(16).padLeft(8, '0'); String _computeHA1( String realm, String? algorithm, String username, String password, String? nonce, String? cnonce, ) { if (algorithm == null || algorithm == 'MD5') { final token1 = '$username:$realm:$password'; return Crypto.md5Hash(token1); } else if (algorithm == 'MD5-sess') { final token1 = '$username:$realm:$password'; final md51 = Crypto.md5Hash(token1); final token2 = '$md51:$nonce:$cnonce'; return Crypto.md5Hash(token2); } else { throw ArgumentError.value( algorithm, 'algorithm', 'Unsupported algorithm', ); } } Map _computeResponse( String method, String path, String body, String? algorithm, String? qop, String? opaque, String realm, String? cnonce, String? nonce, int nc, String username, String password, ) { final ret = {}; final ha1 = _computeHA1(realm, algorithm, username, password, nonce, cnonce); String ha2; if (qop == 'auth-int') { final bodyHash = Crypto.md5Hash(body); final token2 = '$method:$path:$bodyHash'; ha2 = Crypto.md5Hash(token2); } else { // qop in [null, auth] final token2 = '$method:$path'; ha2 = Crypto.md5Hash(token2); } final nonceCount = _formatNonceCount(nc); ret['username'] = username; ret['realm'] = realm; ret['nonce'] = nonce; ret['uri'] = path; if (qop != null) { ret['qop'] = qop; } ret['nc'] = nonceCount; ret['cnonce'] = cnonce; if (opaque != null) { ret['opaque'] = opaque; } ret['algorithm'] = algorithm; if (qop == null) { final token3 = '$ha1:$nonce:$ha2'; ret['response'] = Crypto.md5Hash(token3); } else if (qop == 'auth' || qop == 'auth-int') { final token3 = '$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2'; ret['response'] = Crypto.md5Hash(token3); } return ret; } String getAuthString(String method, Uri url) { final cnonce = _computeNonce(); _nc += 1; // if url has query parameters, append query to path final path = url.hasQuery ? '${url.path}?${url.query}' : url.path; // after the first request we have the nonce, so we can provide credentials final authValues = _computeResponse( method, path, '', _algorithm, _qop, _opaque, _realm!, cnonce, _nonce, _nc, username, password, ); final authValuesString = authValues.entries .where((e) => e.value != null) .map((e) => [e.key, '="', e.value, '"'].join()) .toList() .join(', '); final authString = 'Digest $authValuesString'; return authString; } void initFromAuthenticateHeader(String? authInfo) { if (authInfo == null) { throw ArgumentError.notNull('authInfo'); } final values = splitWWWAuthenticateHeader(authInfo); if (values != null) { _algorithm = values['algorithm'] ?? _algorithm; _qop = values['qop'] ?? _qop; _realm = values['realm'] ?? _realm; _nonce = values['nonce'] ?? _nonce; _opaque = values['opaque'] ?? _opaque; _nc = 0; } } bool isReady() => _nonce != null && (_nc == 0 || _qop != null); }