This commit is contained in:
Jonas Roussel
2021-02-10 18:16:10 +01:00
parent eb4da9be66
commit 85077b96e5
16 changed files with 675 additions and 158 deletions

View File

@ -1,3 +1,12 @@
## 1.6.0
- New ECDSA Algorithm (EC256, EC384, EC512)
- ECPrivateKey and ECPublicKey, two new keys for ECDSA algorithm
- PrivateKey is renamed in RSAPrivateKey
- PublicKey is renamed in RSAPublicKey
- Optimization of private & public keys parsing
- `rsa_pkcs` & `cryptography` have been removed
## 1.5.0 ## 1.5.0
- Debuging `_TypeError issue on sign method` (#4) - Debuging `_TypeError issue on sign method` (#4)

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Jonas Roussel Copyright (c) 2021 Jonas Roussel
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -4,6 +4,7 @@
A dart implementation of the famous javascript library `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. 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. https://jwt.io allows you to decode, verify and generate JWT.
## Usage ## Usage
@ -49,13 +50,16 @@ try {
} }
``` ```
### Supported Algorithms ### Supported Algorithms
- HS256 (HMAC / SHA256) JWTAlgorithm | Digital Signature or MAC Algorithm
- HS384 (HMAC / SHA384) -------------|-----------------------------------
- HS512 (HMAC / SHA512) HS256 | HMAC using SHA-256 hash algorithm
HS384 | HMAC using SHA-384 hash algorithm
- RS256 (RSA / SHA256) HS512 | HMAC using SHA-512 hash algorithm
- RS384 (RSA / SHA384) RS256 | RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm
- RS512 (RSA / SHA512) RS384 | RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm
RS512 | RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm
ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm
ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm
ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm

5
example/ec_private.pem Normal file
View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----

4
example/ec_public.pem Normal file
View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----

View File

@ -1,7 +1,23 @@
import 'dart:io';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
// HMAC SHA-256 algorithm
void main() { void main() {
print('----- HMAC SHA-256 ------');
hs256();
print('-------------------------\n');
print('----- RSA SHA-256 -----');
rs256();
print('-----------------------\n');
print('----- ECDSA SHA-256 -----');
es256();
print('-------------------------');
}
// HMAC SHA-256 algorithm
void hs256() {
String token; String token;
/* Sign */ { /* Sign */ {
@ -55,40 +71,10 @@ void rs256() {
); );
// Sign it // Sign it
token = jwt.sign( final pem = File('rsa_private.pem').readAsStringSync();
PrivateKey( final key = RSAPrivateKey(pem);
'''
-----BEGIN RSA PRIVATE KEY----- token = jwt.sign(key, algorithm: JWTAlgorithm.RS256);
MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw
kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr
m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi
NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV
3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2
QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs
kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go
amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM
+bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9
D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC
0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y
lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+
hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp
bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X
+jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B
BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC
2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx
QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz
5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9
Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0
NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j
8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma
3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K
y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB
jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE=
-----END RSA PRIVATE KEY-----
''',
),
algorithm: JWTAlgorithm.RS256,
);
print('Signed token: $token\n'); print('Signed token: $token\n');
} }
@ -96,22 +82,53 @@ void rs256() {
/* Verify */ { /* Verify */ {
try { try {
// Verify a token // Verify a token
final jwt = JWT.verify( final pem = File('rsa_public.pem').readAsStringSync();
token, final key = RSAPublicKey(pem);
PublicKey(
''' final jwt = JWT.verify(token, key);
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv print('Payload: ${jwt.payload}');
vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc } on JWTExpiredError {
aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy print('jwt expired');
tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 } on JWTError catch (ex) {
e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb print(ex.message); // ex: invalid signature
V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 }
MwIDAQAB }
-----END PUBLIC KEY----- }
''',
), // ECDSA SHA-256 algorithm
); void es256() {
String token;
/* Sign */ {
// Create a json web token
final jwt = JWT(
{
'id': 123,
'server': {
'id': '3e4fc296',
'loc': 'euw-2',
}
},
issuer: 'https://github.com/jonasroussel/jsonwebtoken',
);
// Sign it
final pem = File('ec_private.pem').readAsStringSync();
final key = ECPrivateKey(pem);
token = jwt.sign(key, algorithm: JWTAlgorithm.ES256);
print('Signed token: $token\n');
}
/* Verify */ {
try {
// Verify a token
final pem = File('ec_public.pem').readAsStringSync();
final key = ECPublicKey(pem);
final jwt = JWT.verify(token, key);
print('Payload: ${jwt.payload}'); print('Payload: ${jwt.payload}');
} on JWTExpiredError { } on JWTExpiredError {

27
example/rsa_private.pem Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAq5QLAv9kYTgelglIhC17KdfUoinkwvQ4F0TZAp7qgmu19dCx
PPC5lhPTK8aAdfJNGrnlXw94t4BCmZbWysdUbYHg8qo7sxQMlwixGiCij8/yC3JT
fMxLgNhv7s1NMkG5JPQU1suDsyU3TLI9LcXbST5Ges480+GTZ419KnNcnBVMLmFl
nBP6J0bn2xRs5DufZ2tp8nmzZuLKjVGA4DW3b3VCZmLSQHL9Y0pTQjwkqbhjDfiu
woXGdNJsILuFqpxeVIcsEitbLPzrGEbjE/8afar38gkkBhF5Dv2bt5XD5MpeTvUH
BbL49weOoUdlCh4nrNl5qz0QqplCIMuTdiCGDwIDAQABAoIBADEDz8mBcAmd4I74
A/FOlNq1P5c75JAU+FkdX+kxwMiVQJwCjD9efYX0D+V3hYcsOv6hkHcBknjJeT0r
LB0tqF7INZzdKMTVu2sCHqXaINZ4jyrZ9CO4sPET45YSrDTpNZzq5XEfHnZ08UFF
wdhrp2NbGQNIG3RkApoYf2M7JX+o1ci/4t0Q74ZyzmbHHiKEr5VJvGsd/wajwF9r
waCdbFf7Y7atf27s4ue8pKLo8vVOkDro+gOZYXy/9jOINP9E/NmL1Ggda0mXkNe6
2DX3Riu7WDD8YqxCAWCQuuR+JdsDoAujSppgBeb4JC60Pfsk8QmjXTPqAqaBR2FS
QHh0J3ECgYEA4pWGQIgK+hb0naXCffK+l2KO7Rgj5JBLkrarIU/blpYPeIwbRUGz
qIPcDLdNzwi3CWCwlX3jh3SCeQgI3VCS+BHTJ0B0mZROoMi2jpFsbQUGHethPKj5
/iSqYrHvKfcgFowlrUGrg40rgJ3RD2H3UqT03FhzeQzg9R8yKNmVWq0CgYEAwdpp
mnDT9yEpuM6qAdhtRxwrsKorqdsQp5B5dFDIN11FW8cso6kQM08Rx/wLwx/1fOFc
tMIVxkKcNXLNZXCEPilswHqBTM8JNvxZf8pCACgHtsVClf76zC8vRKfHqN8fli0l
aPK66MBw8jeGrdVmMZ8ehmqCLiE1K8xU1O3P1ysCgYBSTxzgzkqmhLZBmlhnpkmj
9pKTAETxn5VEa4UI6diMAGEIWmUBqJdZ7OR/3gqu3ayYBH7BH2UK7MEEtG177+wj
n14EET8KFzzk3WSrr5oilEOflMTS41NFBg67IYYOyY3nguug340tkZeu9iVeoCjW
5cNpU9xGqe4CPV3cRwYhVQKBgE+AwhrC90u9YB0ZNQQ4vOiTFyAHSo0I+RWf3Q/l
KZf7OrzfPOGKLsi8PvCj4DcQXl/vFVRU3RVIsEQDZ1WZBcNwvdda5riXmJHigNo2
G0dxI2jEEgDuZ/Fh0KL5pJsPlybCMTuPgTR/6FKOjz+v64+iUKST/fvpRqNF0+zX
84utAoGARB+Oun9oJveeFL48Qbh5bxYvYfi3kEZtpqa5kh/2b0/DcaOQprfI5ERE
EAEXPTSykGI60hmlbEMWh6GcfMwHGrrZy0eWw/oQXZclx+HDIjrl869NRLt+WoWp
3vTAqqvIPiBL2BjgTVSLdwwaXKDJb53MutLozOmAliIhRxO5ywI=
-----END RSA PRIVATE KEY-----

9
example/rsa_public.pem Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq5QLAv9kYTgelglIhC17
KdfUoinkwvQ4F0TZAp7qgmu19dCxPPC5lhPTK8aAdfJNGrnlXw94t4BCmZbWysdU
bYHg8qo7sxQMlwixGiCij8/yC3JTfMxLgNhv7s1NMkG5JPQU1suDsyU3TLI9LcXb
ST5Ges480+GTZ419KnNcnBVMLmFlnBP6J0bn2xRs5DufZ2tp8nmzZuLKjVGA4DW3
b3VCZmLSQHL9Y0pTQjwkqbhjDfiuwoXGdNJsILuFqpxeVIcsEitbLPzrGEbjE/8a
far38gkkBhF5Dv2bt5XD5MpeTvUHBbL49weOoUdlCh4nrNl5qz0QqplCIMuTdiCG
DwIDAQAB
-----END PUBLIC KEY-----

View File

@ -4,3 +4,4 @@ export 'src/jwt.dart';
export 'src/errors.dart'; export 'src/errors.dart';
export 'src/algorithms.dart'; export 'src/algorithms.dart';
export 'src/keys.dart'; export 'src/keys.dart';
export 'src/parser.dart';

View File

@ -2,18 +2,41 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:pointycastle/pointycastle.dart' as pc;
import 'package:pointycastle/pointycastle.dart' hide PrivateKey, PublicKey;
import 'package:rsa_pkcs/rsa_pkcs.dart' hide RSAPrivateKey, RSAPublicKey; import 'errors.dart';
import 'keys.dart';
import 'utils.dart';
abstract class JWTAlgorithm { abstract class JWTAlgorithm {
static const HS256 = HMACAlgorithm('HS256'); /// HMAC using SHA-256 hash algorithm
static const HS384 = HMACAlgorithm('HS384'); static const HS256 = _HMACAlgorithm('HS256');
static const HS512 = HMACAlgorithm('HS512');
static const RS256 = RSAAlgorithm('RS256');
static const RS384 = RSAAlgorithm('RS384');
static const RS512 = RSAAlgorithm('RS512');
/// HMAC using SHA-384 hash algorithm
static const HS384 = _HMACAlgorithm('HS384');
/// HMAC using SHA-512 hash algorithm
static const HS512 = _HMACAlgorithm('HS512');
/// RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm
static const RS256 = _RSAAlgorithm('RS256');
/// RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm
static const RS384 = _RSAAlgorithm('RS384');
/// RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm
static const RS512 = _RSAAlgorithm('RS512');
/// ECDSA using P-256 curve and SHA-256 hash algorithm
static const ES256 = _ECDSAAlgorithm('ES256');
/// ECDSA using P-384 curve and SHA-384 hash algorithm
static const ES384 = _ECDSAAlgorithm('ES384');
/// ECDSA using P-512 curve and SHA-512 hash algorithm
static const ES512 = _ECDSAAlgorithm('ES512');
/// Return the `JWTAlgorithm` from his string name
static JWTAlgorithm fromName(String name) { static JWTAlgorithm fromName(String name) {
switch (name) { switch (name) {
case 'HS256': case 'HS256':
@ -28,6 +51,12 @@ abstract class JWTAlgorithm {
return JWTAlgorithm.RS384; return JWTAlgorithm.RS384;
case 'RS512': case 'RS512':
return JWTAlgorithm.RS512; return JWTAlgorithm.RS512;
case 'ES256':
return JWTAlgorithm.ES256;
case 'ES384':
return JWTAlgorithm.ES384;
case 'ES512':
return JWTAlgorithm.ES512;
default: default:
throw JWTInvalidError('unknown algorithm'); throw JWTInvalidError('unknown algorithm');
} }
@ -35,30 +64,40 @@ abstract class JWTAlgorithm {
const JWTAlgorithm(); const JWTAlgorithm();
/// `JWTAlgorithm` name
String get name; String get name;
List<int> sign(Key key, List<int> body);
bool verify(Key key, List<int> body, List<int> signature); /// Create a signature of the `body` with `key`
///
/// return the signature as bytes
Uint8List sign(Key key, Uint8List body);
/// Verify the `signature` of `body` with `key`
///
/// return `true` if the signature is correct `false` otherwise
bool verify(Key key, Uint8List body, Uint8List signature);
} }
class HMACAlgorithm extends JWTAlgorithm { class _HMACAlgorithm extends JWTAlgorithm {
final String _name; final String _name;
const HMACAlgorithm(this._name); const _HMACAlgorithm(this._name);
@override @override
String get name => _name; String get name => _name;
@override @override
List<int> sign(Key key, List<int> body) { Uint8List sign(Key key, Uint8List body) {
assert(key is SecretKey, 'key must be a SecretKey'); assert(key is SecretKey, 'key must be a SecretKey');
final secretKey = key as SecretKey; final secretKey = key as SecretKey;
final hmac = Hmac(_getHash(name), utf8.encode(secretKey.key)); final hmac = Hmac(_getHash(name), utf8.encode(secretKey.key));
return hmac.convert(body).bytes; return hmac.convert(body).bytes;
} }
@override @override
bool verify(Key key, List<int> body, List<int> signature) { bool verify(Key key, Uint8List body, Uint8List signature) {
assert(key is SecretKey, 'key must be a SecretKey'); assert(key is SecretKey, 'key must be a SecretKey');
final actual = sign(key, body); final actual = sign(key, body);
@ -86,78 +125,44 @@ class HMACAlgorithm extends JWTAlgorithm {
} }
} }
class RSAAlgorithm extends JWTAlgorithm { class _RSAAlgorithm extends JWTAlgorithm {
final String _name; final String _name;
const RSAAlgorithm(this._name); const _RSAAlgorithm(this._name);
@override @override
String get name => _name; String get name => _name;
@override @override
List<int> sign(Key key, List<int> body) { Uint8List sign(Key key, Uint8List body) {
assert(key is PrivateKey, 'key must be a PrivateKey'); assert(key is RSAPrivateKey, 'key must be a RSAPrivateKey');
final privateKey = key as PrivateKey; final privateKey = key as RSAPrivateKey;
final parser = RSAPKCSParser(); final signer = pc.Signer('${_getHash(name)}/RSA');
RSAKeyPair pair; final params = pc.PrivateKeyParameter<pc.RSAPrivateKey>(privateKey.key);
pair = parser.parsePEM(privateKey.key, password: privateKey.passphrase);
if (pair.private == null) {
throw JWTInvalidError('invalid private RSA key');
}
final signer = Signer('${_getHash(name)}/RSA');
final params = ParametersWithRandom(
PrivateKeyParameter<RSAPrivateKey>(
RSAPrivateKey(
pair.private.modulus,
pair.private.privateExponent,
pair.private.prime1,
pair.private.prime2,
),
),
SecureRandom('AES/CTR/PRNG'),
);
signer.init(true, params); signer.init(true, params);
RSASignature signature = signer.generateSignature(Uint8List.fromList(body)); pc.RSASignature signature = signer.generateSignature(
Uint8List.fromList(body),
);
return signature.bytes.toList(growable: false); return signature.bytes;
} }
@override @override
bool verify(Key key, List<int> body, List<int> signature) { bool verify(Key key, Uint8List body, Uint8List signature) {
assert(key is PublicKey, 'key must be a PublicKey'); assert(key is RSAPublicKey, 'key must be a RSAPublicKey');
final publicKey = key as PublicKey; final publicKey = key as RSAPublicKey;
final parser = RSAPKCSParser();
RSAKeyPair pair;
try { try {
pair = parser.parsePEM(publicKey.key, password: publicKey.passphrase); final signer = pc.Signer('${_getHash(name)}/RSA');
assert(pair.public != null); final params = pc.PublicKeyParameter<pc.RSAPublicKey>(publicKey.key);
} catch (ex) {
throw JWTInvalidError('invalid public RSA key');
}
try {
final signer = Signer('${_getHash(name)}/RSA');
final params = ParametersWithRandom(
PublicKeyParameter<RSAPublicKey>(
RSAPublicKey(
pair.public.modulus,
BigInt.from(pair.public.publicExponent),
),
),
SecureRandom('AES/CTR/PRNG'),
);
signer.init(false, params); signer.init(false, params);
final msg = Uint8List.fromList(body); final msg = Uint8List.fromList(body);
final sign = RSASignature(Uint8List.fromList(signature)); final sign = pc.RSASignature(Uint8List.fromList(signature));
return signer.verifySignature(msg, sign); return signer.verifySignature(msg, sign);
} catch (ex) { } catch (ex) {
@ -178,3 +183,66 @@ class RSAAlgorithm extends JWTAlgorithm {
} }
} }
} }
class _ECDSAAlgorithm extends JWTAlgorithm {
final String _name;
const _ECDSAAlgorithm(this._name);
@override
String get name => _name;
@override
Uint8List sign(Key key, Uint8List body) {
assert(key is ECPrivateKey, 'key must be a ECPublicKey');
final privateKey = key as ECPrivateKey;
final signer = pc.Signer('${_getHash(name)}/DET-ECDSA');
final params = pc.PrivateKeyParameter<pc.ECPrivateKey>(privateKey.key);
signer.init(true, params);
pc.ECSignature signature = signer.generateSignature(
Uint8List.fromList(body),
);
final len = privateKey.size;
final bytes = Uint8List(len * 2);
bytes.setRange(0, len, bigIntToBytes(signature.r).toList().reversed);
bytes.setRange(len, len * 2, bigIntToBytes(signature.s).toList().reversed);
return bytes;
}
@override
bool verify(Key key, Uint8List body, Uint8List signature) {
assert(key is ECPublicKey, 'key must be a ECPublicKey');
final publicKey = key as ECPublicKey;
final signer = pc.Signer('${_getHash(name)}/DET-ECDSA');
final params = pc.PublicKeyParameter<pc.ECPublicKey>(publicKey.key);
signer.init(false, params);
final len = signature.length ~/ 2;
final sign = pc.ECSignature(
bigIntFromBytes(signature.sublist(0, len)),
bigIntFromBytes(signature.sublist(len)),
);
return signer.verifySignature(body, sign);
}
String _getHash(String name) {
switch (name) {
case 'ES256':
return 'SHA-256';
case 'ES384':
return 'SHA-384';
case 'ES512':
return 'SHA-512';
default:
throw ArgumentError.value(name, 'name', 'unknown hash name');
}
}
}

View File

@ -1,23 +1,25 @@
/// JWTError objects thrown in the case of a jwt sign or verify failure.
class JWTError extends Error { class JWTError extends Error {
JWTError(this.message); JWTError(this.message);
/// Describes the error thrown
final String message; final String message;
@override @override
String toString() => 'JWTError: $message'; String toString() => 'JWTError: $message';
} }
/// An error thrown when token is invalid /// An error thrown when jwt is invalid
class JWTInvalidError extends JWTError { class JWTInvalidError extends JWTError {
JWTInvalidError(String message) : super(message); JWTInvalidError(String message) : super(message);
} }
/// An error thrown when token is expired /// An error thrown when jwt is expired
class JWTExpiredError extends JWTError { class JWTExpiredError extends JWTError {
JWTExpiredError() : super('jwt expired'); JWTExpiredError() : super('jwt expired');
} }
/// An error thrown when token is not active /// An error thrown when jwt is not active
class JWTNotActiveError extends JWTError { class JWTNotActiveError extends JWTError {
JWTNotActiveError() : super('jwt not active'); JWTNotActiveError() : super('jwt not active');
} }

View File

@ -1,15 +1,17 @@
import 'dart:convert'; import 'dart:convert';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'algorithms.dart';
import 'errors.dart';
import './utils.dart'; import 'keys.dart';
import 'utils.dart';
class JWT { class JWT {
/// Verify a token. /// Verify a token.
/// ///
/// `key` must be /// `key` must be
/// - SecretKey with HS256 algorithm /// - SecretKey with HMAC algorithm
/// - PublicKey with RS256 algorithm /// - RSAPublicKey with RSA algorithm
/// - ECPublicKey with ECDSA algorithm
static JWT verify( static JWT verify(
String token, String token,
Key key, { Key key, {
@ -52,8 +54,9 @@ class JWT {
if (payload is Map) { if (payload is Map) {
// exp // exp
if (checkExpiresIn && payload.containsKey('exp')) { if (checkExpiresIn && payload.containsKey('exp')) {
final exp = final exp = DateTime.fromMillisecondsSinceEpoch(
DateTime.fromMillisecondsSinceEpoch(payload['exp'] * 1000); payload['exp'] * 1000,
);
if (exp.isBefore(DateTime.now())) { if (exp.isBefore(DateTime.now())) {
throw JWTExpiredError(); throw JWTExpiredError();
} }
@ -61,8 +64,9 @@ class JWT {
// nbf // nbf
if (checkNotBefore && payload.containsKey('nbf')) { if (checkNotBefore && payload.containsKey('nbf')) {
final nbf = final nbf = DateTime.fromMillisecondsSinceEpoch(
DateTime.fromMillisecondsSinceEpoch(payload['nbf'] * 1000); payload['nbf'] * 1000,
);
if (nbf.isAfter(DateTime.now())) { if (nbf.isAfter(DateTime.now())) {
throw JWTNotActiveError(); throw JWTNotActiveError();
} }
@ -73,8 +77,9 @@ class JWT {
if (!payload.containsKey('iat')) { if (!payload.containsKey('iat')) {
throw JWTInvalidError('invalid issue at'); throw JWTInvalidError('invalid issue at');
} }
final iat = final iat = DateTime.fromMillisecondsSinceEpoch(
DateTime.fromMillisecondsSinceEpoch(payload['iat'] * 1000); payload['iat'] * 1000,
);
if (!iat.isAtSameMomentAs(DateTime.now())) { if (!iat.isAtSameMomentAs(DateTime.now())) {
throw JWTInvalidError('invalid issue at'); throw JWTInvalidError('invalid issue at');
} }
@ -154,8 +159,9 @@ class JWT {
/// Sign and generate a new token. /// Sign and generate a new token.
/// ///
/// `key` must be /// `key` must be
/// - SecretKey with HS256 algorithm /// - SecretKey with HMAC algorithm
/// - PrivateKey with RS256 algorithm /// - RSAPrivateKey with RSA algorithm
/// - ECPrivateKey with ECDSA algorithm
String sign( String sign(
Key key, { Key key, {
JWTAlgorithm algorithm = JWTAlgorithm.HS256, JWTAlgorithm algorithm = JWTAlgorithm.HS256,
@ -194,7 +200,8 @@ class JWT {
); );
} catch (ex) { } catch (ex) {
throw JWTError( throw JWTError(
'invalid payload json format (Map keys must be String type)'); 'invalid payload json format (Map keys must be String type)',
);
} }
final body = '${b64Header}.${b64Payload}'; final body = '${b64Header}.${b64Payload}';

View File

@ -1,24 +1,50 @@
import 'package:pointycastle/pointycastle.dart' as pc;
import 'parser.dart';
abstract class Key {} abstract class Key {}
/// For HS256 algorithm /// For HMAC algorithms
class SecretKey extends Key { class SecretKey extends Key {
String key; String key;
SecretKey(this.key); SecretKey(this.key);
} }
/// For RS256 algorithm, in sign method /// For RSA algorithm, in sign method
class PrivateKey extends Key { class RSAPrivateKey extends Key {
String key; pc.RSAPrivateKey key;
String passphrase;
PrivateKey(this.key, [this.passphrase = '']); RSAPrivateKey(String pem) {
key = parseRSAPrivateKeyPEM(pem);
}
} }
/// For RS256 algorithm, in verify method /// For RSA algorithm, in verify method
class PublicKey extends Key { class RSAPublicKey extends Key {
String key; pc.RSAPublicKey key;
String passphrase;
PublicKey(this.key, [this.passphrase = '']); RSAPublicKey(String pem) {
key = parseRSAPublicKeyPEM(pem);
}
}
/// For ECDSA algorithm, in sign method
class ECPrivateKey extends Key {
pc.ECPrivateKey key;
int size;
ECPrivateKey(String pem) {
key = parseECPrivateKeyPEM(pem);
size = (key.parameters.curve.fieldSize / 8).round();
}
}
/// For ECDSA algorithm, in verify method
class ECPublicKey extends Key {
pc.ECPublicKey key;
ECPublicKey(String pem) {
key = parseECPublicKeyPEM(pem);
}
} }

275
lib/src/parser.dart Normal file
View File

@ -0,0 +1,275 @@
import 'dart:typed_data';
import 'dart:convert';
import 'package:pointycastle/pointycastle.dart' hide ECPoint;
import 'package:pointycastle/ecc/ecc_fp.dart';
import 'utils.dart';
// RSA Private Key -> PKCS#1 format
const String _pkcs1RSAPrivateHeader = '-----BEGIN RSA PRIVATE KEY-----';
const String _pkcs1RSAPrivateFooter = '-----END RSA PRIVATE KEY-----';
// RSA Private Key -> PKCS#8 format
const String _pkcs8RSAPrivateHeader = '-----BEGIN PRIVATE KEY-----';
const String _pkcs8RSAPrivateFooter = '-----END PRIVATE KEY-----';
// RSA Public Key -> PKCS#1 format
const String _pkcs1RSAPublicHeader = '-----BEGIN RSA PUBLIC KEY-----';
const String _pkcs1RSAPublicFooter = '-----END RSA PUBLIC KEY-----';
// RSA Public Key -> PKCS#1 format
const String _pkcs8RSAPublicHeader = '-----BEGIN PUBLIC KEY-----';
const String _pkcs8RSAPublicFooter = '-----END PUBLIC KEY-----';
// ECDSA Private Key -> SEC 1 format
const String _sec1ECPrivateHeader = '-----BEGIN EC PRIVATE KEY-----';
const String _sec1ECPrivateFooter = '-----END EC PRIVATE KEY-----';
// ECDSA Private Key -> PKCS#8 format
const String _pkcs8ECPrivateHeader = '-----BEGIN PRIVATE KEY-----';
const String _pkcs8ECPrivateFooter = '-----END PRIVATE KEY-----';
// ECDSA Public Key -> PKCS#8 format
const String _pkcs8ECPublicHeader = '-----BEGIN PUBLIC KEY-----';
const String _pkcs8ECPublicFooter = '-----END PUBLIC KEY-----';
// ECDSA Curves OID to DomainName
const Map<String, String> _ecCurves = const {
'1.2.840.10045.3.1.7': 'prime256v1', // P-256
'1.3.132.0.10': 'secp256k1', // P-256
'1.3.132.0.34': 'secp384r1', // P-384
'1.3.132.0.35': 'secp521r1', // P-512
};
/// RSA Private Key -> PKCS#1 parser
RSAPrivateKey _pkcs1RSAPrivateKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final values = seq.elements.cast<ASN1Integer>();
return RSAPrivateKey(
values[1].integer, // modulus
values[3].integer, // privateExponent
values[4].integer, // prime1
values[5].integer, // prime2
);
}
/// RSA Private Key -> PKCS#8 parser
RSAPrivateKey _pkcs8RSAPrivateKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final keySeq = seq.elements[2] as ASN1OctetString;
final keyParser = ASN1Parser(keySeq.octets);
final valuesSeq = keyParser.nextObject() as ASN1Sequence;
final values = valuesSeq.elements.cast<ASN1Integer>();
return RSAPrivateKey(
values[1].integer, // modulus
values[3].integer, // privateExponent
values[4].integer, // prime1
values[5].integer, // prime2
);
}
/// RSA Public Key -> PKCS#1 parser
RSAPublicKey _pkcs1RSAPublicKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final values = seq.elements.cast<ASN1Integer>();
return RSAPublicKey(
values[0].integer, // modulus
values[1].integer, // publicExponent
);
}
/// RSA Public Key -> PKCS#8 parser
RSAPublicKey _pkcs8RSAPublicKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final keySeq = seq.elements[1] as ASN1BitString;
final keyParser = ASN1Parser(keySeq.stringValues);
final valuesSeq = keyParser.nextObject() as ASN1Sequence;
final values = valuesSeq.elements.cast<ASN1Integer>();
return RSAPublicKey(
values[0].integer, // modulus
values[1].integer, // publicExponent
);
}
/// ECDSA Private Key -> SEC 1 parser
ECPrivateKey _sec1ECPrivateKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final privateKey = seq.elements[1] as ASN1OctetString;
final params = seq.elements[2];
final paramsParser = ASN1Parser(params.valueBytes);
final oid = (paramsParser.nextObject() as ASN1ObjectIdentifier)
.objectIdentifierAsString;
final curve = _ecCurves[oid];
return ECPrivateKey(
decodeBigInt(privateKey.valueBytes),
ECDomainParameters(curve),
);
}
/// ECDSA Private Key -> PKCS#8 parser
ECPrivateKey _pkcs8ECPrivateKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final oidSeq = seq.elements[1] as ASN1Sequence;
final oid =
(oidSeq.elements[1] as ASN1ObjectIdentifier).objectIdentifierAsString;
final curve = _ecCurves[oid];
final privateKeyParser = ASN1Parser(seq.elements[2].valueBytes);
final privateKeySeq = privateKeyParser.nextObject() as ASN1Sequence;
final privateKey = (privateKeySeq.elements[1] as ASN1OctetString);
return ECPrivateKey(
decodeBigInt(privateKey.valueBytes),
ECDomainParameters(curve),
);
}
/// ECDSA Public Key -> PKCS#8 parser
ECPublicKey _pkcs8ECPublicKey(Uint8List bytes) {
final parser = ASN1Parser(bytes);
final seq = parser.nextObject() as ASN1Sequence;
final oidSeq = seq.elements[0] as ASN1Sequence;
final oid =
(oidSeq.elements[1] as ASN1ObjectIdentifier).objectIdentifierAsString;
final curve = _ecCurves[oid];
var publicKeyBytes = seq.elements[1].valueBytes;
if (publicKeyBytes[0] == 0) {
publicKeyBytes = publicKeyBytes.sublist(1);
}
final compressed = publicKeyBytes[0] != 4;
final x = publicKeyBytes.sublist(1, (publicKeyBytes.length / 2).round());
final y = publicKeyBytes.sublist(1 + x.length, publicKeyBytes.length);
final bigX = decodeBigIntWithSign(1, x);
final bigY = decodeBigIntWithSign(1, y);
final params = ECDomainParameters(curve);
return ECPublicKey(
ECPoint(
params.curve,
params.curve.fromBigInteger(bigX),
params.curve.fromBigInteger(bigY),
compressed,
),
params,
);
}
/// Parse RSA private key from pem string
RSAPrivateKey parseRSAPrivateKeyPEM(String pem) {
if (pem.contains(_pkcs1RSAPrivateHeader) &&
pem.contains(_pkcs1RSAPrivateFooter)) {
final data = pem
.substring(
pem.indexOf(_pkcs1RSAPrivateHeader) + _pkcs1RSAPrivateHeader.length,
pem.indexOf(_pkcs1RSAPrivateFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _pkcs1RSAPrivateKey(base64.decode(data));
} else if (pem.contains(_pkcs8RSAPrivateHeader) &&
pem.contains(_pkcs8RSAPrivateFooter)) {
final data = pem
.substring(
pem.indexOf(_pkcs8RSAPrivateHeader) + _pkcs8RSAPrivateHeader.length,
pem.indexOf(_pkcs8RSAPrivateFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _pkcs8RSAPrivateKey(base64.decode(data));
} else {
return null;
}
}
/// Parse RSA public key from pem string
RSAPublicKey parseRSAPublicKeyPEM(String pem) {
if (pem.contains(_pkcs1RSAPublicHeader) &&
pem.contains(_pkcs1RSAPublicFooter)) {
final data = pem
.substring(
pem.indexOf(_pkcs1RSAPublicHeader) + _pkcs1RSAPublicHeader.length,
pem.indexOf(_pkcs1RSAPublicFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _pkcs1RSAPublicKey(base64.decode(data));
} else if (pem.contains(_pkcs8RSAPublicHeader) &&
pem.contains(_pkcs8RSAPublicFooter)) {
final data = pem
.substring(
pem.indexOf(_pkcs8RSAPublicHeader) + _pkcs8RSAPublicHeader.length,
pem.indexOf(_pkcs8RSAPublicFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _pkcs8RSAPublicKey(base64.decode(data));
} else {
return null;
}
}
/// Parse ECDSA private key from pem string
ECPrivateKey parseECPrivateKeyPEM(String pem) {
if (pem.contains(_sec1ECPrivateHeader) &&
pem.contains(_sec1ECPrivateFooter)) {
final data = pem
.substring(
pem.indexOf(_sec1ECPrivateHeader) + _sec1ECPrivateHeader.length,
pem.indexOf(_sec1ECPrivateFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _sec1ECPrivateKey(base64.decode(data));
} else if (pem.contains(_pkcs8ECPrivateHeader) &&
pem.contains(_pkcs8ECPrivateFooter)) {
final data = pem
.substring(
pem.indexOf(_pkcs8ECPrivateHeader) + _pkcs8ECPrivateHeader.length,
pem.indexOf(_pkcs8ECPrivateFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _pkcs8ECPrivateKey(base64.decode(data));
} else {
return null;
}
}
/// Parse ECDSA public key from pem string
ECPublicKey parseECPublicKeyPEM(String pem) {
if (pem.contains(_pkcs8ECPublicHeader) &&
pem.contains(_pkcs8ECPublicFooter)) {
final data = pem
.substring(
pem.indexOf(_pkcs8ECPublicHeader) + _pkcs8ECPublicHeader.length,
pem.indexOf(_pkcs8ECPublicFooter),
)
.replaceAll(RegExp(r'[\n\r ]'), '');
return _pkcs8ECPublicKey(base64.decode(data));
} else {
return null;
}
}

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
final jsonBase64 = json.fuse(utf8.fuse(base64Url)); final jsonBase64 = json.fuse(utf8.fuse(base64Url));
String base64Unpadded(String value) { String base64Unpadded(String value) {
@ -24,3 +26,66 @@ String base64Padded(String value) {
int secondsSinceEpoch(DateTime time) { int secondsSinceEpoch(DateTime time) {
return time.millisecondsSinceEpoch ~/ 1000; return time.millisecondsSinceEpoch ~/ 1000;
} }
BigInt decodeBigInt(List<int> bytes) {
var negative = bytes.isNotEmpty && bytes[0] & 0x80 == 0x80;
BigInt result;
if (bytes.length == 1) {
result = BigInt.from(bytes[0]);
} else {
result = BigInt.zero;
for (var i = 0; i < bytes.length; i++) {
var item = bytes[bytes.length - i - 1];
result |= (BigInt.from(item) << (8 * i));
}
}
if (result == BigInt.zero) return BigInt.zero;
return negative ? result.toSigned(result.bitLength) : result;
}
BigInt decodeBigIntWithSign(int sign, List<int> bytes) {
if (sign == 0) return BigInt.zero;
BigInt result;
if (bytes.length == 1) {
result = BigInt.from(bytes[0]);
} else {
result = BigInt.zero;
for (var i = 0; i < bytes.length; i++) {
var item = bytes[bytes.length - i - 1];
result |= (BigInt.from(item) << (8 * i));
}
}
if (result == BigInt.zero) return BigInt.zero;
return sign < 0
? result.toSigned(result.bitLength)
: result.toUnsigned(result.bitLength);
}
Uint8List bigIntToBytes(BigInt v) {
final _b256 = BigInt.from(256);
List<int> bytes = List();
while (v.sign != 0) {
bytes.add((v % _b256).toInt());
v = v ~/ _b256;
}
return Uint8List.fromList(bytes);
}
BigInt bigIntFromBytes(Uint8List bytes) {
final _b256 = BigInt.from(256);
return bytes.fold(BigInt.zero, (a, b) => a * _b256 + BigInt.from(b));
}

View File

@ -1,6 +1,6 @@
name: dart_jsonwebtoken name: dart_jsonwebtoken
description: A dart implementation of the famous javascript library 'jsonwebtoken' (JWT). description: A dart implementation of the famous javascript library 'jsonwebtoken' (JWT).
version: 1.5.0 version: 1.6.0
repository: https://github.com/jonasroussel/jsonwebtoken repository: https://github.com/jonasroussel/jsonwebtoken
homepage: https://github.com/jonasroussel/jsonwebtoken#readme homepage: https://github.com/jonasroussel/jsonwebtoken#readme
@ -9,9 +9,7 @@ environment:
dependencies: dependencies:
crypto: ^2.1.5 crypto: ^2.1.5
rsa_pkcs: ^1.1.4 pointycastle: ^2.0.1
pointycastle: ^2.0.0
cryptography: ^1.4.1
dev_dependencies: dev_dependencies:
pedantic: ^1.9.2 pedantic: ^1.9.2