From 997fe84f7f5902fc9962939f5c1aeb293c062b33 Mon Sep 17 00:00:00 2001 From: Jonas Roussel Date: Fri, 12 Jun 2020 13:35:07 +0200 Subject: [PATCH] v1.0.0 --- CHANGELOG.md | 7 +++ README.md | 17 +++++-- example/example.dart | 4 +- lib/dart_jsonwebtoken.dart | 1 + lib/src/algorithms.dart | 98 ++++++++++++++++++++++++++++++++++++-- lib/src/jwt.dart | 60 +++++++++++------------ lib/src/keys.dart | 21 ++++++++ pubspec.yaml | 6 ++- 8 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 lib/src/keys.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e9c17..e9d98f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0 + +- New RSA Algorithm (RS256) +- Keys a now using an abstract class 'Key' instead of a string +- SecretKey: for HMAC (HS256) +- PrivateKey & PublicKey: for RSA (RS256) + ## 0.2.1 - Formatting diff --git a/README.md b/README.md index 7e9083b..20fcc09 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -# JsonWebToken +# JSON Web Token [![pub package](https://img.shields.io/pub/v/dart_jsonwebtoken.svg)](https://pub.dev/packages/dart_jsonwebtoken) A dart implementation of the famous javascript library `jsonwebtoken`. +JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. +https://jwt.io allows you to decode, verify and generate JWT. + ## Usage ### Import @@ -25,8 +28,8 @@ final jwt = JWT( issuer: 'https://github.com/jonasroussel/jsonwebtoken', ); -// Sign it -token = jwt.sign('secret-key'); +// Sign it (default with HS256 algorithm) +token = jwt.sign(SecretKey('secret passphrase')); print('Signed token: $token\n'); ``` @@ -36,7 +39,7 @@ print('Signed token: $token\n'); ```dart try { // Verify a token - final jwt = JWT.verify(token, 'secret-key'); + final jwt = JWT.verify(token, SecretKey('secret passphrase')); print('Payload: ${jwt.payload}'); } on JWTExpiredError { @@ -45,3 +48,9 @@ try { print(ex.message); // ex: invalid signature } ``` + + +### Supported Algorithms + +- HS256 (HMAC / SHA256) +- RS256 (RSA / SHA256) diff --git a/example/example.dart b/example/example.dart index 8dd45f1..657bfa7 100644 --- a/example/example.dart +++ b/example/example.dart @@ -17,7 +17,7 @@ main() { ); // Sign it - token = jwt.sign('secret-key'); + token = jwt.sign(SecretKey('secret passphrase')); print('Signed token: $token\n'); } @@ -25,7 +25,7 @@ main() { /* Verify */ { try { // Verify a token - final jwt = JWT.verify(token, 'secret-key'); + final jwt = JWT.verify(token, SecretKey('secret passphrase')); print('Payload: ${jwt.payload}'); } on JWTExpiredError { diff --git a/lib/dart_jsonwebtoken.dart b/lib/dart_jsonwebtoken.dart index ef29fd4..56a5f3f 100644 --- a/lib/dart_jsonwebtoken.dart +++ b/lib/dart_jsonwebtoken.dart @@ -3,3 +3,4 @@ library jsonwebtoken; export 'src/jwt.dart'; export 'src/errors.dart'; export 'src/algorithms.dart'; +export 'src/keys.dart'; diff --git a/lib/src/algorithms.dart b/lib/src/algorithms.dart index c3d87c5..49971e7 100644 --- a/lib/src/algorithms.dart +++ b/lib/src/algorithms.dart @@ -1,15 +1,21 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; +import 'package:pointycastle/pointycastle.dart' hide PrivateKey, PublicKey; +import 'package:rsa_pkcs/rsa_pkcs.dart' hide RSAPrivateKey, RSAPublicKey; abstract class JWTAlgorithm { static const HS256 = HS256Algorithm(); + static const RS256 = RS256Algorithm(); static JWTAlgorithm fromName(String name) { switch (name) { case 'HS256': return JWTAlgorithm.HS256; + case 'RS256': + return JWTAlgorithm.RS256; default: throw JWTInvalidError('unknown algorithm'); } @@ -18,8 +24,8 @@ abstract class JWTAlgorithm { const JWTAlgorithm(); String get name; - List sign(String key, List body); - bool verify(String key, List body, List signature); + List sign(Key key, List body); + bool verify(Key key, List body, List signature); } class HS256Algorithm extends JWTAlgorithm { @@ -29,13 +35,18 @@ class HS256Algorithm extends JWTAlgorithm { String get name => 'HS256'; @override - List sign(String key, List body) { - final hmac = Hmac(sha256, utf8.encode(key)); + List sign(Key key, List body) { + assert(key is SecretKey, 'key must be a SecretKey'); + final secretKey = key as SecretKey; + + final hmac = Hmac(sha256, utf8.encode(secretKey.key)); return hmac.convert(body).bytes; } @override - bool verify(String key, List body, List signature) { + bool verify(Key key, List body, List signature) { + assert(key is SecretKey, 'key must be a SecretKey'); + final actual = sign(key, body); if (actual.length != signature.length) return false; @@ -47,3 +58,80 @@ class HS256Algorithm extends JWTAlgorithm { return true; } } + +class RS256Algorithm extends JWTAlgorithm { + const RS256Algorithm(); + + @override + String get name => 'RS256'; + + @override + List sign(Key key, List body) { + assert(key is PrivateKey, 'key must be a PrivateKey'); + final privateKey = key as PrivateKey; + + final parser = RSAPKCSParser(); + RSAKeyPair pair; + + try { + pair = parser.parsePEM(privateKey.key, password: privateKey.passphrase); + assert(pair.private != null); + } catch (ex) { + throw JWTInvalidError('invalid private RSA key'); + } + + final signer = Signer('SHA-256/RSA'); + final params = ParametersWithRandom( + PrivateKeyParameter( + RSAPrivateKey( + pair.private.modulus, + pair.private.privateExponent, + pair.private.prime1, + pair.private.prime2, + ), + ), + SecureRandom('AES/CTR/PRNG'), + ); + + signer.init(true, params); + + RSASignature signature = signer.generateSignature(Uint8List.fromList(body)); + + return signature.bytes.toList(growable: false); + } + + @override + bool verify(Key key, List body, List signature) { + assert(key is PublicKey, 'key must be a PublicKey'); + final publicKey = key as PublicKey; + + final parser = RSAPKCSParser(); + RSAKeyPair pair; + + try { + pair = parser.parsePEM(publicKey.key, password: publicKey.passphrase); + assert(pair.public != null); + } catch (ex) { + throw JWTInvalidError('invalid public RSA key'); + } + + try { + final signer = Signer('SHA-256/RSA'); + final params = ParametersWithRandom( + PublicKeyParameter( + RSAPublicKey( + pair.public.modulus, + BigInt.from(pair.public.publicExponent), + ), + ), + SecureRandom('AES/CTR/PRNG'), + ); + + signer.init(false, params); + + return signer.verifySignature(Uint8List.fromList(body), RSASignature(Uint8List.fromList(signature))); + } catch (ex) { + return false; + } + } +} diff --git a/lib/src/jwt.dart b/lib/src/jwt.dart index fb22bd1..5b9a601 100644 --- a/lib/src/jwt.dart +++ b/lib/src/jwt.dart @@ -5,45 +5,39 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import './utils.dart'; class JWT { - static JWT verify(String token, String key) { - try { - final parts = token.split('.'); + static JWT verify(String token, Key key) { + final parts = token.split('.'); - final header = Map.from(jsonBase64.decode(base64Padded(parts[0]))); + final header = Map.from(jsonBase64.decode(base64Padded(parts[0]))); - if (header['typ'] != 'JWT') throw JWTInvalidError('not a jwt'); + if (header['typ'] != 'JWT') throw JWTInvalidError('not a jwt'); - final algorithm = JWTAlgorithm.fromName(header['alg']); + final algorithm = JWTAlgorithm.fromName(header['alg']); - if (parts.length < 3) throw JWTInvalidError('jwt malformated'); + if (parts.length < 3) throw JWTInvalidError('jwt malformated'); - final body = utf8.encode(parts[0] + '.' + parts[1]); - final signature = base64Url.decode(base64Padded(parts[2])); + final body = utf8.encode(parts[0] + '.' + parts[1]); + final signature = base64Url.decode(base64Padded(parts[2])); - if (!algorithm.verify(key, body, signature)) { - throw JWTInvalidError('invalid signature'); - } - - final payload = Map.from(jsonBase64.decode(base64Padded(parts[1]))); - - if (payload.containsKey('exp')) { - final exp = DateTime.fromMillisecondsSinceEpoch(payload['exp'] * 1000); - if (exp.isBefore(DateTime.now())) { - throw JWTExpiredError(); - } - } - - return JWT( - payload: payload, - audience: payload.remove('aud'), - issuer: payload.remove('iss'), - subject: payload.remove('sub'), - ); - } on JWTError { - rethrow; - } catch (ex) { - throw JWTInvalidError('jwt invalid'); + if (!algorithm.verify(key, body, signature)) { + throw JWTInvalidError('invalid signature'); } + + final payload = Map.from(jsonBase64.decode(base64Padded(parts[1]))); + + if (payload.containsKey('exp')) { + final exp = DateTime.fromMillisecondsSinceEpoch(payload['exp'] * 1000); + if (exp.isBefore(DateTime.now())) { + throw JWTExpiredError(); + } + } + + return JWT( + payload: payload, + audience: payload.remove('aud'), + issuer: payload.remove('iss'), + subject: payload.remove('sub'), + ); } JWT({ @@ -59,7 +53,7 @@ class JWT { final String issuer; String sign( - String key, { + Key key, { JWTAlgorithm algorithm = JWTAlgorithm.HS256, Duration expiresIn, bool noTimestamp = false, diff --git a/lib/src/keys.dart b/lib/src/keys.dart new file mode 100644 index 0000000..46f3d98 --- /dev/null +++ b/lib/src/keys.dart @@ -0,0 +1,21 @@ +abstract class Key {} + +class SecretKey extends Key { + String key; + + SecretKey(this.key); +} + +class PrivateKey extends Key { + String key; + String passphrase; + + PrivateKey(this.key, [this.passphrase = '']); +} + +class PublicKey extends Key { + String key; + String passphrase; + + PublicKey(this.key, [this.passphrase = '']); +} diff --git a/pubspec.yaml b/pubspec.yaml index 75d2d78..ba39d5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,13 @@ name: dart_jsonwebtoken description: A dart implementation of the famous javascript library 'jsonwebtoken'. -version: 0.2.1 +version: 1.0.0 repository: https://github.com/jonasroussel/jsonwebtoken homepage: https://github.com/jonasroussel/jsonwebtoken#readme environment: - sdk: '>=2.7.0 <3.0.0' + sdk: '>=2.3.0 <3.0.0' dependencies: crypto: ^2.1.5 + rsa_pkcs: ^1.1.3 + pointycastle: ^1.0.2