Files
Garage-SDKs/garage_entitlements/test/keys.dart
T
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

97 lines
2.8 KiB
Dart

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 = <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 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)}";
}
Map<String, dynamic> 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,
});
}