1import 'dart:convert'; 2import 'dart:math'; 3import 'dart:typed_data'; 4 5import 'package:pointycastle/export.dart' as pointycastle; 6 7import '../runtime/runtime_implementation.dart'; 8 9/// Flutter implementation of Key using pointycastle for cryptographic operations. 10/// 11/// Supports EC keys with the following algorithms: 12/// - ES256 (P-256/secp256r1) 13/// - ES384 (P-384/secp384r1) 14/// - ES512 (P-521/secp521r1) - Note: P-521, not P-512 15/// - ES256K (secp256k1) 16/// 17/// This class handles: 18/// - Key generation with secure randomness 19/// - JWT signing (ES256/ES384/ES512/ES256K) 20/// - JWK representation (public and private components) 21/// - Serialization/deserialization for session storage 22class FlutterKey implements Key { 23 /// The EC private key (contains both private and public components) 24 final pointycastle.ECPrivateKey privateKey; 25 26 /// The EC public key 27 final pointycastle.ECPublicKey publicKey; 28 29 /// The algorithm this key supports 30 final String algorithm; 31 32 /// Optional key ID 33 final String? _kid; 34 35 /// Creates a FlutterKey from EC key components. 36 FlutterKey({ 37 required this.privateKey, 38 required this.publicKey, 39 required this.algorithm, 40 String? kid, 41 }) : _kid = kid; 42 43 @override 44 List<String> get algorithms => [algorithm]; 45 46 @override 47 String? get kid => _kid; 48 49 @override 50 String get usage => 'sign'; 51 52 @override 53 Map<String, dynamic>? get bareJwk { 54 // Return public key components only (no private key 'd') 55 final jwk = _ecPublicKeyToJwk(publicKey, algorithm); 56 if (_kid != null) { 57 jwk['kid'] = _kid; 58 } 59 return jwk; 60 } 61 62 /// Full JWK including private key components. 63 /// 64 /// WARNING: This contains sensitive key material. Never log or expose. 65 /// Only use for secure storage. 66 Map<String, dynamic> get privateJwk { 67 final jwk = _ecPrivateKeyToJwk(privateKey, publicKey, algorithm); 68 if (_kid != null) { 69 jwk['kid'] = _kid; 70 } 71 return jwk; 72 } 73 74 @override 75 Future<String> createJwt( 76 Map<String, dynamic> header, 77 Map<String, dynamic> payload, 78 ) async { 79 // Build JWT header 80 final jwtHeader = <String, dynamic>{ 81 'typ': 'JWT', 82 'alg': algorithm, 83 ...header, 84 }; 85 if (_kid != null) { 86 jwtHeader['kid'] = _kid; 87 } 88 89 // Encode header and payload 90 final headerB64 = _base64UrlEncode(utf8.encode(json.encode(jwtHeader))); 91 final payloadB64 = _base64UrlEncode(utf8.encode(json.encode(payload))); 92 93 // Create signing input 94 final signingInput = '$headerB64.$payloadB64'; 95 final signingBytes = utf8.encode(signingInput); 96 97 // Sign with appropriate algorithm 98 final signature = _signEcdsa(signingBytes, privateKey, algorithm); 99 100 // Encode signature 101 final signatureB64 = _base64UrlEncode(signature); 102 103 // Return compact JWT 104 return '$signingInput.$signatureB64'; 105 } 106 107 /// Generates a new FlutterKey for the given algorithms. 108 /// 109 /// Returns a key supporting the first compatible algorithm from the list. 110 /// 111 /// Throws [UnsupportedError] if no compatible algorithm is found. 112 static Future<FlutterKey> generate(List<String> algs) async { 113 // Try algorithms in order 114 for (final alg in algs) { 115 switch (alg) { 116 case 'ES256': 117 return _generateECKey('ES256', 'P-256'); 118 case 'ES384': 119 return _generateECKey('ES384', 'P-384'); 120 case 'ES512': 121 return _generateECKey('ES512', 'P-521'); // Note: P-521, not P-512 122 case 'ES256K': 123 return _generateECKey('ES256K', 'secp256k1'); 124 } 125 } 126 127 throw UnsupportedError( 128 'No supported algorithm found in: ${algs.join(", ")}', 129 ); 130 } 131 132 /// Reconstructs a FlutterKey from serialized JWK data. 133 /// 134 /// This is used when restoring sessions from storage. 135 factory FlutterKey.fromJwk(Map<String, dynamic> jwk) { 136 final kty = jwk['kty'] as String?; 137 if (kty != 'EC') { 138 throw FormatException('Unsupported key type: $kty'); 139 } 140 141 final crv = jwk['crv'] as String?; 142 final alg = jwk['alg'] as String?; 143 final kid = jwk['kid'] as String?; 144 145 if (crv == null || alg == null) { 146 throw FormatException('Missing required JWK fields'); 147 } 148 149 // Parse key components 150 final x = _base64UrlDecode(jwk['x'] as String); 151 final y = _base64UrlDecode(jwk['y'] as String); 152 final d = jwk['d'] != null ? _base64UrlDecode(jwk['d'] as String) : null; 153 154 if (d == null) { 155 throw FormatException('Private key component (d) is required'); 156 } 157 158 // Get curve 159 final curve = _getCurveForName(crv); 160 161 // Reconstruct public key 162 final publicKey = pointycastle.ECPublicKey( 163 curve.curve.createPoint(_bytesToBigInt(x), _bytesToBigInt(y)), 164 curve, 165 ); 166 167 // Reconstruct private key 168 final privateKey = pointycastle.ECPrivateKey(_bytesToBigInt(d), curve); 169 170 return FlutterKey( 171 privateKey: privateKey, 172 publicKey: publicKey, 173 algorithm: alg, 174 kid: kid, 175 ); 176 } 177 178 /// Serializes this key to JSON (for session storage). 179 /// 180 /// WARNING: Contains private key material. Store securely. 181 Map<String, dynamic> toJson() => privateJwk; 182 183 // ============================================================================ 184 // Private helper methods 185 // ============================================================================ 186 187 /// Generates an EC key pair for the given algorithm and curve. 188 static Future<FlutterKey> _generateECKey( 189 String algorithm, 190 String curveName, 191 ) async { 192 final curve = _getCurveForName(curveName); 193 194 // Create secure random generator 195 final secureRandom = pointycastle.FortunaRandom(); 196 final random = Random.secure(); 197 final seeds = List<int>.generate(32, (_) => random.nextInt(256)); 198 secureRandom.seed(pointycastle.KeyParameter(Uint8List.fromList(seeds))); 199 200 // Generate key pair 201 final keyGen = pointycastle.ECKeyGenerator(); 202 keyGen.init( 203 pointycastle.ParametersWithRandom( 204 pointycastle.ECKeyGeneratorParameters(curve), 205 secureRandom, 206 ), 207 ); 208 209 final keyPair = keyGen.generateKeyPair(); 210 final privateKey = keyPair.privateKey as pointycastle.ECPrivateKey; 211 final publicKey = keyPair.publicKey as pointycastle.ECPublicKey; 212 213 return FlutterKey( 214 privateKey: privateKey, 215 publicKey: publicKey, 216 algorithm: algorithm, 217 ); 218 } 219 220 /// Gets the EC domain parameters for a given curve name. 221 static pointycastle.ECDomainParameters _getCurveForName(String name) { 222 // Use pointycastle's standard curve implementations 223 switch (name) { 224 case 'P-256': 225 case 'prime256v1': 226 case 'secp256r1': 227 return pointycastle.ECCurve_secp256r1(); 228 case 'P-384': 229 case 'secp384r1': 230 return pointycastle.ECCurve_secp384r1(); 231 case 'P-521': 232 case 'secp521r1': 233 return pointycastle.ECCurve_secp521r1(); 234 case 'secp256k1': 235 return pointycastle.ECCurve_secp256k1(); 236 default: 237 throw UnsupportedError('Unsupported curve: $name'); 238 } 239 } 240 241 /// Gets the curve name for JWK representation. 242 static String _getCurveName(String algorithm) { 243 switch (algorithm) { 244 case 'ES256': 245 return 'P-256'; 246 case 'ES384': 247 return 'P-384'; 248 case 'ES512': 249 return 'P-521'; 250 case 'ES256K': 251 return 'secp256k1'; 252 default: 253 throw UnsupportedError('Unsupported algorithm: $algorithm'); 254 } 255 } 256 257 /// Gets the hash algorithm for signing. 258 static String _getHashAlgorithm(String algorithm) { 259 switch (algorithm) { 260 case 'ES256': 261 case 'ES256K': 262 return 'SHA-256'; 263 case 'ES384': 264 return 'SHA-384'; 265 case 'ES512': 266 return 'SHA-512'; 267 default: 268 throw UnsupportedError('Unsupported algorithm: $algorithm'); 269 } 270 } 271 272 /// Signs data using ECDSA with deterministic signatures (RFC 6979). 273 /// 274 /// This uses deterministic ECDSA which doesn't require a source of randomness, 275 /// making it more secure and avoiding SecureRandom initialization issues. 276 static Uint8List _signEcdsa( 277 List<int> data, 278 pointycastle.ECPrivateKey privateKey, 279 String algorithm, 280 ) { 281 // Get the appropriate hash algorithm for this signing algorithm 282 final hashAlg = _getHashAlgorithm(algorithm); 283 284 // Build deterministic ECDSA signer name (e.g., "SHA-256/DET-ECDSA") 285 final signerName = '$hashAlg/DET-ECDSA'; 286 287 // Use deterministic ECDSA signer (RFC 6979) - no randomness required! 288 final signer = pointycastle.Signer(signerName); 289 signer.init( 290 true, // signing mode 291 pointycastle.PrivateKeyParameter<pointycastle.ECPrivateKey>(privateKey), 292 ); 293 294 // Sign the data (signer will hash it internally) 295 final signature = 296 signer.generateSignature(Uint8List.fromList(data)) 297 as pointycastle.ECSignature; 298 299 // Encode as IEEE P1363 format (r || s) 300 final r = _bigIntToBytes(signature.r, _getSignatureLength(algorithm)); 301 final s = _bigIntToBytes(signature.s, _getSignatureLength(algorithm)); 302 303 return Uint8List.fromList([...r, ...s]); 304 } 305 306 /// Creates a pointycastle Digest for the given hash algorithm. 307 static pointycastle.Digest _createDigest(String algorithm) { 308 switch (algorithm) { 309 case 'SHA-256': 310 return pointycastle.SHA256Digest(); 311 case 'SHA-384': 312 return pointycastle.SHA384Digest(); 313 case 'SHA-512': 314 return pointycastle.SHA512Digest(); 315 default: 316 throw UnsupportedError('Unsupported hash: $algorithm'); 317 } 318 } 319 320 /// Gets the signature length in bytes for the algorithm. 321 static int _getSignatureLength(String algorithm) { 322 switch (algorithm) { 323 case 'ES256': 324 case 'ES256K': 325 return 32; 326 case 'ES384': 327 return 48; 328 case 'ES512': 329 return 66; // P-521 uses 66 bytes per component 330 default: 331 throw UnsupportedError('Unsupported algorithm: $algorithm'); 332 } 333 } 334 335 /// Converts an EC public key to JWK format. 336 static Map<String, dynamic> _ecPublicKeyToJwk( 337 pointycastle.ECPublicKey publicKey, 338 String algorithm, 339 ) { 340 final q = publicKey.Q!; 341 final curve = _getCurveName(algorithm); 342 343 return { 344 'kty': 'EC', 345 'crv': curve, 346 'x': _base64UrlEncode(_bigIntToBytes(q.x!.toBigInteger()!)), 347 'y': _base64UrlEncode(_bigIntToBytes(q.y!.toBigInteger()!)), 348 'alg': algorithm, 349 'use': 'sig', 350 'key_ops': ['sign'], 351 }; 352 } 353 354 /// Converts an EC private key to JWK format (includes private component). 355 static Map<String, dynamic> _ecPrivateKeyToJwk( 356 pointycastle.ECPrivateKey privateKey, 357 pointycastle.ECPublicKey publicKey, 358 String algorithm, 359 ) { 360 final jwk = _ecPublicKeyToJwk(publicKey, algorithm); 361 jwk['d'] = _base64UrlEncode(_bigIntToBytes(privateKey.d!)); 362 return jwk; 363 } 364 365 /// Converts a BigInt to bytes with optional padding. 366 static Uint8List _bigIntToBytes(BigInt number, [int? length]) { 367 var bytes = _encodeBigInt(number); 368 369 if (length != null) { 370 if (bytes.length > length) { 371 // Remove leading zeros 372 bytes = bytes.sublist(bytes.length - length); 373 } else if (bytes.length < length) { 374 // Add leading zeros 375 final padded = Uint8List(length); 376 padded.setRange(length - bytes.length, length, bytes); 377 bytes = padded; 378 } 379 } 380 381 return bytes; 382 } 383 384 /// Encodes a BigInt as bytes (unsigned, big-endian). 385 static Uint8List _encodeBigInt(BigInt number) { 386 // Handle zero 387 if (number == BigInt.zero) { 388 return Uint8List.fromList([0]); 389 } 390 391 // Handle negative (should not happen for EC keys) 392 if (number.isNegative) { 393 throw ArgumentError('Cannot encode negative BigInt'); 394 } 395 396 // Convert to bytes 397 final bytes = <int>[]; 398 var n = number; 399 while (n > BigInt.zero) { 400 bytes.insert(0, (n & BigInt.from(0xff)).toInt()); 401 n = n >> 8; 402 } 403 404 return Uint8List.fromList(bytes); 405 } 406 407 /// Converts bytes to BigInt (unsigned, big-endian). 408 static BigInt _bytesToBigInt(List<int> bytes) { 409 var result = BigInt.zero; 410 for (var byte in bytes) { 411 result = (result << 8) | BigInt.from(byte); 412 } 413 return result; 414 } 415 416 /// Base64url encodes bytes (no padding). 417 static String _base64UrlEncode(List<int> bytes) { 418 return base64Url.encode(bytes).replaceAll('=', ''); 419 } 420 421 /// Base64url decodes a string. 422 static Uint8List _base64UrlDecode(String str) { 423 // Add padding if needed 424 var s = str; 425 switch (s.length % 4) { 426 case 2: 427 s += '=='; 428 break; 429 case 3: 430 s += '='; 431 break; 432 } 433 return base64Url.decode(s); 434 } 435}