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
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user