196 lines
5.2 KiB
Dart
196 lines
5.2 KiB
Dart
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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<String, String>? 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 = <String, String>{};
|
|
|
|
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<int>.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<String, String?> _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 = <String, String?>{};
|
|
|
|
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);
|
|
}
|