Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/web-create #56

Merged
2 changes: 1 addition & 1 deletion packages/web5/lib/src/crypto/in_memory_key_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import 'package:web5/src/crypto/crypto.dart';
/// final signatureBytes = await keyManager.sign(keyAlias, Uint8List.fromList([20, 32]));
/// ```
///
class InMemoryKeyManager implements KeyManager, KeyImporter, KeyExporter {
class InMemoryKeyManager implements KeyManager {
fingersonfire marked this conversation as resolved.
Show resolved Hide resolved
final Map<String, Jwk> _keyStore = {};

@override
Expand Down
20 changes: 8 additions & 12 deletions packages/web5/lib/src/crypto/key_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,15 @@ import 'dart:typed_data';
import 'package:web5/src/crypto/algorithm_id.dart';
import 'package:web5/src/crypto/jwk.dart';

abstract interface class KeyImporter {
/// Imports a private key. Returns
/// a unique id that can be utilized to reference the imported key for
/// future operations.
Future<String> import(Jwk jwk);
}

abstract interface class KeyExporter {
/// Exports the private key with the provided id.
Future<Jwk> export(String keyId);
}

/// A key management interface that provides functionality for generating,
/// storing, and utilizing private keys and their associated public keys.
/// Implementations of this interface should handle the secure generation and
/// storage of keys, providing mechanisms for utilizing them in cryptographic
/// operations like signing.
abstract interface class KeyManager {
/// Exports the private key with the provided id.
Future<Jwk> export(String keyId);

/// Generates and securely stores a private key based on the provided
/// algorithm. Returns a unique alias that can be utilized to reference the
/// generated key for future operations.
Expand All @@ -30,6 +21,11 @@ abstract interface class KeyManager {
/// identified by the provided alias.
Future<Jwk> getPublicKey(String keyId);

/// Imports a private key. Returns
/// a unique id that can be utilized to reference the imported key for
/// future operations.
Future<String> import(Jwk jwk);

/// Signs the provided payload using the private key identified by the
/// provided alias.
Future<Uint8List> sign(String keyId, Uint8List payload);
Expand Down
9 changes: 3 additions & 6 deletions packages/web5/lib/src/dids/bearer_did.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ class BearerDid {
String uri;
KeyManager keyManager;
DidDocument document;
DidDocumentMetadata metadata;

BearerDid({
required this.uri,
required this.keyManager,
required this.document,
this.metadata = const DidDocumentMetadata(),
});

Future<PortableDid> export() async {
Expand All @@ -33,15 +35,10 @@ class BearerDid {
document: document,
);

if (keyManager is! KeyExporter) {
return Future.value(portableDid);
}

final keyExporter = keyManager as KeyExporter;
for (final vm in document.verificationMethod!) {
final publicKeyJwk = vm.publicKeyJwk!;
final keyId = publicKeyJwk.computeThumbprint();
final jwk = await keyExporter.export(keyId);
final jwk = await keyManager.export(keyId);

portableDid.privateKeys.add(jwk);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class DidDocumentMetadata {
/// the scope of the containing DID document.
final String? canonicalId;

DidDocumentMetadata({
const DidDocumentMetadata({
this.created,
this.updated,
this.deactivated,
Expand Down
34 changes: 29 additions & 5 deletions packages/web5/lib/src/dids/did_core/did_resolution_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ import 'package:web5/src/dids/did_core/did_document.dart';
import 'package:web5/src/dids/did_core/did_document_metadata.dart';
import 'package:web5/src/dids/did_core/did_resolution_metadata.dart';

enum DidResolutionError {
invalidDid,
notFound,
representationNotSupported,
}

extension ResolutionErrorValue on DidResolutionError {
String get value {
switch (this) {
case DidResolutionError.invalidDid:
return 'invalidDid';
case DidResolutionError.notFound:
return 'notFound';
case DidResolutionError.representationNotSupported:
return 'representationNotSupported';
default:
return 'unknown';
}
}
}

/// A class representing the result of a DID (Decentralized Identifier)
/// resolution.
///
Expand Down Expand Up @@ -61,13 +82,16 @@ class DidResolutionResult {
didResolutionMetadata ?? DidResolutionMetadata(),
didDocumentMetadata = didDocumentMetadata ?? DidDocumentMetadata();

/// A convenience constructor for creating a [DidResolutionResult] representing
/// A factory constructor for creating a [DidResolutionResult] representing
/// an invalid DID scenario. This sets the resolution metadata error to 'invalidDid'
/// and leaves the DID document as `null`.
DidResolutionResult.invalidDid()
: didResolutionMetadata = DidResolutionMetadata(error: 'invalidDid'),
didDocument = null,
didDocumentMetadata = DidDocumentMetadata();
factory DidResolutionResult.withError(DidResolutionError err) {
return DidResolutionResult(
didResolutionMetadata: DidResolutionMetadata(error: err.value),
didDocument: null,
didDocumentMetadata: DidDocumentMetadata(),
);
}

/// Converts this [DidResolutionResult] instance to a JSON map.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:web5/src/crypto.dart';
import 'package:web5/src/dids/did_core/did_resource.dart';
import 'package:web5/src/dids/did_core/did_verification_relationship.dart';

/// A DID document can express verification methods, such as cryptographic
/// public keys, which can be used to authenticate or authorize interactions
Expand Down Expand Up @@ -52,3 +53,19 @@ class DidVerificationMethod implements DidResource {
);
}
}

class DidCreateVerificationMethod {
DidCreateVerificationMethod({
required this.algorithm,
required this.controller,
this.id,
required this.purposes,
required this.type,
});

final AlgorithmId algorithm;
final String controller;
final String? id;
final List<VerificationPurpose> purposes;
final String type;
}
8 changes: 4 additions & 4 deletions packages/web5/lib/src/dids/did_dht/did_dht.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class DidDht {
String relayUrl = 'https://diddht.tbddev.org',
}) async {
if (did.method != methodName) {
return DidResolutionResult.invalidDid();
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final parsedRelayUrl = Uri.parse(relayUrl);
Expand All @@ -42,7 +42,7 @@ class DidDht {
// final seq = bytes.sublist(64, 72);

if (bytes.length < 72) {
return DidResolutionResult.invalidDid();
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final v = bytes.sublist(72);
Expand All @@ -68,7 +68,7 @@ class DidDht {

if (rootRecord == null) {
// TODO: figure out more appopriate resolution error to use.
return DidResolutionResult.invalidDid();
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final Map<String, List<String>> relationshipsMap = {};
Expand All @@ -77,7 +77,7 @@ class DidDht {

if (splitEntry.length != 2) {
// TODO: figure out more appopriate resolution error to use.
return DidResolutionResult.invalidDid();
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final [property, values] = splitEntry;
Expand Down
10 changes: 5 additions & 5 deletions packages/web5/lib/src/dids/did_jwk/did_jwk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,31 +60,31 @@ class DidJwk {
/// an invalid [DidResolutionResult].
///
/// Throws [FormatException] if the JWK parsing fails.
static Future<DidResolutionResult> resolve(Did did) {
static Future<DidResolutionResult> resolve(Did did) async {
if (did.method != methodName) {
return Future.value(DidResolutionResult.invalidDid());
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final dynamic jwk;

try {
jwk = json.fromBase64Url(did.id);
} on FormatException {
return Future.value(DidResolutionResult.invalidDid());
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final Jwk parsedJwk;

try {
parsedJwk = Jwk.fromJson(jwk);
} on Exception {
return Future.value(DidResolutionResult.invalidDid());
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

final didDocument = _createDidDocument(did, parsedJwk);
final didResolutionResult = DidResolutionResult(didDocument: didDocument);

return Future.value(didResolutionResult);
return didResolutionResult;
}

static DidDocument _createDidDocument(Did did, Jwk jwk) {
Expand Down
4 changes: 3 additions & 1 deletion packages/web5/lib/src/dids/did_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ class DidResolver {
try {
did = Did.parse(uri);
} catch (e) {
return Future.value(DidResolutionResult.invalidDid());
return Future.value(
DidResolutionResult.withError(DidResolutionError.invalidDid),
);
}

final resolver = methodResolvers[did.method];
Expand Down
120 changes: 97 additions & 23 deletions packages/web5/lib/src/dids/did_web/did_web.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,123 @@
import 'dart:convert';
import 'dart:io';

import 'package:web5/src/crypto.dart';
import 'package:web5/src/dids/bearer_did.dart';
import 'package:web5/src/dids/did.dart';
import 'package:web5/src/dids/did_core.dart';
import 'package:web5/src/dids/did_method_resolver.dart';
import 'package:web5/src/dids/did.dart';

class DidWeb {
static const String methodName = 'web';
static final resolver = DidMethodResolver(name: methodName, resolve: resolve);

static final DidMethodResolver resolver = DidMethodResolver(
name: methodName,
resolve: resolve,
);

static Future<BearerDid> create({
required String url,
AlgorithmId? algorithm,
KeyManager? keyManager,
List<String>? alsoKnownAs,
List<String>? controllers,
List<DidService>? services,
List<DidCreateVerificationMethod>? verificationMethods,
fingersonfire marked this conversation as resolved.
Show resolved Hide resolved
DidDocumentMetadata? metadata,
}) async {
algorithm ??= AlgorithmId.ed25519;
keyManager ??= InMemoryKeyManager();

final parsed = Uri.tryParse(url);
if (parsed == null) throw 'Unable to parse url $url';
final String didId =
'did:web:${parsed.host}${parsed.pathSegments.join(':')}';

final DidDocument doc = DidDocument(
id: didId,
alsoKnownAs: alsoKnownAs,
controller: controllers ?? didId,
);

final List<DidCreateVerificationMethod> defaultMethods = [
DidCreateVerificationMethod(
algorithm: algorithm,
id: '0',
type: 'JsonWebKey',
controller: didId,
purposes: [
VerificationPurpose.authentication,
VerificationPurpose.assertionMethod,
VerificationPurpose.capabilityDelegation,
VerificationPurpose.capabilityInvocation,
],
),
];

final List<DidCreateVerificationMethod> methodsToAdd =
verificationMethods ?? defaultMethods;

for (final DidCreateVerificationMethod vm in methodsToAdd) {
final Jwk privateKey = await Crypto.generatePrivateKey(vm.algorithm);
fingersonfire marked this conversation as resolved.
Show resolved Hide resolved
final Jwk publicKey = await Crypto.computePublicKey(privateKey);
fingersonfire marked this conversation as resolved.
Show resolved Hide resolved

keyManager.import(privateKey);

final String methodId = '$didId#${vm.id}';
doc.addVerificationMethod(
DidVerificationMethod(
id: methodId,
type: vm.type,
controller: vm.controller,
publicKeyJwk: publicKey,
),
);

for (final VerificationPurpose purpose in vm.purposes) {
doc.addVerificationPurpose(purpose, methodId);
}
}

for (final DidService service in (services ?? [])) {
doc.addService(service);
}

return BearerDid(
uri: didId,
keyManager: keyManager,
document: doc,
metadata: metadata ?? DidDocumentMetadata(),
);
}

static Future<DidResolutionResult> resolve(
Did did, {
HttpClient? client,
}) async {
if (did.method != methodName) {
return DidResolutionResult.invalidDid();
return DidResolutionResult.withError(DidResolutionError.invalidDid);
}

// TODO: http technically not supported. remove after temp use
var resolutionUrl = did.id.replaceAll(':', '/');
if (resolutionUrl.contains('localhost')) {
resolutionUrl = 'http://$resolutionUrl';
} else {
resolutionUrl = 'https://$resolutionUrl';
}
final String documentUrl = Uri.decodeFull(did.id.replaceAll(':', '/'));
Uri? didUri = Uri.tryParse('https://$documentUrl');

if (Uri.parse(resolutionUrl).path.isEmpty) {
resolutionUrl = '$resolutionUrl/.well-known';
}
if (didUri == null) throw 'Unable to parse DID document Url $documentUrl';

resolutionUrl = Uri.decodeFull('$resolutionUrl/did.json');
final parsedUrl = Uri.parse(resolutionUrl);
// If none was specified, use the default path.
if (didUri.path.isEmpty) didUri = didUri.replace(path: '/.well-known');
didUri = didUri.replace(pathSegments: [...didUri.pathSegments, 'did.json']);

final httpClient = client ??= HttpClient();
final request = await httpClient.getUrl(parsedUrl);
final response = await request.close();
final HttpClient httpClient = client ??= HttpClient();
final HttpClientRequest request = await httpClient.getUrl(didUri);
final HttpClientResponse response = await request.close();

if (response.statusCode != 200) {
// TODO: change this to something more appropriate
return DidResolutionResult.invalidDid();
return DidResolutionResult.withError(DidResolutionError.notFound);
}

final str = await response.transform(utf8.decoder).join();
final jsonParsed = json.decode(str);
final doc = DidDocument.fromJson(jsonParsed);
final String str = await response.transform(utf8.decoder).join();
final dynamic jsonParsed = json.decode(str);
final DidDocument doc = DidDocument.fromJson(jsonParsed);

return DidResolutionResult(didDocument: doc);
}
Expand Down
Loading
Loading