Line data Source code
1 : import 'dart:convert';
2 : import 'dart:io';
3 :
4 : import 'package:agattp/src/agattp.dart';
5 : import 'package:agattp/src/agattp_method.dart';
6 : import 'package:agattp/src/agattp_response.dart';
7 : import 'package:agattp/src/agattp_utils.dart';
8 : import 'package:agattp/src/auth/agattp_abstract_auth.dart';
9 : import 'package:crypto/crypto.dart';
10 :
11 : /// A Digest can use either md5 or sha256 algorithms to communicate. This enum
12 : /// encodes both of those options, with a helper factory that returns the
13 : /// appropriate algorithm based on a given value from the digest payload
14 : enum DigestAlgorithm {
15 : md5,
16 : sha256;
17 :
18 : /// Parse the given value and identify which algorithm it correlates with.
19 : /// Will default to md5 if the parsing fails for any reason
20 1 : factory DigestAlgorithm.parse(dynamic value) {
21 : final String name =
22 4 : value.toString().toLowerCase().replaceAll(RegExp(r'-|\s|_'), '');
23 :
24 1 : return values.firstWhere(
25 3 : (DigestAlgorithm alg) => alg.name == name,
26 0 : orElse: () => DigestAlgorithm.md5,
27 : );
28 : }
29 : }
30 :
31 : /// Implementation of the Digest authentication strategy
32 : class AgattpAuthDigest implements AgattpAuthInterface {
33 : /// The digest username to be used in authentication
34 : final String username;
35 :
36 : /// The digest password to be used in authentication
37 : final String password;
38 :
39 : /// Stores misc data for the digest algorithm
40 : final Map<String, String> map = <String, String>{};
41 :
42 : bool _ready = false;
43 : int _nc = 0;
44 : late String _realm;
45 : late String _nonce;
46 : late String _qop;
47 : late String _cnonce;
48 : late DigestAlgorithm _algorithm;
49 :
50 1 : AgattpAuthDigest({
51 : required this.username,
52 : required this.password,
53 1 : }) : super();
54 :
55 1 : void reset() {
56 1 : _ready = false;
57 1 : _nc = 0;
58 : }
59 :
60 2 : bool get ready => _ready;
61 :
62 1 : @override
63 : Future<Map<String, String>> getAuthHeaders(
64 : AgattpMethod method,
65 : Uri uri,
66 : ) async {
67 1 : if (!_ready) {
68 2 : final AgattpResponse response = await Agattp().get(uri);
69 :
70 : final String wwwAuthenticate =
71 2 : response.headers.value(HttpHeaders.wwwAuthenticateHeader) ?? '';
72 :
73 1 : if (wwwAuthenticate.isEmpty) {
74 1 : throw Exception('WWW-Authenticate header not found');
75 : }
76 :
77 : final List<String> parts =
78 3 : wwwAuthenticate.replaceAll(RegExp('^[Dd]igest'), '').split(',');
79 :
80 2 : for (final String part in parts) {
81 2 : final List<String> p2 = Utils.splitFirst(part.trim(), '=');
82 2 : if (p2.length != 2) {
83 : continue;
84 : }
85 :
86 6 : map[p2.first.trim()] = p2.last.trim();
87 : }
88 :
89 2 : map.remove('stale');
90 :
91 4 : _algorithm = DigestAlgorithm.parse(map['algorithm']);
92 :
93 3 : _realm = map['realm'] ?? '';
94 :
95 3 : _nonce = map['nonce'] ?? '';
96 :
97 3 : _qop = map['qop'] ?? '';
98 :
99 1 : _cnonce =
100 5 : md5.convert(DateTime.now().toIso8601String().codeUnits).toString();
101 :
102 1 : _ready = true;
103 : }
104 :
105 4 : map['username'] = '"$username"';
106 :
107 4 : map['uri'] = '"${uri.path}"';
108 :
109 3 : map['realm'] = _realm;
110 :
111 3 : map['nonce'] = _nonce;
112 :
113 3 : map['qop'] = _qop;
114 :
115 4 : map['cnonce'] = '"$_cnonce"';
116 :
117 2 : _nc++;
118 :
119 5 : map['nc'] = _nc.toString().padLeft(8, '0');
120 :
121 3 : map['response'] = '"${_getResponse(
122 1 : username: username,
123 2 : realm: Utils.removeQuotes(_realm),
124 1 : password: password,
125 2 : method: method.name.toUpperCase(),
126 1 : path: uri.path,
127 2 : nonce: Utils.removeQuotes(_nonce),
128 2 : nc: map['nc']!,
129 1 : cnonce: _cnonce,
130 2 : qop: Utils.removeQuotes(_qop),
131 1 : )}"';
132 :
133 3 : final String token = map.entries.map(
134 4 : (MapEntry<String, String> e) => '${e.key}=${e.value}',
135 1 : ).join(', ');
136 :
137 2 : return <String, String>{HttpHeaders.authorizationHeader: 'Digest $token'};
138 : }
139 :
140 1 : String _getResponse({
141 : required String username,
142 : required String realm,
143 : required String password,
144 : required String method,
145 : required String path,
146 : required String nonce,
147 : required String nc,
148 : required String cnonce,
149 : required String qop,
150 : }) {
151 1 : final Hash hash = switch (_algorithm) {
152 1 : DigestAlgorithm.md5 => md5,
153 1 : DigestAlgorithm.sha256 => sha256,
154 : };
155 :
156 : final String h1 =
157 4 : hash.convert(utf8.encode('$username:$realm:$password')).toString();
158 :
159 4 : final String h2 = hash.convert(utf8.encode('$method:$path')).toString();
160 :
161 1 : return hash.convert(
162 2 : utf8.encode('$h1:$nonce:$nc:$cnonce:$qop:$h2'),
163 1 : ).toString();
164 : }
165 : }
|