import "dart:convert"; import "dart:typed_data"; import "package:flutter_test/flutter_test.dart"; import "package:garage_iap/src/jwks_verify.dart"; import "package:garage_iap/src/models.dart"; import "package:pointycastle/export.dart"; 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 with 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)}"; } void main() { test("verifies a signed licence and reads products", () { final pair = _genKey(1); final pub = pair.publicKey as RSAPublicKey; final priv = pair.privateKey as RSAPrivateKey; final jwks = { "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "k1", "n": _b64uBig(pub.modulus!), "e": _b64uBig(pub.exponent!) } ] }; final now = DateTime.now().toUtc(); final token = _signJwt(priv, { "alg": "RS256", "kid": "k1" }, { "sub": "user-123", "app": "my-app", "products": [ {"sku": "pro", "kind": "app"} ], "exp": now.add(const Duration(hours: 24)).millisecondsSinceEpoch ~/ 1000, }); final lic = verifyLicence(token, jwks, expectedApp: "my-app", expectedSub: "user-123"); expect(lic.grants("pro"), isTrue); expect(lic.grants("nope"), isFalse); expect(lic.isExpired, isFalse); }); test("rejects wrong app, wrong sub, and a tampered signature", () { final pair = _genKey(7); final pub = pair.publicKey as RSAPublicKey; final priv = pair.privateKey as RSAPrivateKey; final jwks = { "keys": [ { "kty": "RSA", "kid": "k1", "alg": "RS256", "n": _b64uBig(pub.modulus!), "e": _b64uBig(pub.exponent!) } ] }; final now = DateTime.now().toUtc(); final exp = now.add(const Duration(hours: 1)).millisecondsSinceEpoch ~/ 1000; final good = _signJwt(priv, {"alg": "RS256", "kid": "k1"}, {"sub": "u", "app": "my-app", "products": [], "exp": exp}); expect( () => verifyLicence(good, jwks, expectedApp: "other", expectedSub: "u"), throwsA(isA())); expect( () => verifyLicence(good, jwks, expectedApp: "my-app", expectedSub: "someone-else"), throwsA(isA())); final tampered = "${good.substring(0, good.length - 4)}AAAA"; expect( () => verifyLicence(tampered, jwks, expectedApp: "my-app", expectedSub: "u"), throwsA(isA())); }); test("rejects an expired licence", () { final pair = _genKey(3); final pub = pair.publicKey as RSAPublicKey; final priv = pair.privateKey as RSAPrivateKey; final jwks = { "keys": [ { "kty": "RSA", "kid": "k1", "alg": "RS256", "n": _b64uBig(pub.modulus!), "e": _b64uBig(pub.exponent!) } ] }; final past = DateTime.now() .toUtc() .subtract(const Duration(hours: 1)) .millisecondsSinceEpoch ~/ 1000; final token = _signJwt(priv, {"alg": "RS256", "kid": "k1"}, {"sub": "u", "app": "my-app", "products": [], "exp": past}); expect( () => verifyLicence(token, jwks, expectedApp: "my-app", expectedSub: "u"), throwsA(predicate((e) => e is IapError && e.code == "expired"))); }); }