Skip to content

Commit

Permalink
refactor jws and jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
mistermoe committed Feb 12, 2024
1 parent 0e9de93 commit 3ad3612
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 177 deletions.
66 changes: 66 additions & 0 deletions packages/web5/lib/src/jws/decoded_jws.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:typed_data';

import 'package:web5/src/crypto.dart';
import 'package:web5/src/jws/jws_header.dart';

import 'package:web5/src/dids.dart';

class DecodedJws {
final JwsHeader header;
final Uint8List payload;
final Uint8List signature;
final List<String> parts;

static final _didResolver =
DidResolver(methodResolvers: [DidJwk.resolver, DidDht.resolver]);

DecodedJws({
required this.header,
required this.payload,
required this.signature,
required this.parts,
});

Future<void> verify() async {
if (header.kid == null || header.alg == null) {
throw Exception(
'Malformed JWS. expected header to contain kid and alg.',
);
}

final dereferenceResult = await _didResolver.dereference(header.kid!);
if (dereferenceResult.hasError()) {
throw Exception(
'Verification failed. Failed to dereference kid. Error: ${dereferenceResult.dereferencingMetadata.error}',
);
}

final didResource = dereferenceResult.contentStream;
if (didResource == null) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

if (didResource is! DidVerificationMethod) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

final publicKeyJwk = didResource.publicKeyJwk;
final dsaName =
DsaName.findByAlias(algorithm: header.alg, curve: publicKeyJwk!.crv);

if (dsaName == null) {
throw Exception('${header.alg}:${publicKeyJwk.crv} not supported.');
}

await DsaAlgorithms.verify(
algName: dsaName,
publicKey: publicKeyJwk,
payload: payload,
signature: signature,
);
}
}
134 changes: 53 additions & 81 deletions packages/web5/lib/src/jws/jws.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:web5/src/dids.dart';
import 'package:web5/src/crypto.dart';
import 'package:web5/src/extensions.dart';
import 'package:web5/src/dids.dart';
import 'package:web5/src/extensions/base64url.dart';
import 'package:web5/src/jws/decoded_jws.dart';
import 'package:web5/src/jws/jws_header.dart';

final _base64UrlCodec = Base64Codec.urlSafe();
Expand All @@ -14,6 +15,50 @@ class Jws {
static final _didResolver =
DidResolver(methodResolvers: [DidJwk.resolver, DidDht.resolver]);

static DecodedJws decode(String jws) {
final parts = jws.split('.');

if (parts.length != 3) {
throw Exception(
'Malformed JWT. expected 3 parts. got ${parts.length}',
);
}

final JwsHeader header;
try {
header = JwsHeader.fromBase64Url(parts[0]);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT header',
);
}

final Uint8List payload;
try {
payload = _base64UrlDecoder.convertNoPadding(parts[1]);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT payload',
);
}

final Uint8List signature;
try {
signature = base64.decoder.convertNoPadding(parts[2]);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT payload',
);
}

return DecodedJws(
header: header,
payload: payload,
signature: signature,
parts: parts,
);
}

/// Signs a JWT payload using a specified [Did] and returns the signed JWT.
///
/// Throws [Exception] if any error occurs during the signing process.
Expand Down Expand Up @@ -71,86 +116,13 @@ class Jws {
}
}

static Future<void> verify(String compactJws, {Uint8List? payload}) async {
final splitJws = compactJws.split('.');

if (splitJws.length != 3) {
throw Exception(
'Malformed JWS. expected 3 parts. got ${splitJws.length}',
);
}

final [
base64UrlEncodedHeader,
base64UrlEncodedPayload,
base64UrlEncodedSignature
] = splitJws;

final JwsHeader header;
static Future<DecodedJws> verify(String jws) async {
try {
header = JwsHeader.fromBase64Url(base64UrlEncodedHeader);
} on Exception {
throw Exception(
'Malformed JWS. Invalid base64url encoding for JWS header',
);
}

if (header.kid == null || header.alg == null) {
throw Exception(
'Malformed JWS. expected header to contain kid and alg.',
);
}

try {
payload ??= _base64UrlDecoder.convertNoPadding(base64UrlEncodedPayload);
} on Exception {
throw Exception(
'Malformed JWS. Invalid base64url encoding for JWS payload',
);
}

final Uint8List signature;
try {
signature = _base64UrlDecoder.convertNoPadding(base64UrlEncodedSignature);
} on Exception {
throw Exception(
'Malformed JWS. Invalid base64url encoding for JWS signature',
);
final decodedJws = decode(jws);
await decodedJws.verify();
return decodedJws;
} on Exception catch (e) {
throw Exception('Verification failed. $e');
}

final dereferenceResult = await _didResolver.dereference(header.kid!);
if (dereferenceResult.hasError()) {
throw Exception(
'Verification failed. Failed to dereference kid. Error: ${dereferenceResult.dereferencingMetadata.error}',
);
}

final didResource = dereferenceResult.contentStream;
if (didResource == null) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

if (didResource is! DidVerificationMethod) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

final publicKeyJwk = didResource.publicKeyJwk;
final dsaName =
DsaName.findByAlias(algorithm: header.alg, curve: publicKeyJwk!.crv);

if (dsaName == null) {
throw Exception('${header.alg}:${publicKeyJwk.crv} not supported.');
}

return DsaAlgorithms.verify(
algName: dsaName,
publicKey: publicKeyJwk,
payload: payload,
signature: signature,
);
}
}
30 changes: 30 additions & 0 deletions packages/web5/lib/src/jwt/decoded_jwt.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:web5/src/jws/decoded_jws.dart';
import 'package:web5/web5.dart';

class DecodedJwt {
final JwtHeader header;
final JwtClaims claims;
final Uint8List signature;
final List<String> parts;

DecodedJwt({
required this.header,
required this.claims,
required this.signature,
required this.parts,
});

Future<void> verify() async {
final decodedJws = DecodedJws(
header: header,
payload: Base64Codec.urlSafe().decoder.convertNoPadding(parts[1]),
signature: signature,
parts: parts,
);

await decodedJws.verify();
}
}
90 changes: 18 additions & 72 deletions packages/web5/lib/src/jwt/jwt.dart
Original file line number Diff line number Diff line change
@@ -1,86 +1,32 @@
import 'dart:convert';

import 'package:web5/src/dids.dart';
import 'package:web5/src/extensions.dart';
import 'package:web5/src/jws.dart';
import 'package:web5/src/jws/jws.dart';
import 'package:web5/src/dids/did.dart';
import 'package:web5/src/jwt/decoded_jwt.dart';
import 'package:web5/src/jwt/jwt_claims.dart';
import 'package:web5/src/extensions/json.dart';
import 'package:web5/src/jwt/jwt_decoded.dart';
import 'package:web5/src/jwt/jwt_encoded.dart';
import 'package:web5/src/jwt/jwt_header.dart';

/**
* TODO: refactor. awkward implementation:
* * Jwt.parse() returns an instance of Jwt but you can't call sign on an
* an instance of Jwt. Likely makes most sense for Jwt to have static methods
* only and potentially return something like ParsedJwt instead
* * Jwt.verify calls Jwt.parse first then calls Jws.verify which effectively
* performs the same logic as Jwt.parse
*/

/// A utility class for handling
/// [JSON Web Tokens (JWTs)](https://datatracker.ietf.org/doc/html/rfc7519)
///
/// This class provides functionalities to parse, encode, and sign JWTs.
/// It supports JWT signing with DID keys.
class Jwt {
JwtEncoded encoded;
JwtDecoded decoded;

Jwt({required this.encoded, required this.decoded});

/// Parses a signed JWT string into its decoded form. returns both split
/// encoded and decoded forms
///
/// Throws [Exception] if the JWT is malformed or if it does not meet
/// the expected structure and encoding requirements.
factory Jwt.parse(String signedJwt) {
final splitJwt = signedJwt.split('.');

if (splitJwt.length != 3) {
throw Exception(
'Malformed JWT. expected 3 parts. got ${splitJwt.length}',
);
}

final [
base64UrlEncodedHeader,
base64UrlEncodedPayload,
base64UrlEncodedSignature
] = splitJwt;

final JwtHeader header;
try {
header = JwtHeader.fromBase64Url(base64UrlEncodedHeader);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT header',
);
}
static DecodedJwt decode(String jwt) {
final decodedJws = Jws.decode(jwt);

if (header.typ == null || header.typ?.toUpperCase() != 'JWT') {
throw Exception('Expected JWT header to contain typ property set to JWT');
}

if (header.alg == null || header.kid == null) {
throw Exception('Expected JWT header to contain alg and kid');
}

final JwtClaims payload;
final JwtClaims claims;
try {
payload = JwtClaims.fromBase64Url(base64UrlEncodedPayload);
final str = utf8.decode(decodedJws.payload);
claims = JwtClaims.fromJson(json.decode(str));
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT payload',
);
}

return Jwt(
decoded: JwtDecoded(header: header, payload: payload),
encoded: JwtEncoded(
header: base64UrlEncodedHeader,
payload: base64UrlEncodedPayload,
signature: base64UrlEncodedSignature,
),
return DecodedJwt(
header: decodedJws.header,
claims: claims,
signature: decodedJws.signature,
parts: decodedJws.parts,
);
}

Expand All @@ -97,9 +43,9 @@ class Jwt {
return Jws.sign(did: did, payload: payloadBytes, header: header);
}

static Future<void> verify(String signedJwt) async {
Jwt.parse(signedJwt);

return Jws.verify(signedJwt);
static Future<DecodedJwt> verify(String jwt) async {
final decodedJwt = decode(jwt);
await decodedJwt.verify();
return decodedJwt;
}
}
Loading

0 comments on commit 3ad3612

Please sign in to comment.