import "dart:convert"; import "dart:typed_data"; import "package:pointycastle/export.dart"; // A keypair + a signer, shared by the tests. Lifted from garage_iap's verify // test — generating a real 2048 bit RSA key beats a fixture, since the whole // point is that we verify the same way the backend signs. String b64uBig(BigInt n) { final bytes = []; var v = n; while (v > BigInt.zero) { bytes.insert(0, (v & BigInt.from(0xff)).toInt()); v = v >> 8; } return base64Url.encode(Uint8List.fromList(bytes)).replaceAll("=", ""); } String b64uStr(String s) => base64Url.encode(utf8.encode(s)).replaceAll("=", ""); String b64uBytes(List b) => base64Url.encode(b).replaceAll("=", ""); AsymmetricKeyPair genKey(int seed) { final rng = SecureRandom("Fortuna") ..seed( KeyParameter(Uint8List.fromList(List.generate(32, (i) => (i + seed) & 0xff))), ); final gen = RSAKeyGenerator() ..init( ParametersWithRandom( RSAKeyGeneratorParameters(BigInt.parse("65537"), 2048, 64), rng, ), ); return gen.generateKeyPair(); } // build a compact RS256 JWT the same way the backend does — header.payload // signed PKCS1v15 SHA-256. String signJwt( RSAPrivateKey priv, Map header, Map payload, ) { final h = b64uStr(jsonEncode(header)); final p = b64uStr(jsonEncode(payload)); final input = utf8.encode("$h.$p"); final signer = Signer("SHA-256/RSA") as RSASigner; signer.init(true, PrivateKeyParameter(priv)); final sig = signer.generateSignature(Uint8List.fromList(input)); return "$h.$p.${b64uBytes(sig.bytes)}"; } Map jwksOf(RSAPublicKey pub, {String kid = "k1"}) => { "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": kid, "n": b64uBig(pub.modulus!), "e": b64uBig(pub.exponent!), }, ], }; /// A key the way the store mints them. Everything is overridable so a test can /// break exactly one claim. String keyToken( RSAPrivateKey priv, { String kid = "k1", String sub = "user-123", String iss = "https://pay.imbenji.net", String project = "field-notes", String sku = "pro", String kind = "one_off", String mode = "live", String? entitlementExpiresAt, Duration life = const Duration(hours: 1), String? audOverride, }) { final now = DateTime.now().toUtc(); return signJwt(priv, {"alg": "RS256", "kid": kid, "typ": "JWT"}, { "sub": sub, "iss": iss, "aud": audOverride ?? "$project/$sku", "project": project, "sku": sku, "kind": kind, "mode": mode, if (entitlementExpiresAt != null) "expires_at": entitlementExpiresAt, "iat": now.millisecondsSinceEpoch ~/ 1000, "exp": now.add(life).millisecondsSinceEpoch ~/ 1000, }); }