diff --git a/lib/src/algorithms.dart b/lib/src/algorithms.dart index 4b6cbba..95b26fe 100644 --- a/lib/src/algorithms.dart +++ b/lib/src/algorithms.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; @@ -7,8 +6,8 @@ import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; import 'package:pointycastle/pointycastle.dart' as pc; import 'exceptions.dart'; -import 'keys.dart'; import 'helpers.dart'; +import 'keys.dart'; abstract class JWTAlgorithm { /// HMAC using SHA-256 hash algorithm @@ -147,17 +146,7 @@ class HMACAlgorithm extends JWTAlgorithm { assert(key is SecretKey, 'key must be a SecretKey'); final secretKey = key as SecretKey; - Uint8List keyBytes; - - if (secretKey.isBase64Encoded) { - if (RegExp(r'-|_+').hasMatch(secretKey.key)) { - keyBytes = base64Url.decode(secretKey.key); - } else { - keyBytes = base64.decode(secretKey.key); - } - } else { - keyBytes = utf8.encode(secretKey.key); - } + final keyBytes = decodeHMACSecret(secretKey.key, secretKey.isBase64Encoded); final hmac = Hmac( _getHash(name), diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart index 72bb4ad..3722901 100644 --- a/lib/src/helpers.dart +++ b/lib/src/helpers.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'algorithms.dart'; + final jsonBase64 = json.fuse(utf8.fuse(base64Url)); String base64Unpadded(String value) { @@ -97,3 +99,48 @@ List chunkString(String s, int chunkSize) { } return chunked; } + +Uint8List decodeHMACSecret(String secret, bool isBase64Encoded) { + if (isBase64Encoded) { + if (RegExp(r'-|_+').hasMatch(secret)) { + return base64Url.decode(secret); + } else { + return base64.decode(secret); + } + } else { + return utf8.encode(secret); + } +} + +String curveOpenSSLToNIST(String curveName) { + switch (curveName) { + case "prime256v1": + case "secp256r1": + return "P-256"; + case "secp384r1": + return "P-384"; + case "secp521r1": + return "P-521"; + case "secp192r1": + return "P-192"; + case "secp224r1": + return "P-224"; + default: + return curveName; // Return the original name if not found + } +} + +ECDSAAlgorithm? ecCurveToAlgorithm(String curveName) { + switch (curveName) { + case "P-256": + return JWTAlgorithm.ES256; + case "P-384": + return JWTAlgorithm.ES384; + case "P-521": + return JWTAlgorithm.ES512; + case "secp256k1": + return JWTAlgorithm.ES256K; + default: + return null; + } +} diff --git a/lib/src/keys.dart b/lib/src/keys.dart index 966352e..f0db1ec 100644 --- a/lib/src/keys.dart +++ b/lib/src/keys.dart @@ -1,12 +1,17 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; import 'package:pointycastle/pointycastle.dart' as pc; +import 'algorithms.dart'; import 'exceptions.dart'; +import 'helpers.dart'; import 'key_parser.dart'; -abstract class JWTKey {} +abstract class JWTKey { + Map toJWK({String? keyID}); +} /// For HMAC algorithms class SecretKey extends JWTKey { @@ -14,6 +19,22 @@ class SecretKey extends JWTKey { bool isBase64Encoded; SecretKey(this.key, {this.isBase64Encoded = false}); + + @override + Map toJWK({String? keyID, HMACAlgorithm? algorithm}) { + final keyBytes = decodeHMACSecret(key, isBase64Encoded); + + Map jwk = { + 'kty': 'oct', + 'use': 'sig', + 'k': base64Url.encode(keyBytes), + }; + + if (keyID != null) jwk['kid'] = keyID; + if (algorithm != null) jwk['alg'] = algorithm.name; + + return jwk; + } } /// For RSA algorithm, in sign method @@ -30,6 +51,44 @@ class RSAPrivateKey extends JWTKey { RSAPrivateKey.raw(pc.RSAPrivateKey _key) : key = _key; RSAPrivateKey.clone(RSAPrivateKey _key) : key = _key.key; RSAPrivateKey.bytes(Uint8List bytes) : key = KeyParser.rsaPrivateKey(bytes); + + @override + Map toJWK({String? keyID, RSAAlgorithm? algorithm}) { + final p = key.p; + if (p == null) throw ArgumentError('p is null'); + final q = key.q; + if (q == null) throw ArgumentError('q is null'); + final n = key.n; + if (n == null) throw ArgumentError('n is null'); + final e = key.publicExponent; + if (e == null) throw ArgumentError('e is null'); + final d = key.privateExponent; + if (d == null) throw ArgumentError('d is null'); + final dp = d % (p - BigInt.one); + final dq = d % (q - BigInt.one); + final qi = q.modInverse(p); + + Map jwk = { + 'kty': 'RSA', + 'use': 'sig', + 'p': base64Unpadded(base64Url.encode(bigIntToBytes(p).reversed.toList())), + 'q': base64Unpadded(base64Url.encode(bigIntToBytes(q).reversed.toList())), + 'd': base64Unpadded(base64Url.encode(bigIntToBytes(d).reversed.toList())), + 'e': base64Unpadded(base64Url.encode(bigIntToBytes(e).reversed.toList())), + 'dp': + base64Unpadded(base64Url.encode(bigIntToBytes(dp).reversed.toList())), + 'dq': + base64Unpadded(base64Url.encode(bigIntToBytes(dq).reversed.toList())), + 'qi': + base64Unpadded(base64Url.encode(bigIntToBytes(qi).reversed.toList())), + 'n': base64Unpadded(base64Url.encode(bigIntToBytes(n).reversed.toList())), + }; + + if (keyID != null) jwk['kid'] = keyID; + if (algorithm != null) jwk['alg'] = algorithm.name; + + return jwk; + } } /// For RSA algorithm, in verify method @@ -57,6 +116,26 @@ class RSAPublicKey extends JWTKey { key = RSAPublicKey.bytes(bytes).key; } + + @override + Map toJWK({String? keyID, RSAAlgorithm? algorithm}) { + final e = key.publicExponent; + if (e == null) throw ArgumentError('e is null'); + final n = key.modulus; + if (n == null) throw ArgumentError('n is null'); + + Map jwk = { + 'kty': 'RSA', + 'use': 'sig', + 'e': base64Unpadded(base64Url.encode(bigIntToBytes(e).reversed.toList())), + 'n': base64Unpadded(base64Url.encode(bigIntToBytes(n).reversed.toList())), + }; + + if (keyID != null) jwk['kid'] = keyID; + if (algorithm != null) jwk['alg'] = algorithm.name; + + return jwk; + } } /// For ECDSA algorithm, in sign method @@ -93,6 +172,36 @@ class ECPrivateKey extends JWTKey { : key = _key.key, size = _key.size; ECPrivateKey.bytes(Uint8List bytes) : key = KeyParser.ecPrivateKey(bytes); + + @override + Map toJWK({String? keyID, ECDSAAlgorithm? algorithm}) { + final params = key.parameters; + if (params == null) throw ArgumentError('parameters is null'); + final curve = curveOpenSSLToNIST(params.domainName); + final d = key.d; + if (d == null) throw ArgumentError('d is null'); + final Q = params.G * d; + if (Q == null) throw ArgumentError('Q is null'); + final x = Q.x?.toBigInteger(); + if (x == null) throw ArgumentError('x is null'); + final y = Q.y?.toBigInteger(); + if (y == null) throw ArgumentError('y is null'); + + Map jwk = { + 'kty': 'EC', + 'use': 'sig', + 'crv': curve, + 'd': base64Unpadded(base64Url.encode(bigIntToBytes(d).reversed.toList())), + 'x': base64Unpadded(base64Url.encode(bigIntToBytes(x).reversed.toList())), + 'y': base64Unpadded(base64Url.encode(bigIntToBytes(y).reversed.toList())), + }; + + if (keyID != null) jwk['kid'] = keyID; + final alg = algorithm?.name ?? ecCurveToAlgorithm(curve)?.name; + if (alg != null) jwk['alg'] = alg; + + return jwk; + } } /// For ECDSA algorithm, in verify method @@ -111,6 +220,29 @@ class ECPublicKey extends JWTKey { key = ECPublicKey.bytes(bytes).key; } + + @override + Map toJWK({String? keyID, ECDSAAlgorithm? algorithm}) { + final curve = key.parameters?.domainName; + if (curve == null) throw ArgumentError('curve is null'); + final x = key.Q?.x?.toBigInteger(); + if (x == null) throw ArgumentError('x is null'); + final y = key.Q?.y?.toBigInteger(); + if (y == null) throw ArgumentError('y is null'); + + Map jwk = { + 'kty': 'EC', + 'use': 'sig', + 'crv': curveOpenSSLToNIST(curve), + 'x': base64Unpadded(base64Url.encode(bigIntToBytes(x).reversed.toList())), + 'y': base64Unpadded(base64Url.encode(bigIntToBytes(y).reversed.toList())), + }; + + if (keyID != null) jwk['kid'] = keyID; + if (algorithm != null) jwk['alg'] = algorithm.name; + + return jwk; + } } /// For EdDSA algorithm, in sign method @@ -121,6 +253,22 @@ class EdDSAPrivateKey extends JWTKey { EdDSAPrivateKey.fromPEM(String pem) : key = KeyParser.edPrivateKeyFromPEM(pem); + + @override + Map toJWK({String? keyID}) { + Map jwk = { + 'kty': 'OKP', + 'use': 'sig', + 'crv': 'Ed25519', + 'd': base64Unpadded(base64Url.encode(key.bytes.sublist(0, 32))), + 'x': base64Unpadded(base64Url.encode(key.bytes.sublist(32))), + 'alg': 'EdDSA', + }; + + if (keyID != null) jwk['kid'] = keyID; + + return jwk; + } } /// For EdDSA algorithm, in verify method @@ -130,4 +278,19 @@ class EdDSAPublicKey extends JWTKey { EdDSAPublicKey(List bytes) : key = ed.PublicKey(bytes); EdDSAPublicKey.fromPEM(String pem) : key = KeyParser.edPublicKeyFromPEM(pem); + + @override + Map toJWK({String? keyID}) { + Map jwk = { + 'kty': 'OKP', + 'use': 'sig', + 'crv': 'Ed25519', + 'x': base64Unpadded(base64Url.encode(key.bytes)), + 'alg': 'EdDSA', + }; + + if (keyID != null) jwk['kid'] = keyID; + + return jwk; + } }