Files
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

153 lines
4.5 KiB
Dart

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 = <int>[];
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<int> b) => base64Url.encode(b).replaceAll("=", "");
AsymmetricKeyPair<PublicKey, PrivateKey> _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<String, dynamic> header,
Map<String, dynamic> 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<RSAPrivateKey>(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<IapError>()));
expect(
() => verifyLicence(good, jwks,
expectedApp: "my-app", expectedSub: "someone-else"),
throwsA(isA<IapError>()));
final tampered = "${good.substring(0, good.length - 4)}AAAA";
expect(
() => verifyLicence(tampered, jwks,
expectedApp: "my-app", expectedSub: "u"),
throwsA(isA<IapError>()));
});
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")));
});
}