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}