Main coves client
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}