From d627210916507fb0f0bf2b5a7f8c4bbdde0f99cd Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 14 Sep 2026 21:44:28 +1000 Subject: [PATCH 1/2] Cache DPoP key parsing --- lib/src/dpop/dpop_key_manager.dart | 4 ++++ lib/src/dpop/dpop_token_generator.dart | 32 +++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/src/dpop/dpop_key_manager.dart b/lib/src/dpop/dpop_key_manager.dart index 87bd59d..f18f787 100644 --- a/lib/src/dpop/dpop_key_manager.dart +++ b/lib/src/dpop/dpop_key_manager.dart @@ -33,6 +33,8 @@ library; import 'package:fast_rsa/fast_rsa.dart'; import 'package:logging/logging.dart'; +import 'package:solid_auth/src/dpop/dpop_token_generator.dart'; + final _log = Logger('solid_auth.DpopKeyManager'); /// Manages the RSA key pair used for DPoP proofs. @@ -96,12 +98,14 @@ class DpopKeyManager { /// Use on logout or to rotate the DPoP binding key. static Future rotate() async { _instance = null; + DpopTokenGenerator.clearCachedSigningKey(); return getInstance(); } /// Clears the cached instance (call on logout). static void clear() { _instance = null; + DpopTokenGenerator.clearCachedSigningKey(); } /// Restores the singleton from previously persisted PEM-encoded keys. diff --git a/lib/src/dpop/dpop_token_generator.dart b/lib/src/dpop/dpop_token_generator.dart index aca5e41..2151e93 100644 --- a/lib/src/dpop/dpop_token_generator.dart +++ b/lib/src/dpop/dpop_token_generator.dart @@ -183,13 +183,43 @@ abstract class DpopTokenGenerator { /// Sign the JWT using private key return jwt.sign( - RSAPrivateKey(keyPair.privateKey), + _signingKeyFor(keyPair.privateKey), algorithm: JWTAlgorithm.RS256, ); } + /// Forget the cached signing key. + static void clearCachedSigningKey() { + _cachedPrivateKeyPem = null; + _cachedSigningKey = null; + } + // ── Internal ─────────────────────────────────────────────────────────────── + static String? _cachedPrivateKeyPem; + static RSAPrivateKey? _cachedSigningKey; + + /// The parsed signing key for [privateKeyPem], reusing the last one. + /// + /// Every protected-resource request carries its own DPoP proof, so this runs + /// once per HTTP request. Building an [RSAPrivateKey] re-decodes the PEM and + /// its ASN.1 structure each time, which is pure Dart arbitrary-precision + /// work and is markedly slower on the web and on low-powered devices than on + /// a desktop. The key pair changes only on login or rotation, so parse it + /// once and keep it. + static RSAPrivateKey _signingKeyFor(String privateKeyPem) { + final cached = _cachedSigningKey; + if (cached != null && _cachedPrivateKeyPem == privateKeyPem) { + return cached; + } + + final key = RSAPrivateKey(privateKeyPem); + _cachedPrivateKeyPem = privateKeyPem; + _cachedSigningKey = key; + + return key; + } + /// Returns the base64url-encoded SHA-256 hash of [input] (ASCII encoded). /// Used for the `ath` claim per RFC 9449 §4.2. static String _sha256Base64Url(String input) { From 7b8a711e34862f2ae0e1dc1649dc325cc8f011f8 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 15 Sep 2026 23:16:00 +1000 Subject: [PATCH 2/2] Cancel the caching of DPoP key parsing --- lib/src/dpop/dpop_key_manager.dart | 4 ---- lib/src/dpop/dpop_token_generator.dart | 32 +------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/lib/src/dpop/dpop_key_manager.dart b/lib/src/dpop/dpop_key_manager.dart index f18f787..87bd59d 100644 --- a/lib/src/dpop/dpop_key_manager.dart +++ b/lib/src/dpop/dpop_key_manager.dart @@ -33,8 +33,6 @@ library; import 'package:fast_rsa/fast_rsa.dart'; import 'package:logging/logging.dart'; -import 'package:solid_auth/src/dpop/dpop_token_generator.dart'; - final _log = Logger('solid_auth.DpopKeyManager'); /// Manages the RSA key pair used for DPoP proofs. @@ -98,14 +96,12 @@ class DpopKeyManager { /// Use on logout or to rotate the DPoP binding key. static Future rotate() async { _instance = null; - DpopTokenGenerator.clearCachedSigningKey(); return getInstance(); } /// Clears the cached instance (call on logout). static void clear() { _instance = null; - DpopTokenGenerator.clearCachedSigningKey(); } /// Restores the singleton from previously persisted PEM-encoded keys. diff --git a/lib/src/dpop/dpop_token_generator.dart b/lib/src/dpop/dpop_token_generator.dart index 2151e93..aca5e41 100644 --- a/lib/src/dpop/dpop_token_generator.dart +++ b/lib/src/dpop/dpop_token_generator.dart @@ -183,43 +183,13 @@ abstract class DpopTokenGenerator { /// Sign the JWT using private key return jwt.sign( - _signingKeyFor(keyPair.privateKey), + RSAPrivateKey(keyPair.privateKey), algorithm: JWTAlgorithm.RS256, ); } - /// Forget the cached signing key. - static void clearCachedSigningKey() { - _cachedPrivateKeyPem = null; - _cachedSigningKey = null; - } - // ── Internal ─────────────────────────────────────────────────────────────── - static String? _cachedPrivateKeyPem; - static RSAPrivateKey? _cachedSigningKey; - - /// The parsed signing key for [privateKeyPem], reusing the last one. - /// - /// Every protected-resource request carries its own DPoP proof, so this runs - /// once per HTTP request. Building an [RSAPrivateKey] re-decodes the PEM and - /// its ASN.1 structure each time, which is pure Dart arbitrary-precision - /// work and is markedly slower on the web and on low-powered devices than on - /// a desktop. The key pair changes only on login or rotation, so parse it - /// once and keep it. - static RSAPrivateKey _signingKeyFor(String privateKeyPem) { - final cached = _cachedSigningKey; - if (cached != null && _cachedPrivateKeyPem == privateKeyPem) { - return cached; - } - - final key = RSAPrivateKey(privateKeyPem); - _cachedPrivateKeyPem = privateKeyPem; - _cachedSigningKey = key; - - return key; - } - /// Returns the base64url-encoded SHA-256 hash of [input] (ASCII encoded). /// Used for the `ath` claim per RFC 9449 §4.2. static String _sha256Base64Url(String input) {