Main coves client
1import 'dart:convert';
2import 'dart:typed_data';
3
4import '../utils/lock.dart';
5import 'runtime_implementation.dart';
6
7/// Main runtime class that wraps a RuntimeImplementation and provides
8/// high-level cryptographic operations for OAuth.
9///
10/// This class handles:
11/// - Key generation with algorithm preference sorting
12/// - SHA-256 hashing with base64url encoding
13/// - Nonce generation
14/// - PKCE (Proof Key for Code Exchange) generation
15/// - JWK thumbprint calculation
16///
17/// All operations use the underlying RuntimeImplementation for
18/// platform-specific cryptographic primitives.
19class Runtime {
20 final RuntimeImplementation _implementation;
21
22 /// Whether the implementation provides a custom lock mechanism.
23 final bool hasImplementationLock;
24
25 /// The lock function to use (either custom or local fallback).
26 final RuntimeLock usingLock;
27
28 Runtime(this._implementation)
29 : hasImplementationLock = _implementation.requestLock != null,
30 usingLock = _implementation.requestLock ?? requestLocalLock;
31
32 /// Generates a cryptographic key that supports the given algorithms.
33 ///
34 /// The algorithms are sorted by preference before being passed to the
35 /// key factory. This ensures consistent key selection across platforms.
36 ///
37 /// Algorithm preference order (most to least preferred):
38 /// 1. ES256K (secp256k1)
39 /// 2. ES256, ES384, ES512 (elliptic curve, shorter keys first)
40 /// 3. PS256, PS384, PS512 (RSA-PSS, shorter keys first)
41 /// 4. RS256, RS384, RS512 (RSA-PKCS1, shorter keys first)
42 /// 5. Other algorithms (maintain original order)
43 ///
44 /// Example:
45 /// ```dart
46 /// final key = await runtime.generateKey(['ES256', 'RS256', 'ES384']);
47 /// // Returns key supporting ES256 (preferred over RS256 and ES384)
48 /// ```
49 Future<Key> generateKey(List<String> algs) async {
50 final algsSorted = List<String>.from(algs)..sort(_compareAlgos);
51 return _implementation.createKey(algsSorted);
52 }
53
54 /// Computes the SHA-256 hash of the input text and returns it as base64url.
55 ///
56 /// This is used extensively in OAuth for:
57 /// - PKCE code challenge (S256 method)
58 /// - JWK thumbprint calculation
59 /// - DPoP access token hash (ath claim)
60 ///
61 /// Example:
62 /// ```dart
63 /// final hash = await runtime.sha256('hello world');
64 /// // Returns base64url-encoded SHA-256 hash
65 /// ```
66 Future<String> sha256(String text) async {
67 final bytes = utf8.encode(text);
68 final digest = await _implementation.digest(
69 Uint8List.fromList(bytes),
70 const DigestAlgorithm.sha256(),
71 );
72 return _base64UrlEncode(digest);
73 }
74
75 /// Generates a cryptographically secure random nonce.
76 ///
77 /// The nonce is base64url-encoded and has the specified byte length
78 /// (default 16 bytes = 128 bits of entropy).
79 ///
80 /// Used for:
81 /// - OAuth state parameter
82 /// - OIDC nonce parameter
83 /// - DPoP jti (JWT ID) claim
84 ///
85 /// Example:
86 /// ```dart
87 /// final nonce = await runtime.generateNonce(); // 16 bytes
88 /// final longNonce = await runtime.generateNonce(32); // 32 bytes
89 /// ```
90 Future<String> generateNonce([int length = 16]) async {
91 final bytes = await _implementation.getRandomValues(length);
92 return _base64UrlEncode(bytes);
93 }
94
95 /// Generates PKCE (Proof Key for Code Exchange) parameters.
96 ///
97 /// PKCE is a security extension for OAuth that prevents authorization code
98 /// interception attacks. It's required for public clients (mobile/desktop apps).
99 ///
100 /// Returns a map with:
101 /// - `verifier`: Random code verifier (base64url-encoded)
102 /// - `challenge`: SHA-256 hash of verifier (base64url-encoded)
103 /// - `method`: 'S256' (indicating SHA-256 hashing method)
104 ///
105 /// The verifier should be stored securely and sent during token exchange.
106 /// The challenge is sent during authorization.
107 ///
108 /// See: https://datatracker.ietf.org/doc/html/rfc7636
109 ///
110 /// Example:
111 /// ```dart
112 /// final pkce = await runtime.generatePKCE();
113 /// // Use pkce['challenge'] in authorization request
114 /// // Store pkce['verifier'] for token exchange
115 /// ```
116 Future<Map<String, String>> generatePKCE([int? byteLength]) async {
117 final verifier = await _generateVerifier(byteLength);
118 final challenge = await sha256(verifier);
119 return {'verifier': verifier, 'challenge': challenge, 'method': 'S256'};
120 }
121
122 /// Calculates the JWK thumbprint (jkt) for a given JSON Web Key.
123 ///
124 /// The thumbprint is a hash of the key's essential components, used to
125 /// uniquely identify a key. For DPoP, this binds tokens to specific keys.
126 ///
127 /// The calculation follows RFC 7638:
128 /// 1. Extract required components based on key type (kty)
129 /// 2. Create canonical JSON representation
130 /// 3. Compute SHA-256 hash
131 /// 4. Base64url-encode the result
132 ///
133 /// Required components by key type:
134 /// - EC: crv, kty, x, y
135 /// - OKP: crv, kty, x
136 /// - RSA: e, kty, n
137 /// - oct: k, kty
138 ///
139 /// See: https://datatracker.ietf.org/doc/html/rfc7638
140 ///
141 /// Example:
142 /// ```dart
143 /// final thumbprint = await runtime.calculateJwkThumbprint(jwk);
144 /// // Returns base64url-encoded SHA-256 hash of key components
145 /// ```
146 Future<String> calculateJwkThumbprint(Map<String, dynamic> jwk) async {
147 final components = _extractJktComponents(jwk);
148 final data = jsonEncode(components);
149 return sha256(data);
150 }
151
152 /// Generates a PKCE code verifier.
153 ///
154 /// The verifier is a cryptographically random string that:
155 /// - Has length between 43-128 characters (32-96 bytes before encoding)
156 /// - Is base64url-encoded
157 /// - SHOULD be 32 bytes (43 chars) per RFC 7636 recommendations
158 ///
159 /// See: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
160 Future<String> _generateVerifier([int? byteLength]) async {
161 final length = byteLength ?? 32;
162
163 if (length < 32 || length > 96) {
164 throw ArgumentError(
165 'Invalid code_verifier length: must be between 32 and 96 bytes',
166 );
167 }
168
169 final bytes = await _implementation.getRandomValues(length);
170 return _base64UrlEncode(bytes);
171 }
172
173 /// Base64url encodes a byte array without padding.
174 ///
175 /// Base64url encoding is standard base64 with URL-safe characters:
176 /// - '+' becomes '-'
177 /// - '/' becomes '_'
178 /// - Padding ('=') is removed
179 ///
180 /// This is the encoding used throughout OAuth and JWT specifications.
181 String _base64UrlEncode(Uint8List bytes) {
182 return base64Url.encode(bytes).replaceAll('=', '');
183 }
184}
185
186/// Extracts the required components from a JWK for thumbprint calculation.
187///
188/// This follows RFC 7638 which specifies exactly which fields to include
189/// in the thumbprint hash for each key type.
190///
191/// The components are returned in a Map that will be serialized to JSON
192/// in lexicographic order (Dart's jsonEncode naturally does this).
193///
194/// Throws ArgumentError if:
195/// - Required fields are missing
196/// - Key type (kty) is unsupported
197Map<String, String> _extractJktComponents(Map<String, dynamic> jwk) {
198 String getRequired(String field) {
199 final value = jwk[field];
200 if (value is! String || value.isEmpty) {
201 throw ArgumentError('"$field" parameter missing or invalid');
202 }
203 return value;
204 }
205
206 final kty = getRequired('kty');
207
208 switch (kty) {
209 case 'EC':
210 // Elliptic Curve keys (ES256, ES384, ES512, ES256K)
211 return {
212 'crv': getRequired('crv'),
213 'kty': kty,
214 'x': getRequired('x'),
215 'y': getRequired('y'),
216 };
217
218 case 'OKP':
219 // Octet Key Pair (EdDSA)
220 return {'crv': getRequired('crv'), 'kty': kty, 'x': getRequired('x')};
221
222 case 'RSA':
223 // RSA keys (RS256, RS384, RS512, PS256, PS384, PS512)
224 return {'e': getRequired('e'), 'kty': kty, 'n': getRequired('n')};
225
226 case 'oct':
227 // Symmetric keys (HS256, HS384, HS512)
228 return {'k': getRequired('k'), 'kty': kty};
229
230 default:
231 throw ArgumentError(
232 '"kty" (Key Type) parameter missing or unsupported: $kty',
233 );
234 }
235}
236
237/// Compares two algorithm strings for preference ordering.
238///
239/// Algorithm preference order:
240/// 1. ES256K (secp256k1) - always most preferred
241/// 2. ES* (Elliptic Curve) - prefer shorter keys
242/// - ES256 > ES384 > ES512
243/// 3. PS* (RSA-PSS) - prefer shorter keys
244/// - PS256 > PS384 > PS512
245/// 4. RS* (RSA-PKCS1) - prefer shorter keys
246/// - RS256 > RS384 > RS512
247/// 5. Other algorithms - maintain original order
248///
249/// Returns:
250/// - Negative if `a` is preferred over `b`
251/// - Positive if `b` is preferred over `a`
252/// - Zero if no preference (maintain order)
253int _compareAlgos(String a, String b) {
254 // ES256K is always most preferred
255 if (a == 'ES256K') return -1;
256 if (b == 'ES256K') return 1;
257
258 // Check algorithm families in preference order: ES > PS > RS
259 for (final prefix in ['ES', 'PS', 'RS']) {
260 if (a.startsWith(prefix)) {
261 if (b.startsWith(prefix)) {
262 // Both have same prefix, prefer shorter key length
263 // Extract the number (e.g., "256" from "ES256")
264 final aLen = int.tryParse(a.substring(2, 5)) ?? 0;
265 final bLen = int.tryParse(b.substring(2, 5)) ?? 0;
266
267 // Prefer shorter keys (256 < 384 < 512)
268 return aLen - bLen;
269 }
270 // 'a' has the prefix, 'b' doesn't - prefer 'a'
271 return -1;
272 } else if (b.startsWith(prefix)) {
273 // 'b' has the prefix, 'a' doesn't - prefer 'b'
274 return 1;
275 }
276 }
277
278 // No known preference, maintain original order
279 return 0;
280}