feat: Add JWK (JSON Web Key) conversion methods to key classes

This commit is contained in:
Jonas Roussel
2025-02-26 15:37:55 +01:00
parent 10fc90f3a2
commit d06103bde5
3 changed files with 213 additions and 14 deletions

View File

@ -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),

View File

@ -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<String> 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;
}
}

View File

@ -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<String, dynamic> 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<String, dynamic> toJWK({String? keyID, HMACAlgorithm? algorithm}) {
final keyBytes = decodeHMACSecret(key, isBase64Encoded);
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJWK({String? keyID}) {
Map<String, dynamic> 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<int> bytes) : key = ed.PublicKey(bytes);
EdDSAPublicKey.fromPEM(String pem) : key = KeyParser.edPublicKeyFromPEM(pem);
@override
Map<String, dynamic> toJWK({String? keyID}) {
Map<String, dynamic> jwk = {
'kty': 'OKP',
'use': 'sig',
'crv': 'Ed25519',
'x': base64Unpadded(base64Url.encode(key.bytes)),
'alg': 'EdDSA',
};
if (keyID != null) jwk['kid'] = keyID;
return jwk;
}
}